Repository navigation
Description
A perry-runtime change broke the link of five perry-ext-* crates and no per-PR gate could have caught it (#7650 → fixed in #7655). It would have surfaced at the next tag, days later.
The gap
cargo-test scopes a per-PR run to the changed crates' reverse-dependency closure (scripts/ci_test_scope.py); the full workspace runs on tags and nightly only. perry-ext-* sits outside the closure of a GC change to perry-runtime, so nothing built it.
But the ext crates are structurally more fragile than the closure suggests, not less: they link a feature-stripped runtime through perry-ffi's runtime-link, built with -Wl,-dead_strip. Any new reference edge inside perry-runtime can keep a chain alive that the stripper had been removing, and the failure is a link error, not a test failure:
Undefined symbols for architecture arm64:
"_js_blob_new", "_js_fetch_with_options", "_js_fetch_notify_signal_aborted"
In #7650 the edge was one added call — pin_object → arena::classify_heap_space — in code that had previously done a raw flag write.
Proposal
Add a cheap per-PR arm that links (not runs) the ext crates whenever crates/perry-runtime/** or crates/perry-stdlib/** changes:
cargo test --release --no-run -p perry-ext-pdf -p perry-ext-lru-cache \
-p perry-ext-node-forge -p perry-ext-mongodb -p perry-ext-http
Those five are the ones that actually failed; a wider set is better if it is affordable. Measured cost on a dev Mac from a warm target dir: ~5 minutes for all five together, and it needs no runtime behaviour, no fixture and no host pinning — it is a linker check.
Alternatively, teach ci_test_scope.py that perry-runtime/perry-stdlib changes pull in perry-ext-* regardless of the declared dependency graph, since the real coupling here is the link, not the crate graph.
Two things to get right
- The gate must assert it linked something. A scope rule that silently selects zero ext crates would be green forever — the failure mode this repo has shipped four times (gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942/GC testing: PERRY_GC_FORCE_EVACUATE is inert for gc()-driven tests (full mark-sweep + forced conservative scan) — stress claims may be unsupported #6946, gc-matrix: --pressure disables the very path #7019 added — the 'default' arm runs ZERO copying minors on all 22 corpus rows #7024, gc-matrix: the 'moved' liveness counter sums the C4b mark-sweep evacuation with the copying minor, so a move-arm can be green with zero scavenged objects #7025). Assert a non-zero count of built targets.
- Run it once before making it required, per CLAUDE.md's corollary: a new gate has never been green, so promoting it immediately blocks every open PR.
Context: #7650, #7655; the ext staticlib feature-stripping is also behind #7522 and #6847.
Activity
proggeramlug commentedon Aug 8, 2026
Correction to my own framing above. I wrote that this "would have surfaced at the next tag, days later". That is wrong:
test.ymlalso triggers onschedule(cron: '0 4 * * *'), and the workflow's own comment says the point of it plainly — "a cross-crate regression that slips a scoped PR is caught here within a day.scheduleis notpull_request, so the cargo-test job runs the full workspace."So the safety net exists and is roughly a day wide. That weakens the urgency but not the case:
mainthat every PR branches from, and the author who caused it has moved on by the time it reports.perry-runtime/perry-stdlibchanges attributes it to the PR that caused it, before it lands.The proposal stands; the justification is "attribution and blast radius", not "otherwise invisible". Cost/benefit for a 5-minute conditional arm still looks right to me, but it is a smaller fire than I first wrote.
ci: per-PR ext-link gate so a perry-runtime change cannot silently br…
ci: per-PR ext-link gate so a perry-runtime change cannot silently br…
fix(ext-fetch): let the crate's own test binary link, restoring the e…