Skip to content

Make any release failure turn the run red - #236

Closed
MelbourneDeveloper wants to merge 1 commit into
mainfrom
release-gates-go-red
Closed

MelbourneDeveloper wants to merge 1 commit into
mainfrom
release-gates-go-red

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TL;DR

The release pipeline had five ways to publish nothing and still report success. v0.17.0 went out through all of them intact. This makes every one of them a red run, and adds a backstop for the failure mode none of them covered: a job that never runs at all.

Details

A release is either published or it is not. These each downgraded "did not publish" to something quieter:

Where What it swallowed
build / vsix matrices continue-on-error on win32 — a Windows binary that never built was a warning inside a green run
Package step cp compiler/lib/lib*.a ... || true — a build producing no runtime archives would package, publish and reach Homebrew as a compiler that can link nothing
preflight a missing publish token skipped its channel with a warning, so a release reaching neither Homebrew, Scoop nor Open VSX passed while brew install served the previous version
brew / scoop git commit ... || exit 0 swallowed every commit failure along with the nothing-to-commit case, and skipped the push with it
deploy-webcompiler non-fatal by design — which is how the live playground served a stale build for weeks behind green releases

All five now fail the run. Nothing cancels: fail-fast: false stays on both matrices, so a broken leg still lets its siblings finish and one run shows every platform's result. It just ends red.

The backstop. Removing the swallows does not address the failure mode that survives: GitHub scores a skipped job as green, so a wrong if:, an unset scope output or a cancelled dependency publishes nothing and still succeeds. The new release-complete job runs if: always() after every other job and fails unless each channel this tag requires actually succeeded. It is driven by the same four scope outputs the jobs' own if: conditions use, so "ran" and "should have run" cannot drift apart.

Two same-class defects outside the release path are fixed too: the || true hiding a failed cargo install cargo-llvm-cov behind a confusing error several steps later, and the one on the Android sdkmanager lookup, now a directory test with a real diagnostic.

docs/RELEASING.md gains a section stating the policy, and its secrets table is corrected — it named VSCE_PAT, TAP_TOKEN and SCOOP_BUCKET_TOKEN, none of which exist. The real credentials are BREW_SCOOP_PAT, OPEN_VSX_PAT, AZURE_CLIENT_ID/AZURE_TENANT_ID and FLY_API_TOKEN. It also still advertised a darwin-x64 build leg that was dropped, and a SKIP_VSCE_PUBLISH variable nothing reads.

How Do The Automated Tests Prove It Works?

The backstop's logic is executed, not eyeballed. scripts/test-release-gate.py extracts the release-complete shell body out of release.yml — the real one, so there is no second copy to drift — and runs it under bash against 13 fabricated sets of job outcomes:

  • a full release with everything published → passes
  • Homebrew silently skipped, a build leg failed, a job cancelled, the web compiler deploy failed → each fails
  • a website-only tag whose site deployed → passes; the same tag with the site silently skipped → fails
  • a vsix-only tag whose extension published → passes; the same with the Marketplace publish skipped → fails
  • a prerelease correctly leaving the live site alone → passes; the same prerelease with Open VSX skipped → fails
  • a job with no if: at all that did not run → fails

Six mutations of the gate were each confirmed to turn the suite red: dropping the unconditional jobs from required, no longer distinguishing skipped from success, no longer counting cancelled as broken, ignoring the website output, always exiting 0, and renaming the step out from under the test. The cancelled mutation initially slipped through, which exposed a real gap — a cancelled job the tag does not require is caught only by the failure sweep, and no case covered that. Case 7 now pins it, and the mutation is caught.

The swallows cannot come back. scripts/verify-release-gates.mjs scans every workflow and fails on continue-on-error, || true or || exit 0 in a run step, on any release job missing from release-complete's needs:, on a renamed or deleted backstop, on a backstop without if: always(), and on a reviewed tolerance that no longer matches anything. Eight regressions were each confirmed caught — including adding a plausible new publish-snap job and forgetting to wire it in — with the workflow tree checksummed back to identical after every probe. The single surviving || true, the first-release tag lookup in release-change-detection.yml, is in the script's reviewed list with the reason it is safe; deleting the tolerance without deleting the entry fails the build.

Both run in make lint and as a step in the already-required Build, Format & Analyse job, so the pinned context list in verify-branch-protection.mjs and the ruleset stay in agreement — no new required check, no ruleset edit.

actionlint is clean across all six workflow files.

🤖 Generated with Claude Code

The release pipeline had five ways to publish nothing and still report
success, and v0.17.0 went out through all of them intact.

- The win32 build and VSIX legs were `continue-on-error`, so a Windows
  binary that never built showed as a warning inside a green run.
- The runtime archives were staged with `cp ... || true`, so a build
  producing none would package, publish and reach Homebrew as a compiler
  that can link nothing.
- The publish tokens were probed, and a missing one skipped its channel
  with a warning: a release that reached neither Homebrew, Scoop nor Open
  VSX passed while `brew install` kept serving the previous version.
- `git commit ... || exit 0` in the tap jobs swallowed every commit
  failure along with the nothing-to-commit case, and skipped the push.
- The web compiler deploy was non-fatal by design, which is how the live
  playground served a stale build for weeks behind green releases.

All five are now failures. `fail-fast: false` stays on both matrices, so
a broken leg still lets its siblings finish and one run shows every
platform's result — it just ends red rather than cancelling anything.

That leaves the failure mode none of it addresses: GitHub scores a
*skipped* job as green, so a wrong `if:` or an unset scope output
publishes nothing and still succeeds. The new `release-complete` job
runs `if: always()` after every other job and fails unless each channel
this tag requires actually succeeded, driven by the same four `scope`
outputs the jobs' own conditions use so the two cannot drift apart.

Also removes the `|| true` hiding a failed `cargo install cargo-llvm-cov`
behind a confusing error several steps later, and the one on the Android
`sdkmanager` lookup, which is now a directory test with a real
diagnostic.

How the tests prove it works:

`scripts/test-release-gate.py` extracts the backstop's shell out of
release.yml — the real one, not a copy — and runs it against 13
fabricated outcomes: Homebrew silently skipped, a build leg failed, a
job cancelled, a website-only tag whose site never deployed, a
vsix-only tag whose Marketplace publish skipped, and a prerelease
correctly leaving the live site alone. Six mutations of the gate were
each confirmed to turn it red, including one that first slipped through
and exposed a missing case: a cancelled job the tag does not require is
caught only by the failure sweep, so that case now pins the sweep.

`scripts/verify-release-gates.mjs` fails the PR that reintroduces any of
this: `continue-on-error`, `|| true` or `|| exit 0` in a run step, a
release job absent from `release-complete`'s needs, a renamed or deleted
backstop, or a reviewed tolerance gone stale. Eight regressions were
each confirmed caught, with the tree checksummed back to clean after
every probe. The one surviving `|| true` — the first-release tag lookup
— is in that script's reviewed list with its reason.

Both run in `make lint` and in the already-required "Build, Format &
Analyse" job, so no required-check list or ruleset changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) September 9, 2026 10:19
@MelbourneDeveloper
MelbourneDeveloper deleted the release-gates-go-red branch September 16, 2026 06:13
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