diff --git a/cli/formatter/formatter.go b/cli/formatter/formatter.go index 20b69637a..0c32387fb 100644 --- a/cli/formatter/formatter.go +++ b/cli/formatter/formatter.go @@ -20,6 +20,10 @@ func Truncate(str string) string { return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19)) } +func SmallSHA(hash string) string { + return hash[:8] +} + // RemoveSha remove image sha from a string that are added in some docker outputs func RemoveSha(str string) string { return strings.Split(str, "@")[0] diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index cc6e09c95..c02200d31 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -63,7 +63,14 @@ var recipeLintCommand = &cli.Command{ allImagesTagged = false } - tag := img.(reference.NamedTagged).Tag() + var tag string + switch img.(type) { + case reference.NamedTagged: + tag = img.(reference.NamedTagged).Tag() + case reference.Named: + noUnstableTags = false + } + if tag == "latest" { noUnstableTags = false } diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index a12f8aab6..744d57863 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -7,6 +7,7 @@ import ( var Major bool var MajorFlag = &cli.BoolFlag{ Name: "major", + Usage: "Increase the major part of the version (new functionality, backwards incompatible, x of x.y.z)", Value: false, Aliases: []string{"ma", "x"}, Destination: &Major, @@ -15,6 +16,7 @@ var MajorFlag = &cli.BoolFlag{ var Minor bool var MinorFlag = &cli.BoolFlag{ Name: "minor", + Usage: "Increase the minor part of the version (new functionality, backwards compatible, y of x.y.z)", Value: false, Aliases: []string{"mi", "y"}, Destination: &Minor, @@ -23,6 +25,7 @@ var MinorFlag = &cli.BoolFlag{ var Patch bool var PatchFlag = &cli.BoolFlag{ Name: "patch", + Usage: "Increase the patch part of the version (bug fixes, backwards compatible, z of x.y.z)", Value: false, Aliases: []string{"p", "z"}, Destination: &Patch, diff --git a/cli/recipe/release.go b/cli/recipe/release.go index 62e06f219..563f0443e 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -6,12 +6,14 @@ import ( "strconv" "strings" + abraFormatter "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" + "github.com/docker/distribution/reference" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/sirupsen/logrus" @@ -21,13 +23,16 @@ import ( var Push bool var PushFlag = &cli.BoolFlag{ Name: "push", + Usage: "Git push changes", Value: false, + Aliases: []string{"P"}, Destination: &Push, } var Dry bool var DryFlag = &cli.BoolFlag{ Name: "dry-run", + Usage: "No changes are made, only reports changes that would be made", Value: false, Aliases: []string{"d"}, Destination: &Dry, @@ -36,7 +41,7 @@ var DryFlag = &cli.BoolFlag{ var CommitMessage string var CommitMessageFlag = &cli.StringFlag{ Name: "commit-message", - Usage: "commit message. Implies --commit", + Usage: "Commit message (implies --commit)", Aliases: []string{"cm"}, Destination: &CommitMessage, } @@ -44,7 +49,7 @@ var CommitMessageFlag = &cli.StringFlag{ var Commit bool var CommitFlag = &cli.BoolFlag{ Name: "commit", - Usage: "add compose.yml to staging area and commit changes", + Usage: "Commits compose.**yml file changes to recipe repository", Value: false, Aliases: []string{"c"}, Destination: &Commit, @@ -53,7 +58,7 @@ var CommitFlag = &cli.BoolFlag{ var TagMessage string var TagMessageFlag = &cli.StringFlag{ Name: "tag-comment", - Usage: "tag comment. If not given, user will be asked for it", + Usage: "Description for release tag", Aliases: []string{"t", "tm"}, Destination: &TagMessage, } @@ -83,23 +88,32 @@ updates are properly communicated. Abra does its best to read the "a.b.c" version scheme and communicate what action needs to be taken when performing different operations such as an update or a rollback of an app. + +You may invoke this command in "wizard" mode and be prompted for input: + + abra recipe release gitea + `, Flags: []cli.Flag{ DryFlag, - PatchFlag, - MinorFlag, MajorFlag, + MinorFlag, + PatchFlag, PushFlag, CommitFlag, CommitMessageFlag, TagMessageFlag, }, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipe(c) + recipe := internal.ValidateRecipeWithPrompt(c) directory := path.Join(config.APPS_DIR, recipe.Name) - tagstring := c.Args().Get(1) - imagesTmp := getImageVersions(recipe) + tagString := c.Args().Get(1) mainApp := getMainApp(recipe) + + imagesTmp, err := getImageVersions(recipe) + if err != nil { + logrus.Fatal(err) + } mainAppVersion := imagesTmp[mainApp] if err := recipePkg.EnsureExists(recipe.Name); err != nil { @@ -107,18 +121,55 @@ or a rollback of an app. } if mainAppVersion == "" { - logrus.Fatal("main app version is empty?") + logrus.Fatalf("main 'app' service version for %s is empty?", recipe.Name) } - if tagstring != "" { - if _, err := tagcmp.Parse(tagstring); err != nil { + if tagString != "" { + if _, err := tagcmp.Parse(tagString); err != nil { logrus.Fatal("invalid tag specified") } } + if (!Major && !Minor && !Patch) && tagString != "" { + logrus.Fatal("please specify or bump type (--major/--minor/--patch)") + } + + if (Major || Minor || Patch) && tagString != "" { + logrus.Fatal("cannot specify tag and bump type at the same time") + } + + // bumpType is used to decide what part of the tag should be incremented + bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch) + if bumpType != 0 { + // a bitwise check if the number is a power of 2 + if (bumpType & (bumpType - 1)) != 0 { + logrus.Fatal("you can only use one of: --major, --minor, --patch.") + } + } + + if (!Major && !Minor && !Patch) && tagString == "" { + fmt.Printf(` +semver cheat sheet (more via semver.org): + major: new features/bug fixes, backwards incompatible + minor: new features/bug fixes, backwards compatible + patch: bug fixes, backwards compatible + +`) + var chosenBumpType string + prompt := &survey.Select{ + Message: fmt.Sprintf("select recipe version increment type"), + Options: []string{"major", "minor", "patch"}, + } + if err := survey.AskOne(prompt, &chosenBumpType); err != nil { + logrus.Fatal(err) + } + setBumpType(chosenBumpType) + } + if TagMessage == "" { prompt := &survey.Input{ Message: "tag message", + Default: fmt.Sprintf("chore: publish new %s version", getBumpType()), } if err := survey.AskOne(prompt, &TagMessage); err != nil { logrus.Fatal(err) @@ -128,7 +179,25 @@ or a rollback of an app. var createTagOptions git.CreateTagOptions createTagOptions.Message = TagMessage - if Commit || (CommitMessage != "") { + if !Commit { + prompt := &survey.Confirm{ + Message: "git commit changes also?", + } + if err := survey.AskOne(prompt, &Commit); err != nil { + return err + } + } + + if !Push { + prompt := &survey.Confirm{ + Message: "git push changes also?", + } + if err := survey.AskOne(prompt, &Push); err != nil { + return err + } + } + + if Commit || CommitMessage != "" { commitRepo, err := git.PlainOpen(directory) if err != nil { logrus.Fatal(err) @@ -141,22 +210,28 @@ or a rollback of an app. if CommitMessage == "" { prompt := &survey.Input{ Message: "commit message", + Default: fmt.Sprintf("chore: publish new %s version", getBumpType()), } if err := survey.AskOne(prompt, &CommitMessage); err != nil { logrus.Fatal(err) } } + err = commitWorktree.AddGlob("compose.**yml") if err != nil { logrus.Fatal(err) } logrus.Debug("staged compose.**yml for commit") - _, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{}) - if err != nil { - logrus.Fatal(err) + if !Dry { + _, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{}) + if err != nil { + logrus.Fatal(err) + } + logrus.Info("changes commited") + } else { + logrus.Info("dry run only: NOT committing changes") } - logrus.Info("changes commited") } repo, err := git.PlainOpen(directory) @@ -168,20 +243,8 @@ or a rollback of an app. logrus.Fatal(err) } - // bumpType is used to decide what part of the tag should be incremented - bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch) - if bumpType != 0 { - // a bitwise check if the number is a power of 2 - if (bumpType & (bumpType - 1)) != 0 { - logrus.Fatal("you can only use one of: --major, --minor, --patch.") - } - } - - if tagstring != "" { - if bumpType > 0 { - logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...") - } - tag, err := tagcmp.Parse(tagstring) + if tagString != "" { + tag, err := tagcmp.Parse(tagString) if err != nil { logrus.Fatal(err) } @@ -193,19 +256,23 @@ or a rollback of an app. tag.Patch = "0" tag.MissingPatch = false } - tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion) + tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion) if Dry { - logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash())) + hash := abraFormatter.SmallSHA(head.Hash().String()) + logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagString, hash)) return nil } - repo.CreateTag(tagstring, head.Hash(), &createTagOptions) - logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash())) - if Push { + repo.CreateTag(tagString, head.Hash(), &createTagOptions) + hash := abraFormatter.SmallSHA(head.Hash().String()) + logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash)) + if Push && !Dry { if err := repo.Push(&git.PushOptions{}); err != nil { logrus.Fatal(err) } - logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring)) + logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagString)) + } else { + logrus.Info("dry run only: NOT pushing changes") } return nil @@ -236,10 +303,8 @@ or a rollback of an app. logrus.Fatal(err) } - fmt.Println(lastGitTag) - newTag := lastGitTag - var newTagString string + var newtagString string if bumpType > 0 { if Patch { now, err := strconv.Atoi(newTag.Patch) @@ -263,44 +328,66 @@ or a rollback of an app. newTag.Minor = "0" newTag.Major = strconv.Itoa(now + 1) } - } else { - logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch") } + newTag.Metadata = mainAppVersion - newTagString = newTag.String() + newtagString = newTag.String() if Dry { - logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash())) + hash := abraFormatter.SmallSHA(head.Hash().String()) + logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newtagString, hash)) return nil } - repo.CreateTag(newTagString, head.Hash(), &createTagOptions) - logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash())) - if Push { + repo.CreateTag(newtagString, head.Hash(), &createTagOptions) + hash := abraFormatter.SmallSHA(head.Hash().String()) + logrus.Info(fmt.Sprintf("created tag %s at %s", newtagString, hash)) + if Push && !Dry { if err := repo.Push(&git.PushOptions{}); err != nil { logrus.Fatal(err) } - logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString)) + logrus.Info(fmt.Sprintf("pushed tag %s to remote", newtagString)) + } else { + logrus.Info("dry run only: NOT pushing changes") } return nil - }, } -func getImageVersions(recipe recipe.Recipe) map[string]string { - +// getImageVersions retrieves image versions for a recipe +func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { var services = make(map[string]string) + for _, service := range recipe.Config.Services { if service.Image == "" { continue } - srv := strings.Split(service.Image, ":") - services[srv[0]] = srv[1] + + img, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + return services, err + } + + path := reference.Path(img) + if strings.Contains(path, "library") { + path = strings.Split(path, "/")[1] + } + + var tag string + switch img.(type) { + case reference.NamedTagged: + tag = img.(reference.NamedTagged).Tag() + case reference.Named: + logrus.Fatalf("%s service is missing image tag?", path) + } + + services[path] = tag } - return services + return services, nil } +// getMainApp retrieves the main 'app' image name func getMainApp(recipe recipe.Recipe) string { for _, service := range recipe.Config.Services { name := service.Name @@ -308,12 +395,45 @@ func getMainApp(recipe recipe.Recipe) string { return strings.Split(service.Image, ":")[0] } } + return "" } +// btoi converts a boolean value into an integer func btoi(b bool) int { if b { return 1 } + return 0 } + +// getBumpType figures out which bump type is specified +func getBumpType() string { + var bumpType string + + if Major { + bumpType = "major" + } else if Minor { + bumpType = "minor" + } else if Patch { + bumpType = "patch" + } else { + logrus.Fatal("no version bump type specififed?") + } + + return bumpType +} + +// setBumpType figures out which bump type is specified +func setBumpType(bumpType string) { + if bumpType == "major" { + Major = true + } else if bumpType == "minor" { + Minor = true + } else if bumpType == "patch" { + Patch = true + } else { + logrus.Fatal("no version bump type specififed?") + } +}