fix(release): stop smoke_beta silently skipping on promote-mode betas (#11582)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release channel system re-smokes every published beta as
post-publish verification (`smoke_beta`)
> - The candidate-branch beta lane (#11209) added
`verify_beta_candidate` to `publish_beta`'s needs; that job is skipped
on every normal promote-mode beta
> - `smoke_beta`'s condition has no status-check function, so GitHub
attaches an implicit `success()` that evaluates the needs chain
transitively — a skipped ancestor makes it false
> - This pull request makes the condition explicit so promote-mode betas
smoke again, and pins the shape in the workflow wiring test
> - The benefit is that the post-publish beta gate actually runs instead
of silently skipping

## Linked Issues or Issue Description

**What happened?**

Beta `2026.818.0-beta.0` (run 32082007439) published successfully, but
its post-publish `smoke_beta` job was skipped. No configuration or input
asked for that: the run was a plain `channel: beta` dispatch with
`dry_run` at its default `false`, and the same expression
`!inputs.dry_run` evaluated true inside `publish_beta`'s own steps (the
Docker dispatch step ran).

**Expected behavior**

Every non-dry-run beta publish is followed by the release smoke suite
against the exact published version, as documented in `doc/RELEASING.md`
and `doc/RELEASE-CHECKLIST.md`.

**Steps to reproduce**

Dispatch `release.yml` with `channel: beta` promoting a nightly (promote
mode). `verify_beta_candidate` is skipped by design; `publish_beta` runs
through its explicit `!cancelled()` condition; `smoke_beta` then skips
because its implicit `success()` sees the skipped ancestor in the
transitive needs chain (actions/runner#2205 semantics). The beta
published on 2026-08-11 predated #11209, so this never surfaced before.

**Paperclip version or commit**

master at `43ab441f0` (workflow file, current head).

Related (not duplicates): #11209 introduced the candidate lane whose
skipped job triggers this; #11208 covers the adjacent tag-push failure
playbooks.

## What Changed

- `smoke_beta`'s condition becomes `!cancelled() &&
needs.publish_beta.result == 'success' && !inputs.dry_run` — an explicit
status-check function suppresses the implicit `success()`, and the
result check keeps the dependency on a successful publish.
- A comment above the job records why the explicit form is load-bearing.
- `scripts/__tests__/release-verify-workflow.test.mjs` pins the new
shape so the implicit form cannot silently return.

## Verification

- `node --test scripts/__tests__/release-verify-workflow.test.mjs` — 8
pass, including the new assertion.
- `release.yml` re-parsed as YAML.
- The exact skip is visible on run 32082007439 (`smoke_beta: skipped`
after `publish_beta: success`); the coverage gap for that beta was
closed manually by dispatching `release-smoke.yml` with
`paperclip_version: beta` (run 32084880767).
- Not exercised end-to-end: the corrected condition needs the next real
promote-mode beta to demonstrate; the expression change is minimal and
the semantics are the documented actions/runner behavior.

## Risks

- Low risk: condition-only change on one job plus a test. Dry runs still
skip the smoke (`!inputs.dry_run` retained). Candidate-mode betas, where
`verify_beta_candidate` actually runs, behave as before.

## Model Used

Claude Fable 5 (Claude Code)

## Pre-submission checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
This commit is contained in:
Devin Foley 2026-08-17 17:45:44 -07:00 committed by GitHub
parent 4af55ba6bd
commit 2ec984502a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 21 additions and 1 deletions

View File

@ -623,9 +623,16 @@ jobs:
# Post-publish verification: run the release smoke suite against the exact
# beta version that was just published.
#
# The condition must carry an explicit status-check function: without one,
# GitHub attaches an implicit success(), which evaluates the needs chain
# transitively — and publish_beta's chain contains verify_beta_candidate,
# which is skipped on every promote-mode beta. The implicit form silently
# skipped this job on the first promote-mode beta after the candidate
# lane landed.
smoke_beta:
needs: publish_beta
if: ${{ !inputs.dry_run }}
if: ${{ !cancelled() && needs.publish_beta.result == 'success' && !inputs.dry_run }}
uses: ./.github/workflows/release-smoke.yml
with:
paperclip_version: ${{ needs.publish_beta.outputs.beta_version }}

View File

@ -60,6 +60,19 @@ test("candidate-branch betas are validated and fully verified before publish", (
assert.match(releaseWorkflow, /needs\.verify_beta_candidate\.result == 'success'/);
});
test("post-publish beta smoke survives the skipped candidate-verification ancestor", () => {
const releaseWorkflow = readWorkflow("release.yml");
// publish_beta's needs chain contains verify_beta_candidate, which is
// skipped on promote-mode betas. An `if:` without a status-check function
// gets an implicit success() that evaluates that chain transitively and
// silently skips the smoke. The condition must stay explicit.
assert.match(
releaseWorkflow,
/smoke_beta:\n\s+needs: publish_beta\n\s+if: \$\{\{ !cancelled\(\) && needs\.publish_beta\.result == 'success' && !inputs\.dry_run \}\}/,
);
});
test("every lane's tag push degrades to recovery instructions when rejected", () => {
const releaseWorkflow = readWorkflow("release.yml");