Skip to content

fix(ci): harden the release-cut path (bootstrap pin, dry-run sweep, dispatch target) - #479

Merged
joshua-temple merged 4 commits into
mainfrom
fix/release-bootstrap-pin-and-dryrun-sweeper
Jul 6, 2026
Merged

joshua-temple merged 4 commits into
mainfrom
fix/release-bootstrap-pin-and-dryrun-sweeper

Conversation

@joshua-temple

Copy link
Copy Markdown
Collaborator

Problem

Cutting a real release candidate today failed with invalid version format: v0.9.0-dryrun.30. Three related bugs surfaced while diagnosing it:

  1. orchestrate.yaml and promote.yaml pin the bootstrap cascade CLI (the binary those workflows use to run cascade's own orchestrate/promote steps) to v0.1.0, from before the release-tag filtering (fix(version): ignore non-version tags in latest-tag discovery #382) and prerelease-tolerant version parsing (fix(version): tolerate prerelease suffixes in next-env-version calc #420) that ship in v0.8.0. The stale binary could pick a stray prerelease tag as the latest release base and then reject its own version string.
  2. The nightly sweep-dryrun-tags job skipped entirely on any manual dry_run dispatch, to avoid racing the tag that same run just cut. In practice this switched pruning off for as long as a maintainer kept rehearsing back to back (four consecutive dry-run dispatches today never ran the sweep once), letting dry-run tags accumulate until the next scheduled or non-dry-run trigger.
  3. The Finalize job's fallback dispatch (gh workflow run ./.github/workflows/release.yaml ...) 404s: gh workflow run wants a bare workflow file name or numeric ID for a same-repo dispatch (every call site already passes --repo explicitly), not a ./-prefixed repo path. This fallback exists precisely because the tag-push trigger is unreliable, so it silently never worked when it mattered.

Fix

  • Bump cli_version/cli_version_sha in .github/manifest.yaml to v0.8.0 and regenerate orchestrate.yaml/promote.yaml.
  • Always run sweep-dryrun-tags, including on dry-run dispatches: retention only ever deletes tags older than the newest few, so the tag a concurrent dispatch is cutting (always the newest by run number) can never be pruned. Also reconcile any dry-run-shaped release left with no live tag (an out-of-band tag deletion previously orphaned it forever, invisible to a tag-only scan).
  • Add workflowDispatchTarget, a bare-file-name resolver dedicated to gh workflow run call sites, and route the three affected dispatches (candidate build, release-build trigger, publish callback) through it instead of normalizeWorkflowPath (which is correct only for uses: reusable-workflow references).

Verification

  • go build ./..., go test ./..., go test ./... -race, golangci-lint run ./...: all clean.
  • actionlint on all three touched workflow files: clean (pre-existing SC2129 style notices on lines this PR didn't touch, confirmed present on origin/main too).
  • e2e module: go build ./... and go vet ./... clean.
  • Regenerated orchestrate.yaml/promote.yaml reviewed hunk-by-hunk against each commit to confirm no unrelated drift.
  • Reproduced scenario 38's config standalone against the fixed generator to confirm its updated fixture string matches actual output.
  • Live GitHub state: the three currently-orphaned dry-run draft releases (left over from a prior manual tag cleanup) were deleted by hand as part of this investigation.

This hardens the release-cut path; it does not change any user-facing generator behavior beyond the gh workflow run target fix.

…elease

orchestrate.yaml and promote.yaml pinned the bootstrap cascade CLI (used to
run cascade orchestrate/promote steps against cascade's own release) to
v0.1.0, predating the release-tag filtering and prerelease-tolerant version
parsing that now ship in v0.8.0. The stale binary could pick a stray
prerelease tag as the latest release base and then reject its own version
string, breaking a real release cut. Bump cli_version and cli_version_sha in
.github/manifest.yaml to v0.8.0 and regenerate both workflows so they carry
the fix.

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

The sweep-dryrun-tags job skipped entirely on any manual dry_run dispatch, to
avoid racing the tag that same run had just cut. In practice this switched
pruning off for as long as a maintainer kept rehearsing back to back (four
consecutive dry-run dispatches today never ran the sweep once), letting
dry-run tags pile up until the next scheduled or non-dry-run trigger. The
retention logic only ever deletes tags older than the newest few, so the tag
a concurrent dispatch just cut (always the newest by run number) can never be
among them; the job now always runs.

Separately, a dry-run tag deleted out-of-band (its ref removed without also
deleting the release object) leaves an orphaned draft release with no tag to
find it by again, since discovery was tag-only. The job now also reconciles
any dry-run-shaped release with no live tag before applying retention.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
orchestrate's Finalize job dispatches the configured release workflow via
gh workflow run against normalizeWorkflowPath's output, which produces a
"./.github/workflows/x.yaml"-style path. That form is correct for a uses:
reusable-workflow reference but gh workflow run wants the bare file name (or
a numeric workflow ID) for a same-repo dispatch and 404s on the repo path
("workflow ... not found on the default branch"). Every call site already
passes --repo explicitly, so this fallback dispatch has silently always
failed; it only didn't matter because the tag-push trigger happened to fire
Release directly. That trigger is unreliable by design (a candidate tag can
point at a state commit whose message suppresses CI), so this dispatch is
the safety net for exactly the case where it is needed.

Add workflowDispatchTarget, a bare-file-name resolver dedicated to gh
workflow run call sites, and route the three affected dispatches (candidate
build, release-build trigger, publish callback) through it instead of
normalizeWorkflowPath.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
cli_version_sha was set to git rev-parse v0.8.0, which resolves to the
annotated tag object, not the commit it points at. cascade's own contract
(internal/config/types.go) documents cli_version_sha as the commit SHA a
setup-cli@<sha> ref expects; validate_v1.go only regex-checks 40 hex chars,
so the wrong object type passed validation silently. Use git rev-parse
v0.8.0^{commit} instead, matching the v0.1.0 pin's precedent, and regenerate
orchestrate.yaml and promote.yaml so every setup-cli@<sha> # v0.8.0 ref
carries the commit.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple
joshua-temple merged commit 72f24ed into main Jul 6, 2026
19 checks passed
@joshua-temple
joshua-temple deleted the fix/release-bootstrap-pin-and-dryrun-sweeper branch July 6, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant