package recipe import ( "fmt" "strconv" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/spf13/cobra" ) var RecipeSyncCommand = &cobra.Command{ Use: i18n.G("sync [version] [flags]"), Aliases: []string{i18n.G("s")}, Short: i18n.G("Sync recipe version label"), Long: i18n.G(`Generate labels for the main recipe service. By convention, the service named "app" using the following format: coop-cloud.${STACK_NAME}.version= Where [version] can be specifed on the command-line or Abra can attempt to auto-generate it for you. The configuration will be updated on the local file system.`), Args: cobra.RangeArgs(1, 2), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { switch l := len(args); l { case 0: return autocomplete.RecipeNameComplete() case 1: return autocomplete.RecipeVersionComplete(args[0]) default: return nil, cobra.ShellCompDirectiveError } }, Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) mainApp, err := internal.GetMainAppImage(recipe) if err != nil { log.Fatal(err) } imagesTmp, err := getImageVersions(recipe) if err != nil { log.Fatal(err) } mainAppVersion := imagesTmp[mainApp] tags, err := recipe.Tags() if err != nil { log.Fatal(err) } var nextTag string if len(args) == 2 { nextTag = args[1] } if len(tags) == 0 && nextTag == "" { log.Warn(i18n.G("no git tags found for %s", recipe.Name)) if internal.NoInput { log.Fatal(i18n.G("unable to continue, input required for initial version")) } fmt.Println(i18n.G(` The following options are two types of initial semantic version that you can pick for %s that will be published in the recipe catalogue. This follows the semver convention (more on https://semver.org), here is a short cheatsheet 0.1.0: development release, still hacking. when you make a major upgrade you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to using the "x" part when things are stable. 1.0.0: public release, assumed to be working. you already have a stable and reliable deployment of this app and feel relatively confident about it. If you want people to be able alpha test your current config for %s but don't think it is quite reliable, go with 0.1.0 and people will know that things are likely to change. `, recipe.Name, recipe.Name)) var chosenVersion string edPrompt := &survey.Select{ Message: i18n.G("which version do you want to begin with?"), Options: []string{"0.1.0", "1.0.0"}, } if err := survey.AskOne(edPrompt, &chosenVersion); err != nil { log.Fatal(err) } nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) } if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) { latestRelease := tags[len(tags)-1] if err := internal.PromptBumpType("", latestRelease); err != nil { log.Fatal(err) } } if nextTag == "" { repo, err := git.PlainOpen(recipe.Dir) if err != nil { log.Fatal(err) } var lastGitTag tagcmp.Tag iter, err := repo.Tags() if err != nil { log.Fatal(err) } if err := iter.ForEach(func(ref *plumbing.Reference) error { obj, err := repo.TagObject(ref.Hash()) if err != nil { log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash())) return err } tagcmpTag, err := tagcmp.Parse(obj.Name) if err != nil { return err } if (lastGitTag == tagcmp.Tag{}) { lastGitTag = tagcmpTag } else if tagcmpTag.IsGreaterThan(lastGitTag) { lastGitTag = tagcmpTag } return nil }); err != nil { log.Fatal(err) } // bumpType is used to decide what part of the tag should be incremented bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) if bumpType != 0 { // a bitwise check if the number is a power of 2 if (bumpType & (bumpType - 1)) != 0 { log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch")) } } newTag := lastGitTag if bumpType > 0 { if internal.Patch { now, err := strconv.Atoi(newTag.Patch) if err != nil { log.Fatal(err) } newTag.Patch = strconv.Itoa(now + 1) } else if internal.Minor { now, err := strconv.Atoi(newTag.Minor) if err != nil { log.Fatal(err) } newTag.Patch = "0" newTag.Minor = strconv.Itoa(now + 1) } else if internal.Major { now, err := strconv.Atoi(newTag.Major) if err != nil { log.Fatal(err) } newTag.Patch = "0" newTag.Minor = "0" newTag.Major = strconv.Itoa(now + 1) } } newTag.Metadata = mainAppVersion log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name)) nextTag = newTag.String() } if _, err := tagcmp.Parse(nextTag); err != nil { log.Fatal(i18n.G("invalid version %s specified", nextTag)) } mainService := "app" label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", nextTag) if !internal.Dry { if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { log.Fatal(err) } } else { log.Info(i18n.G("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)) } isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name)) if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { log.Fatal(err) } } }, } func init() { RecipeSyncCommand.Flags().BoolVarP( &internal.Dry, i18n.G("dry-run"), i18n.G("r"), false, i18n.G("report changes that would be made"), ) RecipeSyncCommand.Flags().BoolVarP( &internal.Major, i18n.G("major"), i18n.G("x"), false, i18n.G("increase the major part of the version"), ) RecipeSyncCommand.Flags().BoolVarP( &internal.Minor, i18n.G("minor"), i18n.G("y"), false, i18n.G("increase the minor part of the version"), ) RecipeSyncCommand.Flags().BoolVarP( &internal.Patch, i18n.G("patch"), i18n.G("z"), false, i18n.G("increase the patch part of the version"), ) }