abra/cli/recipe/release.go

455 lines
11 KiB
Go
Raw Normal View History

2021-09-22 14:03:56 +00:00
package recipe
import (
"fmt"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
2021-12-21 01:04:31 +00:00
"coopcloud.tech/abra/pkg/autocomplete"
2021-09-22 14:03:56 +00:00
"coopcloud.tech/abra/pkg/config"
2021-12-28 00:24:23 +00:00
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
2021-09-22 14:03:56 +00:00
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
2021-09-22 14:03:56 +00:00
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
2021-09-22 14:03:56 +00:00
)
var recipeReleaseCommand = cli.Command{
2021-09-22 14:03:56 +00:00
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
2021-11-06 21:38:29 +00:00
ArgsUsage: "<recipe> [<version>]",
2021-09-29 20:36:43 +00:00
Description: `
2021-12-31 15:48:03 +00:00
This command is used to specify a new version of a recipe. These versions are
then published on the Co-op Cloud recipe catalogue. These versions take the
following form:
2021-09-29 20:36:43 +00:00
a.b.c+x.y.z
Where the "a.b.c" part is a semantic version determined by the 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).
2021-09-29 20:36:43 +00:00
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
2021-12-31 15:48:03 +00:00
versioning scheme ("x.y.z") in order to maximise the chances that the nature of
recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are
major and therefore require intervention while doing the upgrade work.
2021-12-31 15:48:03 +00:00
Publish your new release to git.coopcloud.tech with "-p/--publish". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.
2021-09-29 20:36:43 +00:00
`,
2021-09-22 14:03:56 +00:00
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
2021-12-27 18:56:27 +00:00
internal.PublishFlag,
2021-09-22 14:03:56 +00:00
},
Before: internal.SubCommandBefore,
2021-12-21 01:04:31 +00:00
BashComplete: autocomplete.RecipeNameComplete,
2021-09-22 14:03:56 +00:00
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
logrus.Fatal(err)
}
2021-12-27 18:56:27 +00:00
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
}
tagString := c.Args().Get(1)
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
}
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
logrus.Fatal("cannot specify tag and bump type at the same time")
}
if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
logrus.Fatal(err)
}
}
tags, err := recipe.Tags()
if err != nil {
2021-12-25 16:02:47 +00:00
logrus.Fatal(err)
}
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
2021-12-28 02:16:23 +00:00
var err error
tagString, err = getLabelVersion(recipe, false)
2021-12-28 02:16:23 +00:00
if err != nil {
logrus.Fatal(err)
}
}
if len(tags) > 0 {
2021-12-22 00:36:29 +00:00
logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
2021-12-28 02:16:23 +00:00
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
2021-12-22 13:01:49 +00:00
logrus.Fatal(cleanUpErr)
}
logrus.Fatal(err)
}
}
return nil
},
}
// getImageVersions retrieves image versions for a recipe
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
var services = make(map[string]string)
missingTag := false
for _, service := range recipe.Config.Services {
if service.Image == "" {
continue
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return services, err
}
path := reference.Path(img)
path = recipePkg.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
if service.Name == "app" {
missingTag = true
}
continue
}
services[path] = tag
}
if missingTag {
return services, fmt.Errorf("app service is missing image tag?")
}
return services, nil
}
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
2021-12-25 13:04:07 +00:00
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
tag, err := tagcmp.Parse(tagString)
if err != nil {
return err
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
if tagString == "" {
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
}
2021-12-27 18:56:27 +00:00
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err)
}
2021-12-28 02:40:18 +00:00
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
return nil
}
// btoi converts a boolean value into an integer
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// getTagCreateOptions constructs git tag create options
2021-12-27 18:56:27 +00:00
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := fmt.Sprintf("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil
}
2021-12-27 18:56:27 +00:00
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
2021-12-27 18:56:27 +00:00
logrus.Debugf("dry run: no changes committed")
return nil
}
2021-12-27 18:56:27 +00:00
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
return err
}
2021-12-27 18:56:27 +00:00
if isClean {
if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
}
}
2021-12-21 01:08:51 +00:00
msg := fmt.Sprintf("chore: publish %s release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, ".", msg, internal.Dry); err != nil {
return err
}
return nil
}
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
2021-12-27 18:56:27 +00:00
logrus.Debugf("dry run: no git tag created (%s)", tagString)
return nil
}
head, err := repo.Head()
if err != nil {
return err
}
2021-12-27 18:56:27 +00:00
createTagOptions, err := getTagCreateOptions(tagString)
if err != nil {
return err
}
_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
if err != nil {
return err
}
2021-12-28 00:24:23 +00:00
hash := formatter.SmallSHA(head.Hash().String())
2021-12-27 18:56:27 +00:00
logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil
}
2021-12-28 02:40:18 +00:00
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
2021-12-27 18:56:27 +00:00
logrus.Info("dry run: no changes published")
return nil
}
2021-12-27 18:56:27 +00:00
if !internal.Publish && !internal.NoInput {
prompt := &survey.Confirm{
2021-12-27 18:56:27 +00:00
Message: "publish new release?",
2021-09-22 14:03:56 +00:00
}
2021-12-27 18:56:27 +00:00
if err := survey.AskOne(prompt, &internal.Publish); err != nil {
return err
2021-09-23 16:52:21 +00:00
}
}
2021-09-22 14:03:56 +00:00
2021-12-27 18:56:27 +00:00
if internal.Publish {
if err := recipe.Push(internal.Dry); err != nil {
2021-12-23 01:24:43 +00:00
return err
}
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
logrus.Infof("new release published: %s", url)
} else {
logrus.Info("no -p/--publish passed, not publishing")
}
2021-12-27 18:56:27 +00:00
return nil
2021-09-22 14:03:56 +00:00
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
2021-12-25 13:04:07 +00:00
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch")
}
}
var lastGitTag tagcmp.Tag
if tagString == "" {
if err := internal.PromptBumpType(tagString); err != nil {
return err
}
}
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = parsed
} else if parsed.IsGreaterThan(lastGitTag) {
lastGitTag = parsed
}
}
newTag := lastGitTag
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
return err
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion
tagString = newTag.String()
}
2021-09-22 14:03:56 +00:00
if lastGitTag.String() == tagString {
logrus.Fatalf("latest git tag (%s) and synced lable (%s) are the same?", lastGitTag, tagString)
}
2021-12-28 00:51:39 +00:00
if !internal.NoInput {
prompt := &survey.Confirm{
Message: fmt.Sprintf("current: %s, new: %s, correct?", lastGitTag, tagString),
2021-12-28 00:51:39 +00:00
}
var ok bool
if err := survey.AskOne(prompt, &ok); err != nil {
logrus.Fatal(err)
}
if !ok {
logrus.Fatal("exiting as requested")
}
}
if err := commitRelease(recipe, tagString); err != nil {
2021-12-27 18:56:27 +00:00
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err)
2021-09-22 14:03:56 +00:00
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
return nil
2021-09-22 14:03:56 +00:00
}
2021-12-22 13:01:49 +00:00
// cleanUpTag removes a freshly created tag
func cleanUpTag(tag, recipeName string) error {
2021-12-25 13:04:07 +00:00
directory := path.Join(config.RECIPES_DIR, recipeName)
2021-12-22 13:01:49 +00:00
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
if err := repo.DeleteTag(tag); err != nil {
2021-12-28 02:16:23 +00:00
if !strings.Contains(err.Error(), "not found") {
return err
}
2021-12-22 13:01:49 +00:00
}
2021-12-27 18:56:27 +00:00
logrus.Debugf("removed freshly created tag %s", tag)
2021-12-22 13:01:49 +00:00
return nil
}
2021-12-28 02:16:23 +00:00
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
2021-12-28 02:16:23 +00:00
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil {
return "", err
}
if initTag == "" {
logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
}
logrus.Warnf("discovered %s as currently synced recipe label", initTag)
if prompt && !internal.NoInput {
2021-12-28 02:16:23 +00:00
var response bool
prompt := &survey.Confirm{Message: fmt.Sprintf("use %s as the new version?", initTag)}
if err := survey.AskOne(prompt, &response); err != nil {
return "", err
}
if !response {
return "", fmt.Errorf("please fix your synced label for %s and re-run this command", recipe.Name)
}
}
return initTag, nil
}