ci(dependabot): the caps Dependabot was supposed to respect, the gate that asked the wrong question, and a test that measured the scheduler - #121
Conversation
…respect Dependabot rewrites a declared upper bound rather than respecting it, so a load-bearing cap in pyproject.toml is decorative unless it is restated as an ignore range here. PR #66 widened `annotated-types<0.8` -> `<0.9` (both declaration sites) and `ruff>=0.4,<0.16` -> `<0.17`; the lock resync then propagated 0.8.0/0.16.0 into constraints.lock, so ci.yml's `--constraint` pinned CI *to* the broken versions — 8 FHIR ImportErrors on windows-2022 and 868 ruff findings on ubuntu. `python-deps` groups on `*`, so the two of them red the whole batch and hold every benign bump in it hostage. The ignore entries suppress the security track for the named RANGE too; that is accepted and recorded inline, because detection does not depend on Dependabot — the required `pip-audit -r requirements.lock` gate reds within ~24h and a human lifts the pyproject cap, which is the real ceiling either way. Also corrects two comments the evidence retired: the header still claimed no committed lockfile exists, and the PEP 735 question is settled — PR #66 bumped `zizmor` inside `[dependency-groups].ci-scanners`, so the uv ecosystem does reach it.
…er runs Both annotated-types and ruff caps said "CI installs with a FRESH resolve (uv pip install -e ...)" in the present tense. It does not: every uv install in ci.yml, quality-advisory.yml and dast.yml passes `--constraint constraints.lock`. That mattered for more than tidiness — it framed the cap as a CI workaround with "or once CI installs from the hashed lock" as an exit condition, which is wrong in both directions. The cap bounds the RESOLVER, and the lock carries whatever the resolver already chose, so installing from the lock would not retire it; meanwhile it is also what bounds every resolve CI never sees (an end user's `pip install messagefoundry[fhir]`, the local dev venv, dispatch-only legs). The fhir extra additionally claimed the cap "lives here rather than in [project.dependencies]" while an identical cap sat 67 lines above it. Stops restating the ruff finding count: 626, ~525 and 868 were three numbers for one fact at two different scopes. States it once, as a bound, dated. No version specifier changed and no lock is touched — comments only.
The paths filter listed only `.github/**`, but zizmor's own pinned version lives in pyproject.toml's [dependency-groups].ci-scanners and reaches the install step through ci/locks/ci-scanners.lock — outside the filter. So PR #66, which bumps zizmor 1.5.2 -> 1.28.0, ran 33 check contexts and not one of them was zizmor: a 23-minor jump against a deliberately clean baseline would have been adjudicated by the 06:00 cron, against main, after merge. Adds the lock to the filter and corrects the header, whose "a Python / docs / test PR cannot change its result" is precisely what a lock-only bump disproves. The lock also carries bandit and pip-audit, so their bumps now trigger this workflow too (~1 billed min) — arguably correct, since bandit is the other clean-baseline gate. Also states the filter once: three docs restated its contents verbatim, which is three things to forget when it changes. They now point at the workflow.
fetch-metadata's scalar `previous-version` is the FIRST dependency's, even on a grouped PR — output.ts:27 reads `firstDependency?.prevVersion` (verified at the pinned SHA 25dd0e34, v3.1.0). The gate applied it to every name in the group, so on a five-dep PR it asked about four versions the PR does not touch. That fails in the PERMISSIVE direction: advisory ranges are mostly open at the bottom, so a low first-dep version confirms almost anything. Demonstrated on the real shape — a group of aiohttp 3.9.0 + Pillow 10.2.0 queries `pillow@3.9.0` (a 2016 release) and returns a confirmation. Switches to `updated-dependencies-json`, which carries a per-dependency prevVersion, and closes a fail-open case the scalar never had: update_metadata.ts:101 falls back to an EMPTY prevVersion for index > 0, and an unknown previous version must deny rather than skip on a gate whose contract is to fail closed. Verified by executing the step against a stubbed advisories endpoint: each dep is queried at its own version; no-advisory, empty-prevVersion, malformed-metadata and version-track all deny. The old loop was run against the same input first, to confirm the test can see the defect. Also adds a 5-day cooldown to github-actions — the one ecosystem where auto-merge demonstrably fires, and the one where a compromised release runs inside CI with the job's token. SHA-pinning stops tag mutation, not a malicious new release. And scopes the IN SCOPE header, which read as though uv routinely auto-merges; in practice `patterns: ["*"]` plus the deny-list makes it review-by-default.
…mutual exclusion The lock is correct. `test_only_one_of_eight_concurrent_claimants_wins` counted winners, which measures the scheduler: the barrier releases the cohort together, but each process then runs Enter-CoordLock's own prologue — a `git rev-parse` spawn at lock.ps1:47 — before its deadline is set at lock.ps1:56, and nothing synchronizes that. A winner count tolerates only (hold - timeout) = 500 ms of that skew. So a straggler that arrives after the winner released acquires legitimately and is counted as a violation. That is what failed on windows-2025 in run 30672004091, and it is not a flake: under 40 CPU burners on a 20-core box the old assertion fails 6/6 (2-3 winners), the new one passes 6/6. Asserts occupancy instead — a CreateNew sentinel taken under the lock, the same atomic test-and-set the lock itself uses, with no reference to a clock. Validated both directions: with Enter-CoordLock stubbed to a no-op it fails with 7 simultaneous holders, so the assertion can see the defect class it claims to exclude. Cost: hold_ms 1500 -> 8000 to keep a ~3.3x margin over the measured skew, so this test's floor wall-time is ~9 s.
Every uv install in ci.yml, quality-advisory.yml and dast.yml passes `--constraint constraints.lock`; the four plain-pip legs did not, so they re-resolved from pyproject's `>=` floors and would have adopted ruff 0.16.0 and annotated-types 0.8.0 on the day those shipped, exactly as PR #66 did. A benchmark leg is the worst place for that — a silent dependency change reads as a performance regression. freethread-smoke stays unconstrained on purpose and now says so. The lock records what resolved for the GIL build; pinning it there can leave a dep with no cp314t wheel at that version and send pip to a source build, so the canary would stop measuring free-threading and start measuring the lock.
|
Merged main in and picked up one commit that is not mine in spirit. The first CI pass failed main was already red for this — both the ubuntu and windows-2022 legs — and every PR opened since Fixed as Also merged |
07c9171 to
d22d6f9
Compare
|
Correction to my previous comment — the BACKLOG #320 fix has been split out to #122. Commit Consequence, stated plainly: this PR will stay red on The |
The earlier "state it once, link to it" edit replaced the restated filter contents with "(see `zizmor.yml`)" at four sites — but every one of those lines already has zizmor.yml as its subject, so the pointer was circular. CI.md:147 was the worst: "`zizmor.yml` (which is already paths-filtered (see `zizmor.yml`))". Drops the pointer instead. The subject is already named, so plain "paths-filtered" states the fact without restating its contents, which is what the rule was after.
…d pin the premises (#130) * build(deps): bump the python-deps group across 1 directory with 3 updates Bumps the python-deps group with 3 updates in the / directory: [fastapi](https://github.com/fastapi/fastapi), [prometheus-client](https://github.com/prometheus/client_python) and [zizmor](https://github.com/zizmorcore/zizmor). Updates `fastapi` from 0.139.2 to 0.140.4 - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](fastapi/fastapi@0.139.2...0.140.4) Updates `prometheus-client` from 0.25.0 to 0.26.0 - [Release notes](https://github.com/prometheus/client_python/releases) - [Commits](prometheus/client_python@v0.25.0...v0.26.0) Updates `zizmor` from 1.5.2 to 1.28.0 - [Release notes](https://github.com/zizmorcore/zizmor/releases) - [Changelog](https://github.com/zizmorcore/zizmor/blob/main/docs/release-notes.md) - [Commits](zizmorcore/zizmor@v1.5.2...v1.28.0) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.140.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: prometheus-client dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: zizmor dependency-version: 1.28.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: python-deps ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): resync exported lock files (DEP-1) * fix(ci): label the setup-node pins v7.0.0 — the SHA has been v7 since 2026-07-26 Three pins read `actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6` while that SHA is what `refs/tags/v7` AND `refs/tags/v7.0.0` point at — verified against the upstream refs API, not inferred from the lint. So the repo has been running setup-node v7 for a week with a comment asserting v6. This RATIFIES a major already in force; it is not a cosmetic label fix. v7 is green on both `ide build` legs and on the required `npm-audit` job, and reverting to the v6 line would be worse, not safer: v6 carries fast-xml-parser 5.9.3, which sits inside GHSA-8r6m-32jq-jx6q, while v7.0.0's 5.7.2 is in no open advisory range. The SHA is unchanged. Only the comment moves, and all three move together — one Dependabot change introduced them and they must not drift apart. Surfaced by zizmor 1.28.0's ref-version-mismatch audit, which could not run before #121 put ci/locks/ci-scanners.lock in the zizmor paths filter. * ci(zizmor): retire the dead ignore entry and the claims that rotted with it The config header calls every entry "a REVIEWED, justified non-finding", which makes a stale one worse than noise — it reads as a reviewed risk that is actually unexamined. Five had rotted, each verified against the tree rather than taken from the lint: * `release-sync-check.yml` is ignored under artipacked, but that workflow was deleted in 59fbc93 (2026-07-26). An entry naming a file that cannot produce a finding is dead config presenting as coverage. * "The jobs below do not push" — `dependabot-lock-resync.yml` is in that list and pushes at line 160. The carve-out two lines below already says so. * "release.yml sets persist-credentials: false on both checkouts" — it has three (73, 371, 498), all false. The claim is right and the count is a liability, so the count goes. * "cla.yml ... does check out" — cla.yml has zero checkouts. The comparison it was drawing still holds on a different axis (third-party code vs none), so that is what it now says. * "See the SECURITY note in cla.yml" — cla.yml contains no such note. A security justification whose only citation does not resolve. Adds tests/test_zizmor_config_hygiene.py so the first one cannot recur: every ignore entry must name a live workflow. Validated both directions — it passes on this tree and fails when the deleted entry is put back. It also prints what it scanned, so a run that silently covered nothing is distinguishable from a pass. * ci(zizmor): record bot-conditions on the resync gate as a justified non-finding zizmor 1.28.0 reports `github.triggering_actor == 'dependabot[bot]'` at dependabot-lock-resync.yml:76 as a spoofable bot actor check. The observation is correct and the gate is still FAIL-CLOSED, because that expression is the second half of a conjunction: github.event.pull_request.user.login == 'dependabot[bot]' && github.triggering_actor == 'dependabot[bot]' Clause 1 is the immutable PR author — the exact context this audit recommends as the replacement. A conjunction only narrows, so spoofing the actor can make the job SKIP, never run, and skipping is safe: DEP-1 lock sync is independently enforced by security.yml's pip-audit job, a required context on an unfiltered trigger. zizmor itself agrees enough to report at MEDIUM confidence, treating `And` as non-dominating. So this is suppressed, line-anchored, not fixed — and PR #121's description needs no correction: it claimed a human push makes the job skip, which is the fail-closed direction, while this audit is about a check wrongly passing. Two things the suppression does NOT rely on prose for: * tests/test_dep1_lock_resync_lockstep.py now pins the premise — clause 1 present, conjoined, no `||`. Validated by mutation: flipping `&&` to `||` and deleting clause 1 each turn it red. Without this the suppression would be an assertion about a shape nothing enforces. * The comment records that zizmor's auto-fix for this audit is classified SAFE, so a bare `zizmor --fix` would apply it silently — substituting clause 1 for clause 2, collapsing the condition to `A && A` and deleting the self-trigger skip, with every check still green. * ci(zizmor): accept the archived CLA action as a dated residual (ADR 0034) zizmor 1.28.0's archived-uses audit is right: contributor-assistant/github-action is archived upstream (API archived: true, last push 2026-03-23) and v2.6.1 is the final release. Our pin equals that tag's commit exactly, so there is no later patch to move to. Recorded as an accepted residual, not waved away as a false positive. The SHA pin closes tampering — a bundled JS action's SHA fully determines the bytes that run — but not the axis the audit names: a flaw already at that SHA will never be fixed. Not replaced now, deliberately. `cla` is a required status context AND is the job's own conclusion, so a broken step blocks every contribution; and because pull_request_target/issue_comment workflows run only from the default branch, a replacement cannot be exercised on the PR that makes it — it would land on main untested with approvals at 0 and auto-merge armed. The forcing function is not this lint: action.yml declares `runs.using: node20` at the pin and at the archived HEAD, so an archived repo can never re-declare node24 and GitHub's fall-2026 Node20 removal decides it. The suppression is dated and says to re-check before then, rather than being open-ended. The ADR amendment records a contingency candidate with its provenance checked rather than asserted: iainmcgin/cla-github-action is real, Apache-2.0, unarchived — and its v3.2.0 is an ANNOTATED tag, so the ref resolves to a tag object, not the commit. The commit `uses:` must pin was dereferenced and confirmed. Pinning the tag-object SHA would be the same mismatch class fixed two commits ago. * ci: hand the setup-node label fix to the timeout PR to clear the collision gate Drops this branch's one-line ci.yml hunk (`# v6` -> `# v7.0.0` at :301). The change is not abandoned — the "Stuck CIs" session is carrying it in the same commit as the W22/W25 timeout raise. Why: the collision gate keys on a branch's DIFF against main, not on whether a session is actively editing. Standing down verbally did not clear it; while this branch carried any ci.yml hunk, `ci.yml` read as in-flight and the timeout edit was refused. That mattered because #130 is itself running against the 26:00 cap the timeout fix raises — so a timeout on this PR would have trapped the fix for the timeout behind a PR that died of it. Known consequence, accepted: until that commit reaches main, zizmor reports ref-version-mismatch on ci.yml:301 again — measured, 1 medium, exit 13 — so this PR's zizmor check is RED in the interim. It clears when main carries the label fix and this branch merges it. The other two sites (security.yml:159, :189) keep their fix here. Side benefit: #130 is now purely "adopt a scanner and adjudicate its findings", which is the topic separation this PR argued for. * Revert "ci: hand the setup-node label fix to the timeout PR to clear the collision gate" This reverts dcccf9d. The handover did not complete: #131 landed the W22/W25 timeout raise but does NOT touch setup-node, so nothing else carries the `# v6` -> `# v7.0.0` label. Taking it back is now urgent rather than tidy. zizmor is deliberately NOT a required context, so a red zizmor cannot block #130 — and #130 has auto-merge armed. Left as-is, this PR would have landed zizmor 1.28.0 on main together with an unfixed ref-version-mismatch, redding the 06:00 cron that nothing alerts on (nightly-notice.yml watches CI only). That is precisely the outcome this PR exists to prevent, arriving through the PR that prevents it. The reason for handing it over is gone: the collision gate keys on a branch's diff, and #131's ci.yml edit is already committed and pushed, so re-adding this hunk cannot block it. Restores one comment. SHA unchanged; the pin has resolved to v7.0.0 since 2026-07-26. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Triage of the red CI legs plus an end-to-end Dependabot audit. Six commits, one layer each.
The main-branch red (
test_run_load_end_to_end_no_loss) is not here — #115 fixed that and has merged.What was failing
ImportErrors (windows-2022) + 868 ruff findings (ubuntu)ignore:, so Dependabot widened both deliberate capstest_coord_lock"expected exactly 1 winner, got 2" (windows-2025, run 30672004091)The caps were worse than decorative
Dependabot rewrites a declared upper bound rather than respecting it. PR #66 widened
annotated-types<0.8→<0.9(both declaration sites) andruff>=0.4,<0.16→<0.17, and thelock resync then propagated 0.8.0/0.16.0 into
constraints.lock— soci.yml's--constraintpinned CI to the broken versions.
python-depsgroups on*, so the two of them red the wholebatch and hold every benign bump hostage.
The npm ecosystem has carried exactly this pattern since #649/#877; uv never got it.
Accepted trade-off, recorded inline: a Dependabot
ignoresuppresses the SECURITY track for thenamed range too (
update-typesis version-only and cannot express a range). Detection is untouched —the required
pip-audit -r requirements.lockruns on every PR and a daily cron, so a withheld fixreds a merge-blocking gate within ~24h and a human lifts the pyproject cap, which is the real ceiling
either way.
The GHSA gate asked about the wrong versions
fetch-metadata's scalarprevious-versionis the first dependency's even on a grouped PR —output.ts:27readsfirstDependency?.prevVersion, verified at the pinned SHA25dd0e34(v3.1.0).The gate applied it to every name in the group, so a five-dep PR asked about four versions the PR does
not touch. That fails permissively: advisory ranges are mostly open at the bottom, so a low
first-dep version confirms almost anything. Demonstrated on the real shape — a group of aiohttp 3.9.0
pillow@3.9.0, a 2016 release, and returns a confirmation.Switched to
updated-dependencies-json, which carries a per-dependencyprevVersion, and closed afail-open case that came with it:
update_metadata.ts:101falls back to an empty prevVersion forindex > 0, and an unknown previous version must deny rather than skip on a gate whose contract is to
fail closed.
Verified by executing the step against a stubbed advisories endpoint: each dep queried at its own
version; no-advisory, empty-prevVersion, malformed-metadata and version-track all deny. The old loop
was run against the same input first, to confirm the test can see the defect.
A zizmor bump never ran zizmor
zizmor's pinned version arrives from
ci/locks/ci-scanners.lock(installed atzizmor.yml:53) —outside the
.github/**paths filter. PR #66 bumps zizmor 1.5.2 → 1.28.0 and ran 33 check contexts,not one of them zizmor, so a 23-minor jump against a deliberately clean baseline would first have been
adjudicated by the 06:00 cron, against main, after merge.
The coord_lock test measured the scheduler
The lock is correct. The barrier releases the cohort together, but each process then runs
Enter-CoordLock's own prologue — agit rev-parsespawn atlock.ps1:47— before its deadline isset at
lock.ps1:56, and nothing synchronizes that. A winner count tolerates onlyhold - timeout= 500 ms of that skew, against a measured 645–2134 ms. A straggler arriving after thewinner released acquires legitimately and is counted as a violation; hold intervals in five
multi-winner rounds were measured strictly disjoint.
Not a flake. Under 40 CPU burners on a 20-core box:
It now asserts occupancy — a
CreateNewsentinel taken under the lock, the same atomic test-and-setthe lock itself uses, with no reference to a clock. Cost:
hold_ms1500 → 8000 for a ~3.3x margin overthe measured skew, so the test's floor wall-time is ~9 s.
Also
pip install -elegs (benchmark ×3, selfhosted-win2025-sql) had no--constraint, sothey would have adopted the same breaking versions.
freethread-smokestays unconstrained onpurpose and now says why — pinning GIL-build versions there can send pip to a source build and the
canary stops measuring free-threading.
[dependency-groups](PR build(deps): bump the python-deps group across 1 directory with 5 updates #66 bumpedzizmorinsideci-scanners). Scoped to what the evidencesupports — at least
ci-scanners, routine track only.uv pip installCI no longer runs, and offered "once CI installs fromthe hashed lock" as an exit condition that would not retire the cap.
github-actions— the one ecosystem where auto-merge demonstrably fires,and where a compromised release runs inside CI with the job's token. SHA-pinning stops tag mutation,
not a malicious new release.
Verification
ruff format --check .,ruff check .clean. mypy is unchanged by this branch — it touches no fileunder
messagefoundry/. Full pytest running at time of writing;tests/test_coord_lock.py,tests/test_dependabot_automerge_guardrails.pyandtests/test_required_contexts.pypass, and everyrun:block in the edited workflows was bash-syntax-checked and actionlint-clean.No lock file is touched, so DEP-1 is unaffected.
CI-only, cannot be proven locally: the
ignore:taking effect (falsifiable on the next uv PR —annotated-types and ruff must be absent from the pyproject diff entirely, and
constraints.lockmust still read
annotated-types==0.7.0/ruff==0.15.22); the GHSA loop, which only runs on aDependabot-authored PR; zizmor 1.28.0 against the clean baseline.
Follow-up for the owner
PR #66 needs
@dependabot recreateafter this merges — Dependabot reads config from the defaultbranch, so the ignore rules must land first. Not a plain close, and not rebase-and-hand-revert (a human
push makes
dependabot-lock-resync.ymlskip; its gate requirestriggering_actor == 'dependabot[bot]').The recreated PR carries fastapi + prometheus-client + zizmor and needs a manual merge —
fastapiisdeny-listed.
Open questions left alone deliberately: the
hvacprose/spec mismatch (pyproject.toml:167-169documents a 2.x hold that
hvac>=2.3.0does not encode — the fix needsuv lock); pre-emptive ignoresfor pydicom/pynetdicom/webauthn (recommend skipping — no 4.x exists, and an ignore on
webauthnwouldsuppress its security track for no present benefit); and
ide-securityPRs hitting the GHSA loop withecosystem=piphardcoded, which is pre-existing and fails closed.No ADR or BACKLOG number was consumed.
🤖 Generated with Claude Code