diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index d349c2d2..0fd3ef4d 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -18,6 +18,7 @@ Cloud community and you can use Abra to read them and create apps for you. Subcommands: []*cli.Command{ recipeListCommand, recipeVersionCommand, + recipeReleaseCommand, recipeNewCommand, recipeUpgradeCommand, recipeSyncCommand, diff --git a/cli/recipe/release.go b/cli/recipe/release.go new file mode 100644 index 00000000..64ae37ce --- /dev/null +++ b/cli/recipe/release.go @@ -0,0 +1,228 @@ +package recipe + +import ( + "fmt" + "path" + "strconv" + "strings" + + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/tagcmp" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var Push bool +var PushFlag = &cli.BoolFlag{ + Name: "push", + Value: false, + Destination: &Push, +} + +var Dry bool +var DryFlag = &cli.BoolFlag{ + Name: "dry-run", + Value: false, + Aliases: []string{"d"}, + Destination: &Dry, +} + +var Major bool +var MajorFlag = &cli.BoolFlag{ + Name: "major", + Value: false, + Aliases: []string{"ma", "x"}, + Destination: &Major, +} + +var Minor bool +var MinorFlag = &cli.BoolFlag{ + Name: "minor", + Value: false, + Aliases: []string{"mi", "y"}, + Destination: &Minor, +} + +var Patch bool +var PatchFlag = &cli.BoolFlag{ + Name: "patch", + Value: false, + Aliases: []string{"p", "z"}, + Destination: &Patch, +} + +var recipeReleaseCommand = &cli.Command{ + Name: "release", + Usage: "tag a recipe", + Aliases: []string{"rl"}, + ArgsUsage: " []", + Flags: []cli.Flag{ + DryFlag, + PatchFlag, + MinorFlag, + MajorFlag, + PushFlag, + }, + Action: func(c *cli.Context) error { + recipe := internal.ValidateRecipe(c) + directory := path.Join(config.APPS_DIR, recipe.Name) + tagstring := c.Args().Get(1) + imagesTmp := getImageVersions(recipe) + mainApp := getMainApp(recipe) + mainAppVersion := imagesTmp[mainApp] + if mainAppVersion == "" { + logrus.Fatal("Main app's version is empty.") + } + + repo, err := git.PlainOpen(directory) + if err != nil { + logrus.Fatal(err) + } + head, err := repo.Head() + if err != nil { + 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.") + } + tagstring = fmt.Sprintf("%s+%s", tagstring, mainAppVersion) + if Dry { + logrus.Info(fmt.Sprintf("Dry run only. NOT creating tag %s at %s", tagstring, head.Hash())) + return nil + } + + repo.CreateTag(tagstring, head.Hash(), nil) /* &git.CreateTagOptions{ + Message: tag, + })*/ + logrus.Info(fmt.Sprintf("Created tag %s at %s.", tagstring, head.Hash())) + if Push { + if err := repo.Push(&git.PushOptions{}); err != nil { + logrus.Fatal(err) + } + logrus.Info(fmt.Sprintf("Pushed tag %s to remote.", tagstring)) + } + + return nil + } + + // get the latest tag with its hash, name etc + var lastGitTag *object.Tag + iter, err := repo.Tags() + if err != nil { + logrus.Fatal(err) + } + if err := iter.ForEach(func(ref *plumbing.Reference) error { + obj, err := repo.TagObject(ref.Hash()) + if err == nil { + lastGitTag = obj + return nil + } + return err + + }); err != nil { + logrus.Fatal(err) + } + newTag, err := tagcmp.Parse(lastGitTag.Name) + if err != nil { + logrus.Fatal(err) + } + + var newTagString string + if bumpType > 0 { + if Patch { + now, err := strconv.Atoi(newTag.Patch) + if err != nil { + logrus.Fatal(err) + } + newTag.Patch = strconv.Itoa(now + 1) + } else if Minor { + now, err := strconv.Atoi(newTag.Minor) + if err != nil { + logrus.Fatal(err) + } + newTag.Minor = strconv.Itoa(now + 1) + } else if Major { + now, err := strconv.Atoi(newTag.Major) + if err != nil { + logrus.Fatal(err) + } + newTag.Major = strconv.Itoa(now + 1) + } + newTagString = newTag.String() + } else { + // calculate the new tag + var images = make(map[string]tagcmp.Tag) + for name, version := range imagesTmp { + t, err := tagcmp.Parse(version) + if err != nil { + logrus.Fatal(err) + } + images[name] = t + } + } + + newTagString = fmt.Sprintf("%s+%s", newTagString, mainAppVersion) + if Dry { + logrus.Info(fmt.Sprintf("Dry run only. NOT creating tag %s at %s", newTagString, head.Hash())) + return nil + } + + repo.CreateTag(newTagString, head.Hash(), nil) /* &git.CreateTagOptions{ + Message: tag, + })*/ + logrus.Info(fmt.Sprintf("Created tag %s at %s.", newTagString, head.Hash())) + if Push { + if err := repo.Push(&git.PushOptions{}); err != nil { + logrus.Fatal(err) + } + logrus.Info(fmt.Sprintf("Pushed tag %s to remote.", newTagString)) + } + + return nil + + }, +} + +func getImageVersions(recipe recipe.Recipe) map[string]string { + + var services = make(map[string]string) + for _, service := range recipe.Config.Services { + srv := strings.Split(service.Image, ":") + services[srv[0]] = srv[1] + } + + return services +} + +func getMainApp(recipe recipe.Recipe) string { + for _, service := range recipe.Config.Services { + name := service.Name + if name == "app" { + return strings.Split(service.Image, ":")[0] + } + } + return "" +} + +func btoi(b bool) int { + if b { + return 1 + } + return 0 +}