ci(release): per-leg rust-cache keys in build-napi; suite-keyed Tier-B allowance in verify-pr-checks; re-scope #345 (#352, #341, #345) - #369
Merged
Conversation
…7 contexts on release.yml check-suite identity (D-PR8, #341); add 2026-09 fixtures
…-scope #345 to documentation; specs S3+S20
… ids in D-PR7 messages, event-annotated suite log, docblock/comment completeness, drop RED-phase residue, pin assertions, add positive controls (#341)
…; keep KB S19 wording
Extract the repeated `[...releaseSuiteIds].join(', ') || 'none'` expression
(Tier B line and D-PR7 absent-run line in evaluateChecks) into a private
`formatSuiteIds` helper. No logic change; 210/210 gate tests pass.
…ue (#352, #341) S20's rustCacheSteps anchored detection on `- uses: Swatinem/rust-cache@`, so a step written `- name: …` / ` uses: Swatinem/rust-cache@…` returned zero steps and the core per-step assertion iterated nothing. Verified against the live file: with build-napi's step reordered that way the old helper returns [] (only the separate build-napi/build-python non-vacuity guards caught it; a third matrix job would have passed vacuously). Segment steps first, then match `uses:` anywhere in the segment, and read `key:` from within the segment so a bare step cannot borrow the next step's key. hasMatrix anchored `^ strategy:\s*$`, so `strategy:` with a trailing inline comment or written as a flow mapping read as "not a matrix job" and exempted the whole job from S20. Match `^ strategy:(\s|$)` instead. Both hardenings are mutation-verified: with the key dropped and the step reordered (or `strategy:` given a trailing comment) S20 fails with the PF-041 message; release.yml restored byte-identical afterwards. New positive controls PC5b (trailing comment / flow mapping), PC6 (uses: not first key) and PC7 (bare step must not borrow a later step's key) live inside the existing S20 test, so `npm run test:gates` stays 210 pass / 0 fail. Also: remove two spec comments citing commit 3a803f5 as the source of the D-PR7j all-must-pass contract (main's D-PR7j spec cites no commit), remove a stray `// correct spelling` marker, and correct the release-pipeline KB's test:gates count from 211 to 210.
…dynamic CodeQL run does appear in /actions/runs; the path filter is what excludes it); PF-013 in fetch messages; spec naming/shape nits (#341)
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step B2 of the v0.4.3 action plan: three CI-hardening items landed as one CI-gated PR.
build-napiinrelease.ymlhad a bareSwatinem/rust-cache@v2step; all four ubuntu legs and both macOS legs restored ONE cache blob. It now keys onmatrix.settings.target, pinned by spec S20.scripts/verify-pr-checks.mjstoleratedskippedpublish jobs and credited the release-surface contexts by NAME only. Both are now keyed on.github/workflows/release.ymlcheck-suite identity (design id D-PR8), fail-closed, with 2026-09 fixtures added beside the historical 2026-08 set.-zguard), RELEASING.md and the KB now say exactly what each registry check proves. Durable fix tracked in ci(release): migrate crates.io publishing to Trusted Publishing (OIDC) — retires CARGO_REGISTRY_TOKEN and closes the unprobeable-token gap (#345) #368.Also folds in the release-pipeline knowledge base (
.devflow/features/release-pipeline/KNOWLEDGE.md, commit 78cb095) and updates it to the post-B2 state.#352 — per-leg rust-cache keys (PF-041)
Swatinem/rust-cache@v2(v2.9.2) builds its key asv0-rust[-<key>]-<job>-<runner.os>-<runner.arch>-<envhash>-<lockhash>. The env hash coversrustc -vV(the HOST triple) plusCARGO*/CC*/CFLAGS/CXX*/CMAKE*/RUST*env; the cross TARGET is not in it, and build-napi exports itsCARGO_TARGET_*_LINKERvars after the cache step. Live proof in PR run 34065573775: every Linux leg restoredv0-rust-build-napi-Linux-x64-6ff13d87-4c33221b; both macOS legs restoredv0-rust-build-napi-Darwin-arm64-….key: ${{ matrix.settings.target }}on build-napi's step. Thekeyinput precedes the job id, sov0-rust-aarch64-apple-darwin-build-napi-…cannot collide with build-python'sv0-rust-aarch64-apple-darwin--build-python-…. Host is already encoded (runner os/arch + rustc host hash), so only the target is added; nomatrix.settings.host(runner labels float).key: ${{ matrix.target }}-${{ matrix.manylinux }}; its four cache lines are byte-identical here (comment added above). The ticket's "macOS pair share a key" claim was stale.publish-cratesandpublish-npmget a one-line comment; the automatic job-id key is already unique.release.ymlis thewith:/key:pair (verified by diff filter). No job-levelif:,needs:,on:orpermissions:change; no${{inside comments (PF-045); no newuses:(PF-046).pull_requestruns save underrefs/pull/N/merge, invisible to aworkflow_dispatchon the branch;mainholds no build-napi caches (release.yml never runs on pushes to main), so tag runs were always cold and this change is behaviour-neutral on the tag path. Warm evidence therefore needs TWO runs in the SAME scope (two dispatches), never a dispatch after a PR run.release-auth-probe.spec.mjs): every rust-cache step in every matrix job of release.yml must carry a key containingmatrix.; non-vacuity asserts build-napi and build-python are checked and publish-crates is exempt; seven positive controls (bare step, static key, commented-out key, matrix key, commented-outstrategy:,strategy:with trailing comment,uses:not the first key, no key-borrowing from the next step). Proven red on the real file with the key removed.#341 — suite-keyed allowance and D-PR7 attribution (D-PR8)
skippedis allowed iff the name is inTIER_B_EXPECTED_SKIPPED(set unchanged; M10c/S13 still pin it to release.yml's guarded jobs) ANDcheck_suite.idis an integer AND that id belongs to a workflow run whosepathis.github/workflows/release.yml. Any event qualifies — the RELEASING.md dry-run is aworkflow_dispatch, and PR chore(ci): bump checkout v7, setup-node v7, setup-python v7; normalize maturin-action pin (PF-040) #365's head carries a dispatch-only release suite.Version gate,Stage + verify platform packages,Rehearse PyPI publish (no upload)on release-surface PRs): attributed the same way; same-name runs in foreign suites are ignored (and still judged by Tier B); every attributed run must be completed+success (all-must-pass).GET /actions/runs?head_sha=<sha>&per_page=100, at most 5 pages,total_countasserted, projected to{id,path,event,check_suite_id,conclusion}; it is the LAST fetch inmain()so earlier exit-2 paths are unchanged. Call budget 58 worst case (was 53).check_suite→ fail; foreign suite → fail naming both ids;releaseSuiteIdsundefined → allowance DISABLED and D-PR7 cannot attribute (a head with no skipped allow-listed names and an untouched surface still passes); several release suites → each run attributed on its own.app.slugis not consulted: thepathfilter is what excludes the ci.yml run, the dynamic CodeQL analysis run (which DOES appear in/actions/runsasdynamic/github-code-scanning/codeql) and thegithub-advanced-securityumbrella suite (which does not appear there at all).checks-pr366-e02bcf2.json(44 check-runs, 4 suites),runs-pr366-e02bcf2.json(3 runs, one release.yml suite 92290758559),protection-main-2026-09.json(15 contexts).protection-main.json(2026-08, 6 contexts) is byte-identical (md5cd875dabeb4f65bdb31dde15cfc71579).releaseSuiteIdsFromunit +RELEASE_SURFACE.includes(RELEASE_WORKFLOW_PATH)drift guard, and acurrent fixtures (2026-09)describe (CF-1..CF-5), each with a PF-013 flipped-condition control.release.yml suites: 92290758559 (pull_request), fiveallowed (D-PR8)lines, PASS, exit 0."Rehearse PyPI publish (no upload)" — status=completed, conclusion=failure (suite 92287905855).release.yml suites: none,release surface not touched, PASS, exit 0.release.yml suites: 92246852351 (workflow_dispatch), four allowed lines (dispatch-only suite attributed — event-agnostic), D-PR7 absent for the rehearsal, exit 1.#345 — re-scoped to documentation
Verified live 2026-09-07 against
rust-lang/crates.iomain:GET /api/v1/meisAuthCheck::only_cookie()(src/controllers/user/me.rs:38-41); a valid API token, scoped or not, gets HTTP 403this action can only be performed on the crates.io website(src/auth.rs:136-144).src/auth.rs:295,InsecurelyGeneratedTokenRevoked); a well-formed token not in the database gets HTTP 403authentication failed(src/auth.rs:297-303). The live probe with a bogus header returns 401, not the 403 the ticket assumed.GET /api/v1/me/tokens/{id}(src/controllers/token.rs:269-282), accepts legacy unscoped tokens only; a scoped token gets 403this token does not have the required permissions. There is no read-only probe for a scoped publish token. Every request needs aUser-Agent(src/middleware/require_user_agent.rs:35-47).cargo publish(fail-before-write, after the build matrix). v0.4.0 run 33569514359 attempt 1 is the precedent;gh run rerun --failedcompleted it.Landed: the two-line guard comment in
version-gate(no${{, nocargo publish -psubstring), spec S3 now asserts the executableif [ -z "$CARGO_REG_TOKEN" ]; thenline on comment-stripped text (planted control: guard-in-comment-only is rejected; proven red on the real file), RELEASING.md's credential-probe section rewritten as three bullets stating what each registry check proves, prerequisite 3 amended (PF-023), and the fork/Dependabot paragraph corrected: a fail-closedVersion gateon a release-surface PR blocks the merge (D-PR7) by design. #345 will be closed as not planned after this merges; the durable fix (crates.io Trusted Publishing viarust-lang/crates-io-auth-action) is #368.#351
Untouched. The containerised build-python Linux legs remain protected only by the #347 per-leg keys; this PR does not change that.
Verification
Local (all on HEAD 183fc84):
npm run test:gates210 pass / 0 fail (baseline 187);node scripts/verify-no-control-bytes.mjsandnode scripts/verify-versions.mjspass.{"key":"${{ matrix.settings.target }}"}; build-python{"key":"${{ matrix.target }}-${{ matrix.manylinux }}"}; publish-crates/publish-npm nowith.CI:
No cache found.then... Saving cache ...; per-legCache Key:values carry the target, e.g.v0-rust-aarch64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b,v0-rust-x86_64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b,v0-rust-x86_64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b,v0-rust-x86_64-pc-windows-msvc-build-napi-Windows_NT-x64-2113753f-4c33221b(all seven distinct); no leg printedRestored from cache key "v0-rust-build-napi-(the old shared blob);refs/pull/369/mergeholds exactly 7v0-rust-*-build-napi-*keys and none of the old shape. musl readelf gates on both matrices:Shared library: [libc.so], no glibc, no::error::. Version gate:# pass 210/# fail 0,PyPI trusted publisher OK, CI-history gate step-skipped with the notice. Five guarded jobs skipped; everything else success.No cache found.→Saving cache); the CI-history gate executed (not skipped);# pass 210/# fail 0; five guarded jobs skipped.Restored from cache key "v0-rust-<its own target>-build-napi-…" full match: true.and post-stepCache up-to-date.— e.g.Restored from cache key "v0-rust-aarch64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b" full match: true.; env hash4c33221bunchanged between dispatches; the branch scope holds exactly 7v0-rust-*-build-napi-*keys; readelf gates green.node scripts/verify-pr-checks.mjs 369: PASS on head 183fc84 —release.yml suites: 92452493617 (workflow_dispatch), 92451554644 (workflow_dispatch), 92449765759 (pull_request); fifteenallowed (D-PR8)lines (five per suite);release surface touched (1 file(s): .github/workflows/release.yml);✓ PASS — all 15 required contexts completed+success; merged with the printed commandgh pr merge 369 --squash --admin --match-head-commit 183fc84639870c2cfba9512a667775a9d2c42be5verbatim.Publish to TestPyPI (rehearsal)failed at upload with HTTP 400Non-user identities cannot create new projects … pending publisher … project name incorrectly— the test.pypi.org record exists but its project name is notmarkdown-script. User-side fix; not a blocker for this PR.Snyk: the local Snyk MCP server fails to start (ENOENT), so the
security/snyk (dean0x)PR check is the scan of record for this change (no new first-party runtime code; the changes are a gate script, specs, fixtures, workflow comments and docs).Related Issues