feat: tag recipes with abra #99

Merged
decentral1se merged 5 commits from knoflook/abra:recipe-release into main 2021-09-29 12:39:36 +00:00
2 changed files with 229 additions and 0 deletions

View File

@ -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,

228
cli/recipe/release.go Normal file
View File

@ -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,
knoflook marked this conversation as resolved Outdated

This block is all done in internal.ValidateRecipe(c) already.

This block is all done in `internal.ValidateRecipe(c)` already.
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
knoflook marked this conversation as resolved Outdated

The thing is, most versions are not strictly semver so we'd only be able to cover a tiny amount of the recipes we have. But we've already got tagcmp which can handle discovering what is a patch/minor/major upgrade and these formats are supported. So then we just need to check if it IsParsable?

The thing is, most versions are not strictly semver so we'd only be able to cover a tiny amount of the recipes we have. But we've already got `tagcmp` which can handle discovering what is a patch/minor/major upgrade and [these formats](https://git.coopcloud.tech/coop-cloud/tagcmp#types-of-versions-supported) are supported. So then we just need to check if it [`IsParsable`](https://git.coopcloud.tech/coop-cloud/tagcmp/src/branch/main/godoc.md#user-content-func-isparsable)?
}
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: "<recipe> [<tag>]",
Flags: []cli.Flag{
DryFlag,
PatchFlag,
MinorFlag,
MajorFlag,
PushFlag,
knoflook marked this conversation as resolved Outdated

(from recipe.EnsureVersion)

repo, err := git.PlainOpen(directory)
if err != nil {
    return err
}

tags, err := repo.Tags()
if err != nil {
    return nil
}
(from `recipe.EnsureVersion`) ```golang repo, err := git.PlainOpen(directory) if err != nil { return err } tags, err := repo.Tags() if err != nil { return nil } ```
},
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
}