chore(deps): a three-day cool-down on every version this repository picks - #400
Merged
Conversation
…icks A compromised release is most dangerous between publication and yanking. Dependabot already waited three days; nothing else did. This applies the same rule everywhere a version is *chosen*, and pins the paths where one is merely installed so nothing can choose behind it. - pnpm: minimumReleaseAge in pnpm-workspace.yaml — declarative, so every pnpm add/update on a laptop, in Docker and in CI is covered with no flag. - uv: scripts/cooldown.sh, because uv has no rolling setting (--exclude-newer takes absolute dates only). It owns the number and computes the cutoff. - scripts/build_dist.sh runs uv build through it: [build-system] requires is not in uv.lock and a build backend is executed, so it is the sharpest site. - CI's uv sync becomes uv sync --locked. A cutoff on a bare sync makes uv discard the lockfile and re-resolve, which would mean CI testing a set nobody chose; --locked also gates lockfile drift. - Dependabot gains the docker and docker-compose ecosystems, which had no entry and therefore no cool-down; docker/compose.yaml's one floating :latest tag is pinned, since no cool-down can cover a re-pointed tag. - Node is pnpm only: check.sh's two npx call sites become pnpm exec.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…icks (#400) A compromised release is most dangerous between publication and yanking. Dependabot already waited three days; nothing else did. This applies the same rule everywhere a version is *chosen*, and pins the paths where one is merely installed so nothing can choose behind it. - pnpm: minimumReleaseAge in pnpm-workspace.yaml — declarative, so every pnpm add/update on a laptop, in Docker and in CI is covered with no flag. - uv: scripts/cooldown.sh, because uv has no rolling setting (--exclude-newer takes absolute dates only). It owns the number and computes the cutoff. - scripts/build_dist.sh runs uv build through it: [build-system] requires is not in uv.lock and a build backend is executed, so it is the sharpest site. - CI's uv sync becomes uv sync --locked. A cutoff on a bare sync makes uv discard the lockfile and re-resolve, which would mean CI testing a set nobody chose; --locked also gates lockfile drift. - Dependabot gains the docker and docker-compose ecosystems, which had no entry and therefore no cool-down; docker/compose.yaml's one floating :latest tag is pinned, since no cool-down can cover a re-pointed tag. - Node is pnpm only: check.sh's two npx call sites become pnpm exec.
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.
Closes #399.
A compromised package is most dangerous in the hours between its publication and
its yanking. Dependabot already waited three days here; nothing else did — a
pnpm add, auv add, the build backend the release wheel is built with, andevery Docker base image picked versions with no waiting period at all. This
applies one rule, three days, everywhere a version is chosen, and pins the
paths where one is merely installed so nothing can choose behind its back.
The finding that shaped the design
Three things were measured against the pinned toolchains (uv 0.9.13, pnpm
10.30.2) rather than assumed, and the design is mostly a consequence of them.
1. A cool-down is a resolution-time control, and cannot be an install-time
one. Both install-from-lock paths ignore it, verified in both directions:
pnpm install --frozen-lockfileinstalls a locked version that violates a5-year cool-down without a murmur, and
uv sync --frozenaudits and moves on.That is correct — the lockfile is the reviewed artifact — so the rule goes where
versions are picked, not where they are installed.
2.
UV_EXCLUDE_NEWERon a plainuv syncsilently discards the lockfile.This is the trap the obvious implementation walks into. Setting it globally in
CI, which is how one would naturally "apply a cool-down everywhere", makes uv
answer
Ignoring existing lockfile due to addition of timestamp cutoff,re-resolve, and rewrite
uv.lock— so CI would quietly stop testing thepinned dependency set, with no failure anywhere. Hence the scope is the
resolving commands only, and CI's syncs became
--locked, which refuses toresolve at all.
3. uv has no rolling cool-down; pnpm does.
--exclude-neweraccepts RFC 3339timestamps and dates only —
3 days ago,3dandP3Dare each rejected — andthere is no
--minimum-release-age. pnpm'sminimumReleaseAgeis native,rolling and declarative. That asymmetry is the whole reason Node needs one config
line and Python needs a small script.
What changed
pnpm-workspace.yamlminimumReleaseAge: 4320(+ an empty, documentedminimumReleaseAgeExclude). Declarative, so everypnpm add/updateon a laptop, indocker/app.Dockerfile— which copies this file — and in CI is covered with nothing to typescripts/cooldown.shcooldown.sh uv add …, plus--days/--cutoffso the gate and the docs read it rather than restate itscripts/build_dist.shuv buildthrough the wrapper. The sharpest site in the repo:[build-system] requires = ["hatchling"]is not inuv.lock, so build backends resolve fresh from PyPI on every build — and a build backend is executed. Wrapping it here covers thewheeljob, the30-minute flowjob and the PyPI publish workflow at once, since all three reachuv buildonly through this script.github/workflows/ci.ymluv sync→uv sync --locked; the one genuine PyPI resolution (ultralytics, unversioned) goes through the wrapper.github/dependabot.ymldockeranddocker-composeecosystems, which had no entry and therefore no cool-downdocker/compose.yamlminio/minio:latest→ a named release. A floating tag is the one thing a cool-down structurally cannot cover — there is no version to be three days old, and the publisher re-points itscripts/check.shnpx playwright testcall sites →pnpm exec. Beyond the pnpm-only rule:npxfetches and runs a package that is not installed, which is a resolution no lockfile names and no cool-down coversCONTRIBUTING.md, two skills,docs/releasing.mdtests/scripts/cooldown.test.mjsuv sync --lockedalso does a second job worth naming: it fails whenuv.lockhas drifted from
pyproject.toml, so an uncommitted lock is now a named red steprather than a silent re-lock.
Node is pnpm only
git grep -nwE "npx|yarn"now returns only prose stating the rule.docs/releasing.md'snpm org createbecamepnpm login— with a note that creating the organisationhas no client-side equivalent in any package manager and is a website action.
Test plan
tests/scripts/cooldown.test.mjsholds the four spellings of "three days" to eachother, with
scripts/cooldown.sh --daysas the source, and asserts the structuralhalves the number cannot express (install paths pinned, no floating tags, pnpm only,
the build backend covered).
Every assertion was mutation-verified — 13 mutations, each confirmed to turn a
named test red, each reverted by exact content restore with the tree asserted
clean afterwards. Two of them are the ones that would actually happen: moving
the number in
cooldown.shalone, and adding a Dependabot ecosystem with nocooldownblock. The gate's own scan needed narrowing three times because it keptreporting the prose that explains it — the
annotator_boundary.test.mjsfailuremode, hit from three angles; what survives is shape-based rather than word-based
and the comment says so.
Local gate, run in stages against the ~10-minute ceiling. Every exit code verbatim:
uv sync --lockedruff check+ruff format --check+mypy×2 +lint-importsarchitecture/cli/examples/formats/jobskernel/mcpserver+test_versioning.pypnpm -r build+pnpm -r lint+pnpm test(77 script tests)bash scripts/check.sh browser— both Playwright suites, through the newpnpm execpathbash scripts/build_dist.sh— the real release build under the cool-downVISIONSET_REQUIRE_WHEEL=1 pytest tests/packaging— that wheel installs and servespytest tests/packagingandpytest tests/scriptsexit 5 (nothing collected) on alaptop without the env var and for
.mjsrespectively; both are pre-existing and bydesign. The last two rows are the ones that matter for this change: the wheel CI
builds is now built with a cool-down applied to its backend, and it was built and
installed here to prove hatchling still resolves under a three-day-old cutoff.
Considered and declined
astral-sh/setup-uvinstalls whatever uv isnewest. Cooling that is a real question, but the fix is an exact uv pin no
Dependabot ecosystem tracks — precisely the "pin nobody remembers choosing" that
dependabot.yml's own header argues against. It is a distinct concern(bootstrapping the package manager, not cooling the packages) and wants its own
issue. pnpm is already pinned via
packageManagerand cooled via the npm ecosystem.ffmpeg, in CI anddocker/api.Dockerfile). Debian stable is a frozen,signed snapshot; three days over it would be theatre.
security patch a manual bump;
docker/api.Dockerfile's header already argues forchoosing the distribution rather than the digest.
Found, not fixed
IngestFailure.namefor a directory ingest is a full server path — long-standing,unrelated, already recorded in PR feat(mcp): real tools over the SDK — thirty-three of them, and an agent that can see (#35) #107's body.
tests/packagingandtests/scriptsboth exit 5 under a per-directory pytest run.Harmless, but it means a staged local gate cannot tell "opt-in skip" from "directory
vanished" without reading the log. cf. the staged-run guidance in
refactor-protocol.