Skip to content

fix(generate): gate finalize's release/state-write/candidate-dispatch on dry_run - #665

Merged
joshua-temple merged 4 commits into
mainfrom
fix/orchestrate-finalize-dry-run
Jul 24, 2026
Merged

fix(generate): gate finalize's release/state-write/candidate-dispatch on dry_run#665
joshua-temple merged 4 commits into
mainfrom
fix/orchestrate-finalize-dry-run

Conversation

@joshua-temple

@joshua-temple joshua-temple commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

TLDR

  • A dry-run orchestrate dispatch (dry_run: true) currently creates a real tag and release and commits real state to trunk; the finalize job's mutations were never gated on dry_run.
  • Manage Release now forwards --dry-run to cascade manage-release, whose already-tested printDryRunPlan gate (internal/release/command.go) previews the plan instead of mutating; the composite manage-release action gains a dry_run input to carry it.
  • Update Manifest previews the state edit and exits before its commit/push loop; Dispatch Release Candidate Build's if: now also requires dry_run != 'true'.
  • All three use the null-safe github.event.inputs.dry_run accessor so they behave on orchestrate's non-dispatch triggers (push/schedule/workflow_run), matching the existing writeNativeDeploymentSteps guard and callback dry_run forwarding.

Why / value

  • Closes a real safety hole: the whole point of a dry_run rehearsal is to mutate nothing, yet finalize cut real tags/releases and wrote real state, so operators could not safely rehearse an orchestrate run.
  • The CLI-level --dry-run gate already existed and was tested (internal/release/command.go:105); it simply was never wired up from the generated workflow side. This connects the two, matching the project convention that --dry-run is gated twice (at the command and at the mutation boundary).
  • The hand-rolled state write has no CLI to delegate to, so it is gated at the step (a plain DRY_RUN shell guard, mirroring the native-deployment step's existing style).

Evidence

Acceptance criterion Test that proves it (red before the fix)
Manage Release forwards dry_run; the action threads --dry-run to the CLI TestManageReleaseAction_ThreadsDryRunFlag, TestWriteReleaseStep_ForwardsDryRunDispatchInput; runtime dry-run itself is covered by the existing internal/release printDryRunPlan tests
Update Manifest previews and exits before committing state TestWriteManifestUpdateStep_GatesMutationOnDryRun (asserts the gate precedes the commit/push loop)
Dispatch Release Candidate Build is suppressed on dry-run TestWriteCandidateDispatchStep_GatedOnDryRun
No regression to normal (non-dry-run) output TestByteIdenticalBaseline_SingleComponent (goldens regenerated: additive dry_run only), full go test ./..., go vet, golangci-lint, actionlint on regenerated golden all clean; own-repo workflows regenerated so the drift check passes

Each generation-correctness test turns red when its guard is reverted, so a reviewer can falsify it by reverting the behavior alone.

Note on runtime e2e: an earlier revision added a dry-run orchestrate e2e scenario, but two CI runs showed act does not surface github.event.inputs.dry_run to the orchestrate finalize job (the rehearsal still cut the tag and advanced state), the same limitation that keeps the sibling dry-run-input-expression.yaml generation-only. It was removed rather than shipped green-for-the-wrong-reason; the runtime behavior is exercised on real GitHub, where github.event.inputs is populated.

… on dry_run

A dry-run `orchestrate` dispatch (`dry_run: true`) is meant to be a rehearsal
that mutates nothing, but the finalize job's real mutations were never gated:

- The `Manage Release` step invoked `cascade manage-release` with no
  `--dry-run` flag, so it cut a real tag and created a real release even though
  the CLI's own tested `printDryRunPlan` gate (internal/release/command.go) was
  built to prevent exactly that. The step now forwards the dispatch input as
  `--dry-run`; the composite `manage-release` action gains a `dry_run` input to
  carry it.
- The `Update Manifest` step committed real state to trunk. It now previews the
  yq edit and exits before its commit/push loop when `DRY_RUN` is set.
- The `Dispatch Release Candidate Build` step fired a real external release
  workflow run. Its `if:` now also requires `dry_run != 'true'`, mirroring the
  native-deployment step's existing guard.

All three use the null-safe `github.event.inputs.dry_run` accessor so they
behave correctly on orchestrate's non-dispatch triggers (push/schedule/
workflow_run), where the inputs context is null.

Proven by generation-correctness tests in
internal/generate/dry_run_finalize_test.go (red before the fix) and a runtime
e2e scenario orchestrate/dry-run-finalize-no-mutations.yaml that dispatches a
dry-run orchestrate and asserts no tag was cut and state.dev stayed unwritten.
Action-mode (non-dry-run) output is unchanged apart from the additive input.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple joshua-temple self-assigned this Jul 24, 2026
cascade generates its own workflows (own-repo mode), so the generator change in
the previous commit leaves the committed .github/workflows/orchestrate.yaml and
.github/actions/manage-release/action.yaml stale, which the drift check catches.
Regenerated with `cascade generate-workflow --own-repo --config
.github/manifest.yaml --force`; `cascade verify --own-repo` reports no drift.
Additive dry_run wiring only; no other change.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
…ors the input

The runtime scenario ran a push-mode orchestrate under workflow_dispatch, but
act's --detect-event reruns a workflow that still declares a push: trigger as a
push, leaving github.event.inputs empty so the dry_run gate never fired and the
rehearsal wrote real state. Switch the manifest to release_trigger: dispatch,
which drops the push: trigger, so act runs the workflow_dispatch path and seeds
github.event.inputs.dry_run. This mirrors the proven state-advancing dispatch
path in 40-release-trigger-dispatch-only.yaml; here the run is a dry-run that
must advance nothing.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
Two CI runs confirmed act does not surface github.event.inputs.dry_run to the
orchestrate finalize job (the dispatch input the guards read), even under a
dispatch-only manifest: the rehearsal still cut the candidate tag and advanced
state. This is the same act limitation that already keeps the sibling
orchestrate dry-run coverage (dry-run-input-expression.yaml) generation-only.

Rather than ship a runtime scenario that cannot exercise the behavior, remove
it and the unused OrchestrateStep.dry_run harness knob. The three guards stay
proven by the red-able generation-correctness tests in
internal/generate/dry_run_finalize_test.go (each turns red when its guard is
reverted), and the Manage Release path is additionally backed by the CLI-level
dry-run test in internal/release. orchestrate coverage in the registry is
already satisfied by 02/03; the runtime dry-run behavior is verified on real
GitHub, where github.event.inputs is populated.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple
joshua-temple merged commit 3742e04 into main Jul 24, 2026
21 checks passed
@joshua-temple
joshua-temple deleted the fix/orchestrate-finalize-dry-run branch July 24, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant