From 7d4dc66a5cbd0740c1ffd665a700394ad6bce92e Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Wed, 10 Jun 2026 08:41:53 -0400 Subject: [PATCH] fix: correct single-env release promotion and stale multistep e2e fixtures The e2e TestMultiStepScenarios suite had four failing subtests rooted in two distinct causes. Single Environment Happy Path failed because the promote step always dispatched the multi-env promote input (mode), but a single-environment repo generates a Release workflow whose dispatch input is release_action. The unrecognized mode was ignored, the workflow fell back to its create-draft default, and the run reported success without ever publishing the final release, so prod was never reconciled and the RC tags were never cleaned up. The runner now dispatches release_action: release for single-env repos. That workflow records its published pointer under ci.latest_release rather than a state[release] env, so the state sync now surfaces latest_release under the synthetic release key and the scenario asserts the single-env model (latest_release populated, trunk-tracking env retained) instead of the multi-env wipe semantics. Dispatch Inputs failed because the harness Config struct had no dispatch_inputs field, so the block was dropped when the scenario config was re-marshaled into the manifest and the generator never saw it. Added a passthrough field so operator-defined inputs reach the generator. Inline Run Callback and Inline Job Attributes asserted uses: build.yaml, but the harness localizes a bare reusable-workflow reference to the GHA-valid uses: ./build.yaml form before the assertion reads the file. Restored the ./-prefixed assertions to match the localized, valid output. Signed-off-by: Joshua Temple --- e2e/harness/runner.go | 71 ++++++++++++++++----- e2e/harness/scenario.go | 6 ++ e2e/scenarios/09-inline-run-callback.yaml | 2 +- e2e/scenarios/09-single-env-repo.yaml | 7 +- e2e/scenarios/10-inline-job-attributes.yaml | 2 +- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/e2e/harness/runner.go b/e2e/harness/runner.go index 7a379b68..8b46455f 100644 --- a/e2e/harness/runner.go +++ b/e2e/harness/runner.go @@ -376,24 +376,43 @@ func (r *Runner) executePromote(ctx context.Context, promote *PromoteStep, confi // pair into the "-to-" form. Source defaults to the first // env (typically dev) since the workflow generator only emits dev-rooted // cascade options. - mode := promote.Mode - if mode == "cascade" { - source := "dev" - if len(config.Environments) > 0 { - source = config.Environments[0] + var inputs map[string]string + if len(config.Environments) == 1 { + // Single-environment repos generate a Release workflow (see + // command.go IsSingleEnvironment → NewReleaseGenerator) written to + // promote.yaml. That workflow's dispatch input is release_action + // (create-draft|prerelease|release), not the multi-env promote's + // mode. A "promote to release" step on a single-env repo means + // publish the final release, so dispatch release_action: release. + // Sending mode here was silently ignored, the workflow fell back to + // its create-draft default, and the run "succeeded" without ever + // publishing v0.1.0, wiping prod state, or cleaning up the RC tags. + inputs = map[string]string{ + "release_action": "release", + } + if promote.AllowBreaking { + inputs["allow_breaking_changes"] = "true" + } + } else { + mode := promote.Mode + if mode == "cascade" { + source := "dev" + if len(config.Environments) > 0 { + source = config.Environments[0] + } + mode = fmt.Sprintf("%s-to-%s", source, promote.Target) + } + inputs = map[string]string{ + "mode": mode, + } + if promote.AllowBreaking { + // Workflow input is named allow_breaking_changes (see internal/generate + // promote.go); the workflow forwards it to the CLI's --allow-breaking + // flag. The harness's previous "allow_breaking" key was silently + // ignored, leaving the breaking-change gate active even when the + // scenario asked for it to be bypassed. + inputs["allow_breaking_changes"] = "true" } - mode = fmt.Sprintf("%s-to-%s", source, promote.Target) - } - inputs := map[string]string{ - "mode": mode, - } - if promote.AllowBreaking { - // Workflow input is named allow_breaking_changes (see internal/generate - // promote.go); the workflow forwards it to the CLI's --allow-breaking - // flag. The harness's previous "allow_breaking" key was silently - // ignored, leaving the breaking-change gate active even when the - // scenario asked for it to be bypassed. - inputs["allow_breaking_changes"] = "true" } // Determine the branch ref @@ -521,6 +540,14 @@ func (r *Runner) syncStateFromGitea(ctx context.Context, config Config) error { SHA string `yaml:"sha"` } `yaml:"deploys"` } `yaml:"state"` + // LatestRelease is the single-environment Release workflow's published + // pointer (ci.latest_release). Single-env repos publish via the Release + // workflow's finalize step, which writes latest_release.{version,sha} + // rather than a state[release] env (see internal/generate/release.go). + LatestRelease struct { + SHA string `yaml:"sha"` + Version string `yaml:"version"` + } `yaml:"latest_release"` } if err := yaml.Unmarshal([]byte(manifestContent), &manifest); err != nil { return fmt.Errorf("failed to parse manifest: %w", err) @@ -550,6 +577,16 @@ func (r *Runner) syncStateFromGitea(ctx context.Context, config Config) error { } } + // Surface the single-env Release workflow's latest_release pointer under the + // synthetic "release" state key so scenarios can assert it the same way they + // assert any other environment's state. Multi-env repos leave latest_release + // empty (they wipe and repopulate state[release] directly), so this only + // records when the Release workflow has actually published. + if lr := ciData.LatestRelease; lr.Version != "" || lr.SHA != "" { + r.ctx.RecordState("release", lr.SHA, lr.Version) + r.t.Logf(" Synced state[release] (from latest_release) = %s @ %s", truncateSHA(lr.SHA), lr.Version) + } + return nil } diff --git a/e2e/harness/scenario.go b/e2e/harness/scenario.go index 0c4d15ec..f4d64543 100644 --- a/e2e/harness/scenario.go +++ b/e2e/harness/scenario.go @@ -33,6 +33,12 @@ type Config struct { Builds []BuildConfig `yaml:"builds"` Deploys []DeployConfig `yaml:"deploys"` Publish *PublishConfig `yaml:"publish,omitempty"` + // DispatchInputs carries operator-facing workflow_dispatch inputs through to + // the generated manifest untouched. A generic map (rather than a typed + // struct) is used so the harness stays decoupled from the generator's + // DispatchInput shape while preserving every key (type, options, default, + // description, required) across the marshal round-trip. + DispatchInputs map[string]map[string]any `yaml:"dispatch_inputs,omitempty"` } // PublishConfig defines a publish callback invoked after a release is published diff --git a/e2e/scenarios/09-inline-run-callback.yaml b/e2e/scenarios/09-inline-run-callback.yaml index 8d43f867..6672a121 100644 --- a/e2e/scenarios/09-inline-run-callback.yaml +++ b/e2e/scenarios/09-inline-run-callback.yaml @@ -36,7 +36,7 @@ steps: - path: ".github/workflows/orchestrate.yaml" contains: - "build-app:" - - "uses: build.yaml" + - "uses: ./build.yaml" - "build-smoke:" - "shell: bash" - 'echo "smoke check"' diff --git a/e2e/scenarios/09-single-env-repo.yaml b/e2e/scenarios/09-single-env-repo.yaml index 9f296c76..4e455f1c 100644 --- a/e2e/scenarios/09-single-env-repo.yaml +++ b/e2e/scenarios/09-single-env-repo.yaml @@ -96,11 +96,16 @@ steps: mode: default expect: state: + # Single-env repos publish via the Release workflow, which writes the + # ci.latest_release pointer (surfaced here as state[release]) rather than + # a state[release] env. It does not wipe the trunk-tracking env, so prod + # legitimately still holds the latest RC it was orchestrated to. release: sha: commit2 version: "v0.1.0" prod: - wiped: true + sha: commit2 + version: "v0.1.0-rc.1" releases: - tag: "v0.1.0" prerelease: false diff --git a/e2e/scenarios/10-inline-job-attributes.yaml b/e2e/scenarios/10-inline-job-attributes.yaml index e097ca07..0651c089 100644 --- a/e2e/scenarios/10-inline-job-attributes.yaml +++ b/e2e/scenarios/10-inline-job-attributes.yaml @@ -53,4 +53,4 @@ steps: - "group: sign-${{ github.ref }}" - "cancel-in-progress: true" - "build-app:" - - "uses: build.yaml" + - "uses: ./build.yaml"