From 039a29f27cb8d1f4e5015e3ef996723535976930 Mon Sep 17 00:00:00 2001 From: iexos Date: Wed, 25 Feb 2026 16:54:00 +0100 Subject: [PATCH] feat: recipe release reverts completely on failure --- cli/recipe/release.go | 82 ++++++++++----------------- tests/integration/recipe_release.bats | 23 ++++++++ 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/cli/recipe/release.go b/cli/recipe/release.go index d96178b1..250f2b0b 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -97,6 +97,16 @@ your private key and enter your passphrase beforehand. log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name)) } + repo, err := git.PlainOpen(recipe.Dir) + if err != nil { + log.Fatal(err) + } + + preCommitHead, err := repo.Head() + if err != nil { + log.Fatal(err) + } + isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) @@ -201,11 +211,6 @@ likely to change. } if tagString == "" { - repo, err := git.PlainOpen(recipe.Dir) - if err != nil { - log.Fatal(err) - } - var lastGitTag tagcmp.Tag iter, err := repo.Tags() if err != nil { @@ -282,16 +287,6 @@ likely to change. log.Fatal(i18n.G("invalid version %s specified", tagString)) } - mainService := "app" - label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", tagString) - if !internal.Dry { - if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { - log.Fatal(err) - } - } else { - log.Info(i18n.G("dry run: not syncing label %s for recipe %s", tagString, recipe.Name)) - } - for _, tag := range tags { previousTagLeftHand := strings.Split(tag, "+")[0] newTagStringLeftHand := strings.Split(tagString, "+")[0] @@ -300,24 +295,15 @@ likely to change. } } - repo, err := git.PlainOpen(recipe.Dir) - if err != nil { - log.Fatal(err) - } - - preCommitHead, err := repo.Head() - if err != nil { - log.Fatal(err) - } - if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if cleanErr := cleanTag(recipe, tagString); cleanErr != nil { - log.Fatal(cleanErr) + log.Fatal(i18n.G("unable to clean up tag after failed release attempt: %s", cleanErr)) } - if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil { - log.Fatal(cleanErr) + if resetErr := resetCommit(recipe, preCommitHead); resetErr != nil { + log.Fatal(i18n.G("unable to reset commit after failed release attempt: %s", resetErr)) } - log.Fatal(err) + log.Error(err) + log.Fatal(i18n.G("release failed. any changes made have been reverted")) } }, } @@ -375,23 +361,14 @@ func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion str 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) + mainService := "app" + label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", tagString) + if !internal.Dry { + if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { + log.Fatal(err) + } + } else { + log.Info(i18n.G("dry run: not syncing label %s for recipe %s", tagString, recipe.Name)) } if err := addReleaseNotes(recipe, tagString); err != nil { @@ -587,9 +564,10 @@ func pushRelease(recipe recipePkg.Recipe, tagString string) 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 recipePkg.Recipe, head *plumbing.Reference) error { +// resetCommit hard resets to the state before release was started. +// This will only remove changes made by the release process due to requiring +// a clean working directory. +func resetCommit(recipe recipePkg.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)) @@ -600,12 +578,12 @@ func cleanCommit(recipe recipePkg.Recipe, head *plumbing.Reference) error { 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} + opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.HardReset} if err := worktree.Reset(opts); err != nil { - return errors.New(i18n.G("unable to soft reset %s: %s", recipe.Dir, err)) + return errors.New(i18n.G("unable to hard reset %s: %s", recipe.Dir, err)) } - log.Debug(i18n.G("removed freshly created commit")) + log.Debug(i18n.G("reset commit to pre-release state")) return nil } diff --git a/tests/integration/recipe_release.bats b/tests/integration/recipe_release.bats index e62dbbd6..e7e365df 100644 --- a/tests/integration/recipe_release.bats +++ b/tests/integration/recipe_release.bats @@ -217,3 +217,26 @@ teardown() { assert_failure assert_output --partial "automagic insertion not supported yet" } + +@test "push during release fails" { + run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit + assert_success + + run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show + assert_success + assert_output --partial 'image: nginx:1.21.6' + + wantHash="$(_get_current_hash)" + + run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote set-url origin-ssh "$ABRA_DIR/does/not/exist" + assert_success + + run $ABRA recipe release "$TEST_RECIPE" --no-input --patch + assert_failure + assert_output --partial 'failed to publish new release:' + assert_output --partial 'any changes made have been reverted' + + assert_equal "$wantHash" "$(_get_current_hash)" + + assert_equal "$(_git_status)" "" +} -- 2.49.0