Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion e2e/harness/hotfix_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ func (r *Runner) resolveCommit(ref string) string {
return ref
}

// resolveCommits resolves a comma-delimited commit-ref list, resolving each
// entry to its SHA via the execution context and rejoining with commas. It
// mirrors how the plan job forwards a multi-commit dispatch input to
// `cascade hotfix plan --commits`: each ref must be a real SHA the planner can
// rev-parse, so a scenario reference like "commit2,commit3" must resolve to
// "<sha2>,<sha3>" before reaching the workflow. A single ref with no comma
// resolves identically to resolveCommit, keeping single-commit callers stable.
func (r *Runner) resolveCommits(refList string) string {
parts := strings.Split(refList, ",")
resolved := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
resolved = append(resolved, r.resolveCommit(part))
}
return strings.Join(resolved, ",")
}

// shortSHA returns the first 8 characters of a commit SHA, mirroring the hotfix
// workflow's SHORT_SHA computation (internal/generate/hotfix.go).
func shortSHA(sha string) string {
Expand Down Expand Up @@ -65,7 +85,11 @@ func (r *Runner) executeHotfixPlan(ctx context.Context, step *HotfixPlanStep) er
return nil
}

sha := r.resolveCommit(step.CommitRef)
// CommitRef may be a comma-delimited list (the multi-commit chain dispatch
// input). Resolve each entry to a real SHA so the plan job's
// `cascade hotfix plan --commits` can rev-parse every commit; a single ref
// resolves identically to the single-commit path.
sha := r.resolveCommits(step.CommitRef)
dryRun := "false"
if step.DryRun {
dryRun = "true"
Expand Down
223 changes: 223 additions & 0 deletions e2e/scenarios/hotfix/hotfix-multi-env-clean.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
name: "Hotfix Multi-Env Clean Chain"
description: |
Verifies a clean multi-environment hotfix chain. The dispatch targets staging,
so the planner's bottom-up chain is [test, staging]: the fix must land on every
intermediate environment between the build target and the requested target, not
just the requested one.

Three trunk fixes (commit2, commit3, commit4) are cherry-picked onto both env/test
and env/staging. Each (commit, env) lands via a discrete plan-apply-merge-merged
cycle - the e2e backend is gitea, whose cherry-picks apply cleanly and whose
hotfix_apply step drives the real API while the workflow's apply job stays a
dry-run plan (the act image lacks the gh CLI). The final state shows both test and
staging carrying all three patches.

The generated apply job's env_sequence loop, per-env case lookup, and conflict
halt contract are asserted in the generator unit tests; this scenario covers the
observable outcome that the fix reaches every chain environment.

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() {}

- name: "Third trunk fix"
action: commit
commit:
message: "fix: third patch"
files:
src/fix3.go: |
package main
func fix3() {}

# Plan the chain once. Targeting staging exercises the bottom-up [test, staging]
# sequence the apply loop walks. dry_run keeps it to validation: the act image
# has no gh CLI, so the discrete hotfix_apply steps below drive the real
# cherry-picks via the gitea API.
- name: "Plan hotfix chain for staging"
action: hotfix_plan
hotfix_plan:
target_env: staging
commit_ref: commit2
dry_run: true

# env/test receives all three fixes, one discrete cycle each.
- name: "Apply fix1 onto env/test"
action: hotfix_apply
hotfix_apply:
target_env: test
commit_ref: commit2

- name: "Merge fix1 test PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix1 on test"
action: hotfix_merged
hotfix_merged:
target_env: test
expect:
state:
test:
ref: "env/test"

- name: "Apply fix2 onto env/test"
action: hotfix_apply
hotfix_apply:
target_env: test
commit_ref: commit3

- name: "Merge fix2 test PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix2 on test"
action: hotfix_merged
hotfix_merged:
target_env: test

- name: "Apply fix3 onto env/test"
action: hotfix_apply
hotfix_apply:
target_env: test
commit_ref: commit4

- name: "Merge fix3 test PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix3 on test; test carries all three patches"
action: hotfix_merged
hotfix_merged:
target_env: test
expect:
state:
test:
ref: "env/test"
patches: [commit2, commit3, commit4]

# env/staging receives the same three fixes - the chain target.
- name: "Apply fix1 onto env/staging"
action: hotfix_apply
hotfix_apply:
target_env: staging
commit_ref: commit2

- name: "Merge fix1 staging PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix1 on staging"
action: hotfix_merged
hotfix_merged:
target_env: staging
expect:
state:
staging:
ref: "env/staging"

- name: "Apply fix2 onto env/staging"
action: hotfix_apply
hotfix_apply:
target_env: staging
commit_ref: commit3

- name: "Merge fix2 staging PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix2 on staging"
action: hotfix_merged
hotfix_merged:
target_env: staging

- name: "Apply fix3 onto env/staging"
action: hotfix_apply
hotfix_apply:
target_env: staging
commit_ref: commit4

- name: "Merge fix3 staging PR"
action: merge_pr
merge_pr:
label: cascade-hotfix

- name: "Finalize fix3 on staging; staging carries all three patches"
action: hotfix_merged
hotfix_merged:
target_env: staging
expect:
state:
test:
ref: "env/test"
patches: [commit2, commit3, commit4]
staging:
ref: "env/staging"
patches: [commit2, commit3, commit4]
Loading
Loading