From 6482c11c85914faea51646eace3c754ed028c438 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Tue, 23 Jun 2026 00:42:47 -0400 Subject: [PATCH] fix: record every commit of a multi-commit hotfix set per environment Signed-off-by: Joshua Temple --- e2e/harness/hotfix_actions.go | 38 ++++--- .../hotfix/hotfix-multi-commit-clean.yaml | 102 ++++++++++++++++++ internal/generate/hotfix.go | 18 ++-- internal/generate/hotfix_test.go | 27 +++++ internal/hotfix/command.go | 9 +- internal/hotfix/finalize.go | 27 +++-- internal/hotfix/finalize_integration_test.go | 2 +- internal/hotfix/finalize_test.go | 72 ++++++++++--- 8 files changed, 247 insertions(+), 48 deletions(-) create mode 100644 e2e/scenarios/hotfix/hotfix-multi-commit-clean.yaml diff --git a/e2e/harness/hotfix_actions.go b/e2e/harness/hotfix_actions.go index 10570e0c..8c95ef1d 100644 --- a/e2e/harness/hotfix_actions.go +++ b/e2e/harness/hotfix_actions.go @@ -145,12 +145,19 @@ func (r *Runner) executeHotfixApply(ctx context.Context, step *HotfixApplyStep) return nil } - commit := r.resolveCommit(step.CommitRef) + // CommitRef may be a comma-delimited set so a single apply (and the single + // finalize that follows it) carries a multi-commit hotfix, mirroring the + // product workflow's per-env $COMMITS cherry-pick loop. resolveCommits maps each + // scenario ref to its SHA and rejoins with commas; a single ref resolves + // identically to resolveCommit, keeping single-commit scenarios stable. + commitList := r.resolveCommits(step.CommitRef) + commits := strings.Split(commitList, ",") + commit := commits[0] env := step.TargetEnv envBranch := "env/" + env short := shortSHA(commit) hotfixBranch := "hotfix/" + env + "/" + short - r.t.Logf(" HotfixApply: commit=%s env=%s branch=%s", truncateSHA(commit), env, hotfixBranch) + r.t.Logf(" HotfixApply: commits=%s env=%s branch=%s", commitList, env, hotfixBranch) // Ensure env/ exists, anchored at the env's recorded state SHA (or HEAD). branches, err := r.harness.gitea.ListBranches(ctx, r.harness.repo) @@ -205,16 +212,21 @@ func (r *Runner) executeHotfixApply(ctx context.Context, step *HotfixApplyStep) "git fetch origin '+refs/heads/*:refs/remotes/origin/*' --tags >/dev/null 2>&1", fmt.Sprintf("git branch -D %q >/dev/null 2>&1 || true", hotfixBranch), fmt.Sprintf("git switch -c %q %q", hotfixBranch, "origin/"+envBranch), - fmt.Sprintf("git cherry-pick -x %q", commit), - "CP_EXIT=$?", - "if [ \"$CP_EXIT\" -ne 0 ]; then", - " CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\\n' ' ')", - " echo \"CONFLICT_FILES=$CONFLICTS\"", - " git add -A", - fmt.Sprintf(" git -c core.editor=true cherry-pick --continue || git commit -m %q", "hotfix: cherry-pick "+short8+" with conflicts"), - "else", - " echo \"CONFLICT_FILES=\"", - "fi", + // Cherry-pick every commit in the set onto the hotfix branch, mirroring the + // product apply loop. On the first conflict, classify and force-commit the + // partial resolution, then stop; clean sets apply all commits. + "CONFLICT_FILES=", + fmt.Sprintf("for commit in %s; do", strings.Join(commits, " ")), + " git cherry-pick -x \"$commit\"", + " CP_EXIT=$?", + " if [ \"$CP_EXIT\" -ne 0 ]; then", + " CONFLICT_FILES=$(git diff --name-only --diff-filter=U | tr '\\n' ' ')", + " git add -A", + fmt.Sprintf(" git -c core.editor=true cherry-pick --continue || git commit -m %q", "hotfix: cherry-pick "+short8+" with conflicts"), + " break", + " fi", + "done", + "echo \"CONFLICT_FILES=$CONFLICT_FILES\"", // Force-push the uniquely-named, per-apply throwaway hotfix branch. The // branch name embeds the source short SHA, so a force-push only ever // overwrites this apply's own prior attempt (e.g. a retried sync), never a @@ -242,7 +254,7 @@ func (r *Runner) executeHotfixApply(ctx context.Context, step *HotfixApplyStep) // Build the PR body with the three product trailers; append the conflict file // list on the conflict path. - body := fmt.Sprintf("Cascade-Hotfix-Target: %s\nCascade-Hotfix-Source: %s\nCascade-Hotfix-Base: %s\n", env, commit, baseSHA) + body := fmt.Sprintf("Cascade-Hotfix-Target: %s\nCascade-Hotfix-Source: %s\nCascade-Hotfix-Base: %s\n", env, commitList, baseSHA) title := fmt.Sprintf("hotfix(%s): cherry-pick %s", env, short) label := "cascade-hotfix" if conflict { diff --git a/e2e/scenarios/hotfix/hotfix-multi-commit-clean.yaml b/e2e/scenarios/hotfix/hotfix-multi-commit-clean.yaml new file mode 100644 index 00000000..034cb125 --- /dev/null +++ b/e2e/scenarios/hotfix/hotfix-multi-commit-clean.yaml @@ -0,0 +1,102 @@ +name: "Hotfix Multi-Commit Single Cycle" +description: | + Verifies that a single hotfix apply-merge-finalize cycle carrying a SET of trunk + commits records every commit in state..patches, not just the first. + + This is the multi-commit-per-finalize case the multi-env-clean scenario does not + reach: that scenario applies one commit per cycle, so each finalize records a + single patch. Here one apply cherry-picks both commit2 and commit3 onto env/test + in a single PR; the apply stamps the full comma-joined set in the + Cascade-Hotfix-Source trailer, and the single finalize must append both to the + patch set. + +config: + trunk_branch: main + environments: [dev, test, staging, prod] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: + - name: deploy-dev + workflow: deploy.yaml + triggers: ["src/**"] + - name: deploy-test + workflow: deploy.yaml + triggers: ["src/**"] + - name: deploy-staging + workflow: deploy.yaml + triggers: ["src/**"] + - name: deploy-prod + workflow: deploy.yaml + triggers: ["src/**"] + +steps: + - name: "Initial commit" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + + - name: "Orchestrate trunk into dev" + action: orchestrate + + - name: "Promote to establish test/staging/prod baselines" + action: promote + promote: + mode: default + + - name: "Promote again to carry the baseline down the chain" + action: promote + promote: + mode: default + + - name: "Promote once more so every env shares the baseline" + action: promote + promote: + mode: default + + - name: "First trunk fix" + action: commit + commit: + message: "fix: first patch" + files: + src/fix1.go: | + package main + func fix1() {} + + - name: "Second trunk fix" + action: commit + commit: + message: "fix: second patch" + files: + src/fix2.go: | + package main + func fix2() {} + + # One apply cherry-picks both fixes onto env/test in a single PR, stamping the + # full comma-joined commit set in the Cascade-Hotfix-Source trailer. + - name: "Apply both fixes onto env/test in one cycle" + action: hotfix_apply + hotfix_apply: + target_env: test + commit_ref: commit2,commit3 + + - name: "Merge the multi-commit test PR" + action: merge_pr + merge_pr: + label: cascade-hotfix + + # A single finalize must record BOTH commits, not just commit2. + - name: "Finalize; test carries both patches from one cycle" + action: hotfix_merged + hotfix_merged: + target_env: test + expect: + state: + test: + ref: "env/test" + patches: [commit2, commit3] diff --git a/internal/generate/hotfix.go b/internal/generate/hotfix.go index dd2aa846..e49fefba 100644 --- a/internal/generate/hotfix.go +++ b/internal/generate/hotfix.go @@ -390,9 +390,11 @@ func (g *HotfixGenerator) writeApplyJob(sb *strings.Builder) { sb.WriteString(" git fetch origin \"+refs/heads/env/${env}:refs/remotes/origin/env/${env}\"\n") sb.WriteString(" fi\n") sb.WriteString(" git switch -c \"$BRANCH\" \"$BASE\"\n") - // The PR-body trailers carry the first applied commit and the base anchor so - // the post-merge context job can recover the fix and base SHAs. - sb.WriteString(" BODY=$(printf 'Cascade-Hotfix-Target: %s\\nCascade-Hotfix-Source: %s\\nCascade-Hotfix-Base: %s\\n' \"$env\" \"$FIRST_COMMIT\" \"$BASE\")\n") + // The PR-body trailers carry the full comma-joined set of applied trunk + // commits and the base anchor so the post-merge context job can recover every + // fix SHA (not just the first) and the base SHA. The Source trailer mirrors the + // per-env $COMMITS list the cherry-pick loop below applies. + sb.WriteString(" BODY=$(printf 'Cascade-Hotfix-Target: %s\\nCascade-Hotfix-Source: %s\\nCascade-Hotfix-Base: %s\\n' \"$env\" \"$COMMITS\" \"$BASE\")\n") sb.WriteString(" CLEAN=true\n") sb.WriteString(" CONFLICT_COMMIT=\"\"\n") sb.WriteString(" CONFLICTS=\"\"\n") @@ -518,10 +520,12 @@ func (g *HotfixGenerator) writeContextJob(sb *strings.Builder) { sb.WriteString(" PR_BODY: ${{ github.event.pull_request.body }}\n") sb.WriteString(" run: |\n") sb.WriteString(" TARGET_ENV=\"${BASE_REF#env/}\"\n") - // Recover the trunk fix commit and the trunk base anchor from the trailers - // the apply job stamped into the resolution PR body. grep tolerates absent - // trailers (the || true) so the step never hard-fails here; the finalize - // command enforces that the required SHAs are present. + // Recover the full comma-joined set of trunk fix commits and the trunk base + // anchor from the trailers the apply job stamped into the resolution PR body. + // The Source trailer carries every applied commit, so keeping the whole value + // (rather than the first field) threads the complete set to finalize. grep + // tolerates absent trailers (the || true) so the step never hard-fails here; + // the finalize command enforces that the required SHAs are present. sb.WriteString(" FIX_SHA=$(printf '%s\\n' \"$PR_BODY\" | grep -m1 '^Cascade-Hotfix-Source:' | sed 's/^Cascade-Hotfix-Source:[[:space:]]*//' || true)\n") sb.WriteString(" BASE_SHA=$(printf '%s\\n' \"$PR_BODY\" | grep -m1 '^Cascade-Hotfix-Base:' | sed 's/^Cascade-Hotfix-Base:[[:space:]]*//' || true)\n") // Resolve the auto-rollback target: the target env's state SHA as recorded in diff --git a/internal/generate/hotfix_test.go b/internal/generate/hotfix_test.go index 23a5182a..b285b9e3 100644 --- a/internal/generate/hotfix_test.go +++ b/internal/generate/hotfix_test.go @@ -223,6 +223,33 @@ func TestHotfixGenerator_ConflictPath(t *testing.T) { assert.Contains(t, content, "Cascade-Hotfix-Base:") } +// TestHotfixGenerator_SourceTrailerCarriesFullCommitSet guards the multi-commit +// patch-accumulation fix: the resolution PR's Cascade-Hotfix-Source trailer must +// carry the whole per-env $COMMITS set so the post-merge finalize records every +// cherry-picked commit, not just the first. Stamping only the first commit (the +// old $FIRST_COMMIT trailer) dropped the rest from state..patches. +func TestHotfixGenerator_SourceTrailerCarriesFullCommitSet(t *testing.T) { + gen := NewHotfixGenerator(threeEnvHotfixConfig(), "") + content, err := gen.Generate() + require.NoError(t, err) + + // The trailer interpolates the full comma-joined per-env commit set. + assert.Contains(t, content, `Cascade-Hotfix-Source: %s\n`, + "the Source trailer must be a printf field") + assert.Contains(t, content, `"$env" "$COMMITS" "$BASE"`, + "the Source trailer must carry the full per-env $COMMITS set, not just the first commit") + assert.NotContains(t, content, `"$env" "$FIRST_COMMIT" "$BASE"`, + "the Source trailer must not stamp only the first commit") + + // The context job recovers the whole Source value (it strips the prefix and + // keeps the comma-joined remainder) and the finalize job passes it straight + // to --fix-sha, which the command splits back into the full set. + assert.Contains(t, content, `sed 's/^Cascade-Hotfix-Source:[[:space:]]*//'`, + "the context job must keep the whole Source value, not a single field") + assert.Contains(t, content, `--fix-sha "$FIX_SHA"`, + "the finalize job must forward the recovered (possibly comma-joined) fix-sha set") +} + func TestHotfixGenerator_CleanPath(t *testing.T) { gen := NewHotfixGenerator(threeEnvHotfixConfig(), "") content, err := gen.Generate() diff --git a/internal/hotfix/command.go b/internal/hotfix/command.go index 3dd828b5..0b207856 100644 --- a/internal/hotfix/command.go +++ b/internal/hotfix/command.go @@ -90,7 +90,12 @@ records the merge SHA is a no-op.`, finalizer.SetBuildResult(name, result) } - return finalizer.Finalize(targetEnv, mergeSHA, fixSHA, baseSHA) + fixSHAs, err := parseCommitRefs(fixSHA) + if err != nil { + return fmt.Errorf("invalid --fix-sha %q: %w", fixSHA, err) + } + + return finalizer.Finalize(targetEnv, mergeSHA, fixSHAs, baseSHA) }, } @@ -98,7 +103,7 @@ records the merge SHA is a no-op.`, cmd.Flags().StringVar(&manifestKey, "key", config.DefaultManifestKey, "Top-level manifest key") cmd.Flags().StringVar(&targetEnv, "target-env", "", "Environment to finalize (required)") cmd.Flags().StringVar(&mergeSHA, "merge-sha", "", "Tip of env/ after the resolution PR merged (required)") - cmd.Flags().StringVar(&fixSHA, "fix-sha", "", "Trunk commit the hotfix carries (required)") + cmd.Flags().StringVar(&fixSHA, "fix-sha", "", "Trunk commit(s) the hotfix carries; comma-delimited for a multi-commit set (required)") cmd.Flags().StringVar(&baseSHA, "base-sha", "", "Trunk anchor the integration branch diverged from (required)") cmd.Flags().StringVar(&actor, "actor", "", "Actor recorded on the state (default: $GITHUB_ACTOR)") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Validate and compute without writing state, tags, or releases") diff --git a/internal/hotfix/finalize.go b/internal/hotfix/finalize.go index c43db504..8740e8c7 100644 --- a/internal/hotfix/finalize.go +++ b/internal/hotfix/finalize.go @@ -342,17 +342,23 @@ func (f *Finalizer) SetBuildResult(name, result string) { // Finalize writes the diverged state for a completed hotfix on env/. // // targetEnv is the environment being hotfixed; mergeSHA is the tip of -// env/ after the resolution PR merged; fixSHA is the trunk commit the -// hotfix carries; baseSHA is the trunk anchor the integration branch diverged -// from. It cross-checks the merge SHA against the env-branch tip, allocates the -// next free hotfix version, snapshots the prior state into the Previous ring, -// writes the divergence fields and substates, commits the manifest to trunk, and -// creates the hotfix tag and release object. +// env/ after the resolution PR merged; fixSHAs are the trunk commits +// the hotfix carries (every cherry-picked commit, in apply order); baseSHA is the +// trunk anchor the integration branch diverged from. It cross-checks the merge +// SHA against the env-branch tip, allocates the next free hotfix version, +// snapshots the prior state into the Previous ring, writes the divergence fields +// and substates, commits the manifest to trunk, and creates the hotfix tag and +// release object. Every commit in fixSHAs is appended to the env's recorded +// patch set so a multi-commit hotfix records all of its commits, not just the +// first. // // Finalize is idempotent on identical inputs: a rerun after the state already // records the merge SHA is a no-op that neither double-applies patches nor // re-snapshots Previous. -func (f *Finalizer) Finalize(targetEnv, mergeSHA, fixSHA, baseSHA string) error { +func (f *Finalizer) Finalize(targetEnv, mergeSHA string, fixSHAs []string, baseSHA string) error { + if len(fixSHAs) == 0 { + return fmt.Errorf("no fix commits supplied; finalize needs at least one trunk commit") + } cfg := f.cicd.Config if cfg == nil { return fmt.Errorf("manifest has no config block") @@ -437,7 +443,7 @@ func (f *Finalizer) Finalize(targetEnv, mergeSHA, fixSHA, baseSHA string) error if prior.BaseSHA == "" { prior.BaseSHA = baseSHA } - prior.Patches = append(prior.Patches, fixSHA) + prior.Patches = append(prior.Patches, fixSHAs...) prior.Ref = branch prior.SHA = mergeSHA prior.Version = hotfixVersion @@ -456,8 +462,9 @@ func (f *Finalizer) Finalize(targetEnv, mergeSHA, fixSHA, baseSHA string) error return fmt.Errorf("committing hotfix state: %w", err) } - // Create the hotfix tag and release object. - if err := f.createRelease(cfg, targetEnv, mergeSHA, hotfixVersion, fixSHA, baseVersion); err != nil { + // Create the hotfix tag and release object. The release body references the + // first carried commit as the representative fix SHA. + if err := f.createRelease(cfg, targetEnv, mergeSHA, hotfixVersion, fixSHAs[0], baseVersion); err != nil { return err } diff --git a/internal/hotfix/finalize_integration_test.go b/internal/hotfix/finalize_integration_test.go index 0ae70a6c..e0e39057 100644 --- a/internal/hotfix/finalize_integration_test.go +++ b/internal/hotfix/finalize_integration_test.go @@ -93,7 +93,7 @@ func TestFinalize_Integration_PlanThenMergeThenFinalize(t *testing.T) { f.SetDeployResult("api", "success") f.SetBuildResult("api", "success") - if err := f.Finalize("test", mergeSHA, fix, base); err != nil { + if err := f.Finalize("test", mergeSHA, []string{fix}, base); err != nil { t.Fatalf("finalize: %v", err) } diff --git a/internal/hotfix/finalize_test.go b/internal/hotfix/finalize_test.go index 334c2826..58da2c52 100644 --- a/internal/hotfix/finalize_test.go +++ b/internal/hotfix/finalize_test.go @@ -164,7 +164,7 @@ func TestFinalize_WritesDivergedState(t *testing.T) { ) f.SetDeployResult("api", "success") - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -239,7 +239,7 @@ func TestFinalize_PreviousRingSnapshot(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -291,7 +291,7 @@ func TestFinalize_PreviousRingBounded(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -308,6 +308,48 @@ func TestFinalize_PreviousRingBounded(t *testing.T) { } } +func TestFinalize_RecordsAllCommitsOfMultiCommitSet(t *testing.T) { + newScratchRepo(t) + base := commitFile(t, "a.txt", "one", "first") + fix1 := commitFile(t, "b.txt", "two", "first fix") + fix2 := commitFile(t, "d.txt", "four", "second fix") + + // env/test exists at base; the resolution merged a cherry-pick of both fixes, + // so its tip is the merge SHA the workflow finalizes against. + runGit(t, "branch", "env/test", base) + runGit(t, "checkout", "env/test") + commitFile(t, "c.txt", "fixed1", "cp first") + merge := commitFile(t, "e.txt", "fixed2", "cp second") + runGit(t, "checkout", "main") + + manifest := writeFinalizeManifest(t, []string{"dev", "test", "prod"}, map[string]envFixture{ + "dev": {sha: fix2, version: "v1.4.0-rc.2"}, + "test": {sha: base, version: "v1.4.0-rc.2"}, + "prod": {sha: base, version: "v1.4.0-rc.2"}, + }) + + f := newFinalizer(t, manifest, + WithReleaseManager(&stubReleaseManager{}), + WithTagLister(stubTagLister{}), + WithStatePusher(&recordingPusher{}), + ) + + // A two-commit hotfix set must record BOTH original trunk SHAs, not just the + // first. Finalizing an env with empty patches using both commits yields two + // patch entries. + if err := f.Finalize("test", merge, []string{fix1, fix2}, base); err != nil { + t.Fatalf("Finalize: %v", err) + } + + st := loadState(t, manifest, "test") + if len(st.Patches) != 2 { + t.Fatalf("state.patches = %v, want both commits recorded (len 2)", st.Patches) + } + if st.Patches[0] != fix1 || st.Patches[1] != fix2 { + t.Errorf("state.patches = %v, want [%s %s]", st.Patches, fix1, fix2) + } +} + func TestFinalize_StacksSecondHotfix(t *testing.T) { newScratchRepo(t) base := commitFile(t, "a.txt", "one", "first") @@ -341,7 +383,7 @@ func TestFinalize_StacksSecondHotfix(t *testing.T) { WithTagLister(stubTagLister{tags: []string{"v1.4.0-rc.2.hotfix.1"}}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("test", tip, fix2, base); err != nil { + if err := f.Finalize("test", tip, []string{fix2}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -379,7 +421,7 @@ func TestFinalize_MergeSHATipMismatch_Fails(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(&recordingPusher{}), ) - err := f.Finalize("test", other, fix, base) + err := f.Finalize("test", other, []string{fix}, base) if err == nil { t.Fatal("expected mismatch error when merge SHA is not env/test tip") } @@ -412,7 +454,7 @@ func TestFinalize_Idempotent_Rerun(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(pusher), ) - if err := f1.Finalize("test", merge, fix, base); err != nil { + if err := f1.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("first Finalize: %v", err) } @@ -427,7 +469,7 @@ func TestFinalize_Idempotent_Rerun(t *testing.T) { WithTagLister(stubTagLister{tags: []string{"v1.4.0-rc.2.hotfix.1"}}), WithStatePusher(pusher), ) - if err := f2.Finalize("test", merge, fix, base); err != nil { + if err := f2.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("second Finalize (idempotent): %v", err) } @@ -482,7 +524,7 @@ func TestFinalize_StateWriteTargetsTrunkBranch(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(pusher), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -529,7 +571,7 @@ func TestFinalize_ReadsPriorStateFromTrunk(t *testing.T) { WithTrunkStateReader(reader), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize should succeed reading prior state from trunk: %v", err) } @@ -576,7 +618,7 @@ func TestFinalize_TrunkIsSourceOfPriorState(t *testing.T) { WithStatePusher(&recordingPusher{}), WithTrunkStateReader(&stubTrunkReader{bytes: trunkBytes}), ) - if err := f.Finalize("test", merge, fix, shaB); err != nil { + if err := f.Finalize("test", merge, []string{fix}, shaB); err != nil { t.Fatalf("Finalize: %v", err) } @@ -633,7 +675,7 @@ func TestFinalize_DoesNotClobberOtherEnvsTrunkState(t *testing.T) { WithStatePusher(&recordingPusher{}), WithTrunkStateReader(&stubTrunkReader{bytes: trunkBytes}), ) - if err := f.Finalize("staging", merge, fix, base); err != nil { + if err := f.Finalize("staging", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -701,7 +743,7 @@ func TestFinalize_TrunkStateAbsent_Errors(t *testing.T) { WithStatePusher(&recordingPusher{}), WithTrunkStateReader(&stubTrunkReader{bytes: trunkBytes}), ) - err := f.Finalize("test", merge, fix, base) + err := f.Finalize("test", merge, []string{fix}, base) if err == nil { t.Fatal("expected missing-state error when trunk has no recorded SHA for the target env") } @@ -762,7 +804,7 @@ func TestFinalize_VersionAllocation_SkipsExistingTags(t *testing.T) { WithTagLister(stubTagLister{tags: []string{"v1.4.0-rc.2.hotfix.1", "v1.4.0-rc.2.hotfix.2"}}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -799,7 +841,7 @@ func TestFinalize_PublishedBase_PatchBump_NoCollision(t *testing.T) { WithTagLister(stubTagLister{tags: []string{"v1.3.0", "v1.3.1"}}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("test", merge, fix, base); err != nil { + if err := f.Finalize("test", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) } @@ -836,7 +878,7 @@ func TestFinalize_PrereleaseEnv_ReplacesPrerelease(t *testing.T) { WithTagLister(stubTagLister{}), WithStatePusher(&recordingPusher{}), ) - if err := f.Finalize("uat", merge, fix, base); err != nil { + if err := f.Finalize("uat", merge, []string{fix}, base); err != nil { t.Fatalf("Finalize: %v", err) }