-
Notifications
You must be signed in to change notification settings - Fork 18
Automatic tag helper script #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
ecaf9a9
First draft of the automatic tag script
vicentepinto98 f0b96a5
Create and push tags
vicentepinto98 433d56f
Finish script, add to lint
vicentepinto98 4cdcd82
Unit tests
vicentepinto98 de3b97e
Replace url
vicentepinto98 195ccec
Rename variables
vicentepinto98 f5504ff
Remove key
vicentepinto98 6b27a65
Skip scripts for go1.18
vicentepinto98 a8e6fa7
Test and lint scripts only for go 1.21
vicentepinto98 4e79bd1
Remove go work sum
vicentepinto98 7cd7a29
Updated go.sum
vicentepinto98 964bd6c
Add make commands
vicentepinto98 ea75dfc
Add password argument
vicentepinto98 f183f78
Sanity check
vicentepinto98 e737490
Changes after review
vicentepinto98 3164cbb
Fix lint
vicentepinto98 75dc886
Replace url
vicentepinto98 6e39928
Remove leftover condition
vicentepinto98 28f7004
Update scripts/automatic_tag.go
vicentepinto98 750cc47
Moved logic out of computeLatestVersion
vicentepinto98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"os" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/go-git/go-git/v5" | ||
"github.com/go-git/go-git/v5/config" | ||
"github.com/go-git/go-git/v5/plumbing" | ||
"github.com/go-git/go-git/v5/plumbing/transport/ssh" | ||
"golang.org/x/mod/semver" | ||
) | ||
|
||
const ( | ||
sdkRepo = "[email protected]:stackitcloud/stackit-sdk-go.git" | ||
patch = "patch" | ||
minor = "minor" | ||
allServices = "all-services" | ||
core = "core" | ||
|
||
updateTypeFlag = "update-type" | ||
sshPrivateKeyFilePathFlag = "ssh-private-key-file-path" | ||
passwordFlag = "password" | ||
targetFlag = "target" | ||
) | ||
|
||
var ( | ||
updateTypes = []string{minor, patch} | ||
targets = []string{allServices, core} | ||
usage = "go run automatic_tag.go --update-type [minor|patch] --ssh-private-key-file-path path/to/private-key --password password --target [all-services|core]" | ||
) | ||
|
||
func main() { | ||
if err := run(); err != nil { | ||
fmt.Fprintf(os.Stderr, "Error: %s\n", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func run() error { | ||
var updateType string | ||
var sshPrivateKeyFilePath string | ||
var password string | ||
var target string | ||
|
||
flag.StringVar(&updateType, updateTypeFlag, "", fmt.Sprintf("Update type, must be one of: %s (required)", strings.Join(updateTypes, ","))) | ||
flag.StringVar(&sshPrivateKeyFilePath, sshPrivateKeyFilePathFlag, "", "Path to the ssh private key (required)") | ||
flag.StringVar(&password, passwordFlag, "", "Password of the ssh private key (optional)") | ||
flag.StringVar(&target, targetFlag, allServices, fmt.Sprintf("Create tags for this target, must be one of %s (optional, default is %s)", strings.Join(targets, ","), allServices)) | ||
|
||
flag.Parse() | ||
|
||
validUpdateType := false | ||
for _, t := range updateTypes { | ||
if updateType == t { | ||
validUpdateType = true | ||
break | ||
} | ||
} | ||
if !validUpdateType { | ||
return fmt.Errorf("the provided update type `%s` is not valid, the valid values are: [%s]", updateType, strings.Join(updateTypes, ",")) | ||
} | ||
|
||
validTarget := false | ||
for _, t := range targets { | ||
if target == t { | ||
validTarget = true | ||
break | ||
} | ||
} | ||
if !validTarget { | ||
return fmt.Errorf("the provided target `%s` is not valid, the valid values are: [%s]", target, strings.Join(targets, ",")) | ||
} | ||
|
||
_, err := os.Stat(sshPrivateKeyFilePath) | ||
if err != nil { | ||
return fmt.Errorf("the provided private key file path %s is not valid: %w\nUsage: %s", sshPrivateKeyFilePath, err, usage) | ||
} | ||
|
||
err = automaticTagUpdate(updateType, sshPrivateKeyFilePath, password, target) | ||
if err != nil { | ||
return fmt.Errorf("updating tags: %s", err.Error()) | ||
} | ||
return nil | ||
} | ||
|
||
// automaticTagUpdate goes through all of the existing tags, gets the latest for the target, creates a new one according to the updateType and pushes them | ||
func automaticTagUpdate(updateType, sshPrivateKeyFilePath, password, target string) error { | ||
tempDir, err := os.MkdirTemp("", "") | ||
if err != nil { | ||
return fmt.Errorf("create temporary directory: %w", err) | ||
} | ||
|
||
defer func() { | ||
tempErr := os.RemoveAll(tempDir) | ||
if tempErr != nil { | ||
fmt.Printf("Warning: temporary directory %s could not be removed: %s", tempDir, tempErr.Error()) | ||
} | ||
}() | ||
|
||
publicKeys, err := ssh.NewPublicKeysFromFile("git", sshPrivateKeyFilePath, password) | ||
if err != nil { | ||
return fmt.Errorf("get public keys from private key file: %w", err) | ||
} | ||
|
||
r, err := git.PlainClone(tempDir, false, &git.CloneOptions{ | ||
Auth: publicKeys, | ||
URL: sdkRepo, | ||
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("clone SDK repo: %w", err) | ||
} | ||
|
||
tagrefs, err := r.Tags() | ||
if err != nil { | ||
return fmt.Errorf("get tags: %w", err) | ||
} | ||
|
||
latestTags := map[string]string{} | ||
err = tagrefs.ForEach(func(t *plumbing.Reference) error { | ||
latestTags, err = storeLatestTag(t, latestTags, target) | ||
if err != nil { | ||
return fmt.Errorf("store latest tag: %w", err) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("iterate over existing tags: %w", err) | ||
} | ||
|
||
for module, version := range latestTags { | ||
updatedVersion, err := computeUpdatedVersion(version, updateType) | ||
if err != nil { | ||
fmt.Printf("Error computing updated version for %s with version %s, this tag will be skipped: %s\n", module, version, err.Error()) | ||
continue | ||
} | ||
|
||
var newTag string | ||
switch target { | ||
case core: | ||
if module != "core" { | ||
return fmt.Errorf("%s target was provided but there is a stored latest tag from another service: %s", target, module) | ||
} | ||
newTag = fmt.Sprintf("core/%s", updatedVersion) | ||
case allServices: | ||
newTag = fmt.Sprintf("services/%s/%s", module, updatedVersion) | ||
default: | ||
fmt.Printf("Error computing updated version for %s with version %s, this tag will be skipped: target %s not supported in version increment, fix the script\n", module, version, target) | ||
continue | ||
} | ||
|
||
err = createTag(r, newTag) | ||
if err != nil { | ||
fmt.Printf("Create tag %s returned error: %s\n", newTag, err) | ||
continue | ||
} | ||
fmt.Printf("Created tag %s\n", newTag) | ||
} | ||
|
||
err = pushTags(r, publicKeys) | ||
if err != nil { | ||
return fmt.Errorf("push tags: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// storeLatestTag receives a tag in the form of a plumbing.Reference and a map with the latest tag per service | ||
// It checks if the tag is part of the current target (if it is belonging to a service or to core), | ||
// checks if it is newer than the current latest tag stored in the map and if it is, updates latestTags and returns it | ||
func storeLatestTag(t *plumbing.Reference, latestTags map[string]string, target string) (map[string]string, error) { | ||
tagName, _ := strings.CutPrefix(t.Name().String(), "refs/tags/") | ||
splitTag := strings.Split(tagName, "/") | ||
|
||
switch target { | ||
case core: | ||
if len(splitTag) != 2 || splitTag[0] != "core" { | ||
return latestTags, nil | ||
} | ||
|
||
version := splitTag[1] | ||
if semver.Prerelease(version) != "" { | ||
return latestTags, nil | ||
} | ||
|
||
// invalid (or empty) semantic version are considered less than a valid one | ||
vicentepinto98 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if semver.Compare(latestTags["core"], version) == -1 { | ||
latestTags["core"] = version | ||
} | ||
case allServices: | ||
if len(splitTag) != 3 || splitTag[0] != "services" { | ||
return latestTags, nil | ||
} | ||
|
||
service := splitTag[1] | ||
version := splitTag[2] | ||
if semver.Prerelease(version) != "" { | ||
return latestTags, nil | ||
} | ||
|
||
// invalid (or empty) semantic version are considered less than a valid one | ||
if semver.Compare(latestTags[service], version) == -1 { | ||
latestTags[service] = version | ||
} | ||
default: | ||
return nil, fmt.Errorf("target not supported in storeLatestTag, fix the script") | ||
} | ||
return latestTags, nil | ||
} | ||
|
||
// computeUpdatedVersion returns the updated version according to the update type | ||
// example: for version v0.1.1 and updateType minor, it returns v0.2.0 | ||
func computeUpdatedVersion(version, updateType string) (string, error) { | ||
canonicalVersion := semver.Canonical(version) | ||
splitVersion := strings.Split(canonicalVersion, ".") | ||
if len(splitVersion) != 3 { | ||
return "", fmt.Errorf("invalid canonical version") | ||
} | ||
|
||
switch updateType { | ||
case patch: | ||
patchNumber, err := strconv.Atoi(splitVersion[2]) | ||
if err != nil { | ||
return "", fmt.Errorf("couldnt convert patch number to int") | ||
} | ||
updatedPatchNumber := patchNumber + 1 | ||
splitVersion[2] = fmt.Sprint(updatedPatchNumber) | ||
case minor: | ||
minorNumber, err := strconv.Atoi(splitVersion[1]) | ||
if err != nil { | ||
return "", fmt.Errorf("couldnt convert minor number to int") | ||
} | ||
updatedPatchNumber := minorNumber + 1 | ||
splitVersion[1] = fmt.Sprint(updatedPatchNumber) | ||
splitVersion[2] = "0" | ||
default: | ||
return "", fmt.Errorf("update type not supported in version increment, fix the script") | ||
} | ||
|
||
updatedVersion := strings.Join(splitVersion, ".") | ||
return updatedVersion, nil | ||
} | ||
|
||
func createTag(r *git.Repository, tag string) error { | ||
h, err := r.Head() | ||
if err != nil { | ||
return fmt.Errorf("get HEAD: %w", err) | ||
} | ||
_, err = r.CreateTag(tag, h.Hash(), nil) | ||
if err != nil { | ||
return fmt.Errorf("create tag: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func pushTags(r *git.Repository, publicKeys *ssh.PublicKeys) error { | ||
po := &git.PushOptions{ | ||
Auth: publicKeys, | ||
RemoteName: "origin", | ||
Progress: os.Stdout, | ||
RefSpecs: []config.RefSpec{config.RefSpec("refs/tags/*:refs/tags/*")}, | ||
} | ||
err := r.Push(po) | ||
|
||
if err != nil { | ||
if errors.Is(err, git.NoErrAlreadyUpToDate) { | ||
return fmt.Errorf("origin remote was up to date, no push done") | ||
} | ||
return fmt.Errorf("push to remote origin: %w", err) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.