forked from toolshed/abra
		
	
		
			
				
	
	
		
			677 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			677 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package recipe
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/autocomplete"
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	gitPkg "coopcloud.tech/abra/pkg/git"
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"coopcloud.tech/abra/pkg/recipe"
 | |
| 	"coopcloud.tech/tagcmp"
 | |
| 	"github.com/AlecAivazis/survey/v2"
 | |
| 	"github.com/distribution/reference"
 | |
| 	"github.com/go-git/go-git/v5"
 | |
| 	"github.com/go-git/go-git/v5/plumbing"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| // translators: `abra recipe release` aliases. use a comma separated list of
 | |
| // aliases with no spaces in between
 | |
| var recipeReleaseAliases = i18n.G("rl")
 | |
| 
 | |
| var RecipeReleaseCommand = &cobra.Command{
 | |
| 	// translators: `recipe release` command
 | |
| 	Use:     i18n.G("release <recipe> [version] [flags]"),
 | |
| 	Aliases: strings.Split(recipeReleaseAliases, ","),
 | |
| 	// translators: Short description for `recipe release` command
 | |
| 	Short: i18n.G("Release a new recipe version"),
 | |
| 	Long: i18n.G(`Create a new version of a recipe.
 | |
| 
 | |
| These versions are then published on the Co-op Cloud recipe catalogue. These
 | |
| versions take the following form:
 | |
| 
 | |
|     a.b.c+x.y.z
 | |
| 
 | |
| Where the "a.b.c" part is a semantic version determined by the maintainer. The
 | |
| "x.y.z" part is the image tag of the recipe "app" service (the main container
 | |
| which contains the software to be used, by naming convention).
 | |
| 
 | |
| We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
 | |
| 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.
 | |
| 
 | |
| Publish your new release to git.coopcloud.tech with "--publish/-p". This
 | |
| requires that you have permission to git push to these repositories and have
 | |
| your SSH keys configured on your account. Enable ssh-agent and make sure to add
 | |
| your private key and enter your passphrase beforehand.
 | |
| 
 | |
|     eval ` + "`ssh-agent`" + `
 | |
|     ssh-add ~/.ssh/<my-ssh-private-key-for-git-coopcloud-tech>`),
 | |
| 	Example: `  # publish release
 | |
|   eval ` + "`ssh-agent`" + `
 | |
|   ssh-add ~/.ssh/id_ed25519
 | |
|   abra recipe release gitea -p`,
 | |
| 	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.ShellCompDirectiveDefault
 | |
| 		}
 | |
| 	},
 | |
| 	Run: func(cmd *cobra.Command, args []string) {
 | |
| 		recipe := internal.ValidateRecipe(args, cmd.Name())
 | |
| 
 | |
| 		imagesTmp, err := GetImageVersions(recipe)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		mainApp, err := internal.GetMainAppImage(recipe)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		mainAppVersion := imagesTmp[mainApp]
 | |
| 		if mainAppVersion == "" {
 | |
| 			log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
 | |
| 		}
 | |
| 
 | |
| 		var tagString string
 | |
| 		if len(args) == 2 {
 | |
| 			tagString = args[1]
 | |
| 		}
 | |
| 
 | |
| 		if tagString != "" {
 | |
| 			if _, err := tagcmp.Parse(tagString); err != nil {
 | |
| 				log.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
 | |
| 			log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
 | |
| 		}
 | |
| 
 | |
| 		repo, err := git.PlainOpen(recipe.Dir)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		preCommitHead, err := repo.Head()
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if tagString != "" {
 | |
| 			if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
 | |
| 				if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		tags, err := recipe.Tags()
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
 | |
| 			var err error
 | |
| 			tagString, err = getLabelVersion(recipe, false)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		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)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(tags) > 0 {
 | |
| 			log.Warn(i18n.G("previous git tags detected, assuming new semver release"))
 | |
| 
 | |
| 			if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
 | |
| 				if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		} else {
 | |
| 			log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming initial release", recipe.Name))
 | |
| 
 | |
| 			if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
 | |
| 				if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
 | |
| 					log.Fatal(cleanErr)
 | |
| 				}
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // GetImageVersions retrieves image versions for a recipe
 | |
| func GetImageVersions(recipe recipe.Recipe) (map[string]string, error) {
 | |
| 	services := make(map[string]string)
 | |
| 
 | |
| 	config, err := recipe.GetComposeConfig(nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	missingTag := false
 | |
| 	for _, service := range config.Services {
 | |
| 		if service.Image == "" {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		img, err := reference.ParseNormalizedNamed(service.Image)
 | |
| 		if err != nil {
 | |
| 			return services, err
 | |
| 		}
 | |
| 
 | |
| 		path := reference.Path(img)
 | |
| 
 | |
| 		path = formatter.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, errors.New(i18n.G("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
 | |
| 
 | |
| 	repo, err := git.PlainOpen(recipe.Dir)
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	if err := addReleaseNotes(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to add release notes: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := commitRelease(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to commit changes: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := tagRelease(tagString, repo); err != nil {
 | |
| 		return errors.New(i18n.G("failed to tag release: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := pushRelease(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to publish new release: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
 | |
| 	msg := i18n.G("chore: publish %s release", tag)
 | |
| 	return git.CreateTagOptions{Message: msg}, nil
 | |
| }
 | |
| 
 | |
| // addReleaseNotes checks if the release/next release note exists and moves the
 | |
| // file to release/<tag>.
 | |
| func addReleaseNotes(recipe recipe.Recipe, tag string) error {
 | |
| 	releaseDir := path.Join(recipe.Dir, "release")
 | |
| 	if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
 | |
| 		if err := os.Mkdir(releaseDir, 0755); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	tagReleaseNotePath := path.Join(releaseDir, tag)
 | |
| 	if _, err := os.Stat(tagReleaseNotePath); err == nil {
 | |
| 		// Release note for current tag already exist exists.
 | |
| 		return nil
 | |
| 	} else if !errors.Is(err, os.ErrNotExist) {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	var addNextAsReleaseNotes bool
 | |
| 
 | |
| 	nextReleaseNotePath := path.Join(releaseDir, "next")
 | |
| 	if _, err := os.Stat(nextReleaseNotePath); err == nil {
 | |
| 		// release/next note exists. Move it to release/<tag>
 | |
| 		if internal.Dry {
 | |
| 			log.Debug(i18n.G("dry run: move release note from 'next' to %s", tag))
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		if !internal.NoInput {
 | |
| 			prompt := &survey.Confirm{
 | |
| 				Message: i18n.G("Use release note in release/next?"),
 | |
| 			}
 | |
| 
 | |
| 			if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			if !addNextAsReleaseNotes {
 | |
| 				return nil
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	} else if !errors.Is(err, os.ErrNotExist) {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// NOTE(d1): No release note exists for the current release. Or, we've
 | |
| 	// already used release/next as the release note
 | |
| 	if internal.NoInput || addNextAsReleaseNotes {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	prompt := &survey.Input{
 | |
| 		Message: i18n.G("add release note? (leave empty to skip)"),
 | |
| 	}
 | |
| 
 | |
| 	var releaseNote string
 | |
| 	if err := survey.AskOne(prompt, &releaseNote); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if releaseNote == "" {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func commitRelease(recipe recipe.Recipe, tag string) error {
 | |
| 	if internal.Dry {
 | |
| 		log.Debug(i18n.G("dry run: no changes committed"))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	isClean, err := gitPkg.IsClean(recipe.Dir)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if isClean {
 | |
| 		if !internal.Dry {
 | |
| 			return errors.New(i18n.G("no changes discovered in %s, nothing to publish?", recipe.Dir))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	msg := fmt.Sprintf("chore: publish %s release", tag)
 | |
| 	if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func tagRelease(tagString string, repo *git.Repository) error {
 | |
| 	if internal.Dry {
 | |
| 		log.Debug(i18n.G("dry run: no git tag created (%s)", tagString))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	head, err := repo.Head()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	createTagOptions, err := getTagCreateOptions(tagString)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	hash := formatter.SmallSHA(head.Hash().String())
 | |
| 	log.Debug(i18n.G("created tag %s at %s", tagString, hash))
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func pushRelease(recipe recipe.Recipe, tagString string) error {
 | |
| 	if internal.Dry {
 | |
| 		log.Info(i18n.G("dry run: no changes published"))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if !publish && !internal.NoInput {
 | |
| 		prompt := &survey.Confirm{
 | |
| 			Message: i18n.G("publish new release?"),
 | |
| 		}
 | |
| 
 | |
| 		if err := survey.AskOne(prompt, &publish); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if publish {
 | |
| 		if os.Getenv("SSH_AUTH_SOCK") == "" {
 | |
| 			return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
 | |
| 		}
 | |
| 
 | |
| 		if err := recipe.Push(internal.Dry); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
 | |
| 		log.Info(i18n.G("new release published: %s", url))
 | |
| 	} else {
 | |
| 		log.Info(i18n.G("no -p/--publish passed, not publishing"))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
 | |
| 	repo, err := git.PlainOpen(recipe.Dir)
 | |
| 	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 errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var lastGitTag tagcmp.Tag
 | |
| 	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()
 | |
| 	}
 | |
| 
 | |
| 	if lastGitTag.String() == tagString {
 | |
| 		return errors.New(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
 | |
| 	}
 | |
| 
 | |
| 	if !internal.NoInput {
 | |
| 		prompt := &survey.Confirm{
 | |
| 			Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
 | |
| 		}
 | |
| 
 | |
| 		var ok bool
 | |
| 		if err := survey.AskOne(prompt, &ok); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if !ok {
 | |
| 			return errors.New(i18n.G("exiting as requested"))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := addReleaseNotes(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to add release notes: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := commitRelease(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to commit changes: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := tagRelease(tagString, repo); err != nil {
 | |
| 		return errors.New(i18n.G("failed to tag release: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	if err := pushRelease(recipe, tagString); err != nil {
 | |
| 		return errors.New(i18n.G("failed to publish new release: %s", err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // cleanCommit soft removes the latest release commit. No change are lost the
 | |
| // the commit itself is removed. This is the equivalent of `git reset HEAD~1`.
 | |
| func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
 | |
| 	repo, err := git.PlainOpen(recipe.Dir)
 | |
| 	if err != nil {
 | |
| 		return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
 | |
| 	}
 | |
| 
 | |
| 	worktree, err := repo.Worktree()
 | |
| 	if err != nil {
 | |
| 		return errors.New(i18n.G("unable to open work tree in %s: %s", recipe.Dir, err))
 | |
| 	}
 | |
| 
 | |
| 	opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.MixedReset}
 | |
| 	if err := worktree.Reset(opts); err != nil {
 | |
| 		return errors.New(i18n.G("unable to soft reset %s: %s", recipe.Dir, err))
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("removed freshly created commit"))
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // cleanTag removes a freshly created tag
 | |
| func cleanTag(recipe recipe.Recipe, tag string) error {
 | |
| 	repo, err := git.PlainOpen(recipe.Dir)
 | |
| 	if err != nil {
 | |
| 		return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
 | |
| 	}
 | |
| 
 | |
| 	if err := repo.DeleteTag(tag); err != nil {
 | |
| 		if !strings.Contains(err.Error(), "not found") {
 | |
| 			return errors.New(i18n.G("unable to delete tag %s: %s", tag, err))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("removed freshly created tag %s", tag))
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
 | |
| 	initTag, err := recipe.GetVersionLabelLocal()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if initTag == "" {
 | |
| 		return "", errors.New(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
 | |
| 	}
 | |
| 
 | |
| 	log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
 | |
| 
 | |
| 	if prompt && !internal.NoInput {
 | |
| 		var response bool
 | |
| 		prompt := &survey.Confirm{Message: i18n.G("use %s as the new version?", initTag)}
 | |
| 		if err := survey.AskOne(prompt, &response); err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 
 | |
| 		if !response {
 | |
| 			return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return initTag, nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	publish bool
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	RecipeReleaseCommand.Flags().BoolVarP(
 | |
| 		&internal.Dry,
 | |
| 		i18n.G("dry-run"),
 | |
| 		i18n.G("r"),
 | |
| 		false,
 | |
| 		i18n.G("report changes that would be made"),
 | |
| 	)
 | |
| 
 | |
| 	RecipeReleaseCommand.Flags().BoolVarP(
 | |
| 		&internal.Major,
 | |
| 		i18n.G("major"),
 | |
| 		i18n.G("x"),
 | |
| 		false,
 | |
| 		i18n.G("increase the major part of the version"),
 | |
| 	)
 | |
| 
 | |
| 	RecipeReleaseCommand.Flags().BoolVarP(
 | |
| 		&internal.Minor,
 | |
| 		i18n.G("minor"),
 | |
| 		i18n.G("y"),
 | |
| 		false,
 | |
| 		i18n.G("increase the minor part of the version"),
 | |
| 	)
 | |
| 
 | |
| 	RecipeReleaseCommand.Flags().BoolVarP(
 | |
| 		&internal.Patch,
 | |
| 		i18n.G("patch"),
 | |
| 		i18n.G("z"),
 | |
| 		false,
 | |
| 		i18n.G("increase the patch part of the version"),
 | |
| 	)
 | |
| 
 | |
| 	RecipeReleaseCommand.Flags().BoolVarP(
 | |
| 		&publish,
 | |
| 		i18n.G("publish"),
 | |
| 		i18n.G("p"),
 | |
| 		false,
 | |
| 		i18n.G("publish changes to git.coopcloud.tech"),
 | |
| 	)
 | |
| }
 |