Skip to content

Cap what a single fork pull request can spend - #122

Merged
gvonnessi merged 3 commits into
mainfrom
ci/limit-fork-pr-runs
Aug 20, 2026
Merged

Cap what a single fork pull request can spend#122
gvonnessi merged 3 commits into
mainfrom
ci/limit-fork-pr-runs

Conversation

@gvonnessi

Copy link
Copy Markdown
Collaborator

Cap what a single fork pull request can spend

This workflow is reachable from a fork pull request and had neither a
concurrency group nor a job timeout, so it inherited GitHub's defaults for
both: unlimited concurrent runs, and six hours per job. On paid
Blacksmith runners that is a real number rather than a theoretical one —
15 job runs at the six-hour ceiling is 90 compute-hours for a single
push, and without a concurrency group a contributor pushing five times held
five complete sets at once rather than superseding the previous one.

The repo now requires approval for every external contributor's fork PR, so
nothing external starts without a deliberate click. This is the second half:
the approval gate decides who runs, and these two settings decide how
much
a run that has been approved can cost. It bounds our own pushes too,
which is where most of the waste actually was.

Sized against measurement rather than a guess. Over the last six runs the
slowest job here is 4m25s; 20 minutes leaves generous headroom while turning
the worst case from 90 compute-hours into about 5. A job that hits 20
minutes is not a slow job, it is a stuck one.

Change

  • concurrency group keyed on workflow + ref, with cancel-in-progress: true
  • timeout-minutes: 20 on all 11 job definitions

Worst case per push

before after
15 job runs at the ceiling 90 compute-hours ~5 compute-hours
repeated pushes accumulate superseded

Test plan

  • YAML parses; triggers unchanged; step count identical to main
  • Every job definition carries timeout-minutes
  • This PR's own run is the check — confirm it passes well inside 20 min
  • Push twice in quick succession and confirm the first run is cancelled

This workflow is reachable from a fork pull request and had neither a
concurrency group nor a job timeout, so it inherited GitHub's defaults for
both: unlimited concurrent runs, and **six hours** per job. On paid
Blacksmith runners that is a real number rather than a theoretical one —
15 job runs at the six-hour ceiling is 90 compute-hours for a single
push, and without a concurrency group a contributor pushing five times held
five complete sets at once rather than superseding the previous one.

The repo now requires approval for every external contributor's fork PR, so
nothing external starts without a deliberate click. This is the second half:
the approval gate decides *who* runs, and these two settings decide *how
much* a run that has been approved can cost. It bounds our own pushes too,
which is where most of the waste actually was.

Sized against measurement rather than a guess. Over the last six runs the
slowest job here is 4m25s; 20 minutes leaves generous headroom while turning
the worst case from 90 compute-hours into about 5. A job that hits 20
minutes is not a slow job, it is a stuck one.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Neither failure was caused by the timeout change in this branch — both
failed well inside the new 20-minute cap, and both fail on any cdx-core
commit made today. `main` last ran in June and has not run since, so the
drift accumulated where nothing was looking.

**Lint.** `clippy::manual_assert_eq` did not exist when this last passed;
under `-D warnings` a new lint is a new failure. One site,
`cdx-core/src/document/tests.rs:227`, and the line directly above it
already used `assert_eq!`, so the fix is what the surrounding code was
doing anyway. It also buys a real failure message: `assert!(len() == 1)`
reports only that a bool was false, while `assert_eq!` prints both sides.

**Security Audit.** Two advisories published since June, both against
*transitive* crates — neither appears in any `Cargo.toml`, so this is a
lockfile-only change with no manifest touched and no semver decision taken:

- RUSTSEC-2026-0204, `crossbeam-epoch` 0.9.18 -> 0.9.20 (solution: >= 0.9.20)
- RUSTSEC-2026-0185, `quinn-proto` 0.11.13 -> 0.11.17 (solution: >= 0.11.15)

`cargo update` also adds `rand` 0.10.2 and `rand_pcg` 0.10.2 as new
transitive entries of the newer `quinn-proto`; the existing `rand` 0.9.2
pin is untouched.

Verified locally on rustc 1.97.1, running CI's own commands rather than
approximations of them: `cargo audit --ignore RUSTSEC-2023-0071 --ignore
RUSTSEC-2026-0037` exits 0, with only the five pre-existing *allowed*
warnings (unsound and yanked) remaining, and `cargo clippy --workspace
--all-targets --all-features -- -D warnings` is clean.

The five allowed warnings are deliberately left alone: they were passing
before, they are warnings rather than vulnerabilities, and folding them
into a drift fix would hide a judgement call inside a mechanical change.
…warns

The previous commit fixed what `cargo audit` was failing on and stopped
there, because that is the command I checked. The job runs **two** tools
and they do not agree: `cargo audit` treats an `informational = "unsound"`
advisory as a warning, while `cargo deny` treats it as `error[unsound]`.
So step 7 went green and step 9 stayed red on an advisory that had been
sitting in audit's own output the whole time, counted among its "5 allowed
warnings".

RUSTSEC-2026-0190, `anyhow` — unsoundness in `Error::downcast_mut()`. The
advisory gives `patched = [">= 1.0.103"]`; this moves 1.0.101 -> 1.0.104.
`cdx-cli/Cargo.toml` already declares `anyhow = "1.0"`, so the caret range
admits it and this stays lockfile-only, like the previous commit.

The two configurations have drifted and this does not fix that. `ci.yml`
passes `--ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2026-0037` to audit,
while `deny.toml` ignores only `RUSTSEC-2023-0071` and additionally sets
`yanked = "warn"`. RUSTSEC-2026-0037 is ignored by one tool and unknown to
the other, so the pair can disagree again on the next advisory. Left alone
here: reconciling them is a policy decision about which tool is the source
of truth, not a drift fix.

Verified locally on rustc 1.97.1 with **both** of CI's commands this time,
which is the check that was missing before:
`cargo audit --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2026-0037` exits
0; `cargo deny check` reports "advisories ok, bans ok, licenses ok,
sources ok"; and `cargo clippy --workspace --all-targets --all-features --
-D warnings` is clean.
@gvonnessi
gvonnessi merged commit 57fb80a into main Aug 20, 2026
16 checks passed
@gvonnessi
gvonnessi deleted the ci/limit-fork-pr-runs branch August 20, 2026 11:57
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