abra/cli/recipe/release.go

316 lines
7.6 KiB
Go

package recipe
import (
"fmt"
"path"
"strconv"
"strings"
"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/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"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 CommitMessage string
var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message",
Usage: "commit message. Implies --commit",
Aliases: []string{"cm"},
Destination: &CommitMessage,
}
var Commit bool
var CommitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "add compose.yml to staging area and commit changes",
Value: false,
Aliases: []string{"c"},
Destination: &Commit,
}
var TagMessage string
var TagMessageFlag = &cli.StringFlag{
Name: "tag-comment",
Usage: "tag comment. If not given, user will be asked for it",
Aliases: []string{"t", "tm"},
Destination: &TagMessage,
}
var recipeReleaseCommand = &cli.Command{
Name: "release",
Usage: "tag a recipe",
Aliases: []string{"rl"},
ArgsUsage: "<recipe> [<tag>]",
Description: `
This command is used to specify a new tag for a recipe. These tags are used to
identify different versions of the recipe and are published on the Co-op Cloud
recipe catalogue.
These tags take the following form:
a.b.c+x.y.z
Where the "a.b.c" part is maintained as a semantic version of the recipe by the
recipe maintainer. And the "x.y.z" part is the image tag of the recipe "app"
service (the main container which contains the software to be used).
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
versioning scheme in order to maximise the chances that the nature of recipe
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.
`,
Flags: []cli.Flag{
DryFlag,
PatchFlag,
MinorFlag,
MajorFlag,
PushFlag,
CommitFlag,
CommitMessageFlag,
TagMessageFlag,
},
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 err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if mainAppVersion == "" {
logrus.Fatal("main app version is empty?")
}
if tagstring != "" {
if _, err := tagcmp.Parse(tagstring); err != nil {
logrus.Fatal("invalid tag specified")
}
}
if TagMessage == "" {
prompt := &survey.Input{
Message: "tag message",
}
survey.AskOne(prompt, &TagMessage)
}
var createTagOptions git.CreateTagOptions
createTagOptions.Message = TagMessage
if Commit || (CommitMessage != "") {
commitRepo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if CommitMessage == "" {
prompt := &survey.Input{
Message: "commit message",
}
survey.AskOne(prompt, &CommitMessage)
}
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)
}
logrus.Info("changes commited")
}
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...")
}
tag, err := tagcmp.Parse(tagstring)
if err != nil {
logrus.Fatal(err)
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
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()))
return nil
}
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
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 tagcmp.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 {
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 {
logrus.Fatal(err)
}
fmt.Println(lastGitTag)
newTag := lastGitTag
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.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
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()
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(), &createTagOptions)
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 {
if (service.Image == "") {
continue
}
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
}