fix(release): reject promotion sources that predate their channel tooling (#11197)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release subsystem promotes builds along canary → nightly → beta
→ stable, and each publish job checks out the promotion's source commit
and runs that tree's release tooling
> - The first beta dispatch failed with `unexpected argument: beta`: the
selected nightly's source predated the beta channel, so its `release.sh`
did not know the argument
> - The failure was clean (argument parsing, nothing published) but
cryptic, and the same trap waits for any promotion of a source older
than its target channel's tooling
> - This pull request makes the selection jobs reject such sources with
an actionable error and documents the property
> - The benefit is that a bootstrapping or old-source promotion fails in
seconds with instructions, instead of mid-publish with a parser error

## Linked Issues or Issue Description

Refs #11008 — the guard hardens the beta promotion flow introduced
there, after its first dispatch surfaced the gap described below.

**Subsystem affected**

Release automation: `.github/workflows/release.yml`, `doc/RELEASING.md`,
workflow wiring tests.

**Problem or motivation**

Run 31444045044 (first beta dispatch) failed in `publish_beta` with
`unexpected argument: beta`. Promotions deliberately build from the
pinned source commit, which means they also run that commit's
`scripts/release.sh` — and a source that predates the target channel's
introduction cannot publish it. Nothing guards this today; the error
surfaces deep in the publish job with no explanation.

**Proposed solution**

Guard at selection time: `select_nightly` requires the source canary's
`release.sh` to know the nightly channel, and `select_beta` requires the
source nightly's `release.sh` to know the beta channel. Each guard
literally matches the channel case arm and fails closed with a clear
message naming the remedy (promote a newer source). Document the
tooling-era property in `RELEASING.md` and pin the guards with a wiring
test.

## What Changed

- `.github/workflows/release.yml`: tooling-era guards in
`select_nightly` and `select_beta`
- `doc/RELEASING.md`: documents that promotions run the source commit's
release tooling
- `scripts/__tests__/release-verify-workflow.test.mjs`: wiring test
pinning both guards

## Verification

- Wiring tests: 5 pass
- Guard expressions exercised against real commits: accepts the
beta-capable merge commit of the beta-channel change, rejects a pre-beta
commit
- YAML parse of the workflow
- After merge: the next beta dispatch selects a beta-capable nightly and
passes the guard

## Risks

- Low. Selection-time check only; the guards match the channel case arm
literally and fail closed (with the same actionable message) if that
line is ever reformatted

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) in Claude Code, with
extended thinking and full tool use. All changes model-authored under
human direction.

## 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
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending — will confirm before
merge)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending — will confirm before merge)
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Devin Foley authored and GitHub committed 2026-08-10 17:22:54 -07:00
1 parent 35aaaa0bd0
commit 6601014898
3 files changed
+31

No files matched your search

+18
View File
@@ -192,6 +192,15 @@ jobs:
exit 0
fi
# Promotions run the release tooling of the source commit, so the
# source must already understand the nightly channel. (Literal match
# of release.sh's channel case arm; if that line is reformatted this
# fails closed and should be updated alongside it.)
if ! git show "${sha}:scripts/release.sh" | grep -qF 'canary|nightly'; then
echo "Error: source commit $sha predates nightly release tooling; promote a newer canary." >&2
exit 1
fi
echo "proceed=true" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "canary_version=$canary_version" >> "$GITHUB_OUTPUT"
@@ -380,6 +389,15 @@ jobs:
exit 1
fi
# Promotions run the release tooling of the source commit, so the
# source must already understand the beta channel. (Literal match of
# release.sh's channel case arm; if that line is reformatted this
# fails closed and should be updated alongside it.)
if ! git show "${sha}:scripts/release.sh" | grep -qF 'canary|nightly|beta|stable)'; then
echo "Error: source commit $sha predates beta release tooling; promote a newer nightly whose source contains the beta channel." >&2
exit 1
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "nightly_version=$nightly_version" >> "$GITHUB_OUTPUT"
{
+4
View File
@@ -148,6 +148,10 @@ Betas are manual promotions. Dispatch
it does not exist or already shipped as a beta
- the publish waits for approval in the **`npm-beta` environment** — its
required reviewers are the promotion gate
- promotions run the release tooling of the source commit, so the source
nightly must postdate the beta channel's introduction; the selection job
rejects older sources with a clear error (in practice every nightly cut
after the beta tooling merged qualifies)
- the same commit is republished as `YYYY.MDD.P-beta.N` under the npm
dist-tag `beta`, tagged `beta/vYYYY.MDD.P-beta.N`, and `docker.yml` is
dispatched at that tag to publish the `:beta` images
@@ -38,6 +38,15 @@ test("onboard smoke container binds beyond loopback so the mapped port is reacha
assert.match(dockerfile, /onboard --yes --bind lan/);
});
test("promotion selection guards against sources that predate their channel tooling", () => {
const releaseWorkflow = readWorkflow("release.yml");
// Promotions run the source commit's release.sh, so selection must reject
// sources whose tooling does not know the target channel yet.
assert.match(releaseWorkflow, /git show "\$\{sha\}:scripts\/release\.sh" \| grep -qF 'canary\|nightly'/);
assert.match(releaseWorkflow, /git show "\$\{sha\}:scripts\/release\.sh" \| grep -qF 'canary\|nightly\|beta\|stable\)'/);
});
test("release smoke workflow extends the container readiness budget for CI", () => {
const smokeWorkflow = readWorkflow("release-smoke.yml");
const harness = readFileSync(path.join(repoRoot, "scripts/docker-onboard-smoke.sh"), "utf8");