Skip to content

chore(deps): a three-day cool-down on every version this repository picks - #400

Merged
JArmandoAnaya merged 1 commit into
mainfrom
chore/package-cooldown
Aug 7, 2026
Merged

chore(deps): a three-day cool-down on every version this repository picks#400
JArmandoAnaya merged 1 commit into
mainfrom
chore/package-cooldown

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

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, a uv add, the build backend the release wheel is built with, and
every 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-lockfile installs a locked version that violates a
5-year cool-down without a murmur, and uv sync --frozen audits 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_NEWER on a plain uv sync silently 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 the
pinned dependency set, with no failure anywhere. Hence the scope is the
resolving commands only, and CI's syncs became --locked, which refuses to
resolve at all.

3. uv has no rolling cool-down; pnpm does. --exclude-newer accepts RFC 3339
timestamps and dates only — 3 days ago, 3d and P3D are each rejected — and
there is no --minimum-release-age. pnpm's minimumReleaseAge is 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.yaml minimumReleaseAge: 4320 (+ an empty, documented minimumReleaseAgeExclude). Declarative, so every pnpm add/update on a laptop, in docker/app.Dockerfile — which copies this file — and in CI is covered with nothing to type
scripts/cooldown.sh new. Owns the number and computes the rolling cutoff for uv. cooldown.sh uv add …, plus --days/--cutoff so the gate and the docs read it rather than restate it
scripts/build_dist.sh runs uv build through the wrapper. The sharpest site in the repo: [build-system] requires = ["hatchling"] is not in uv.lock, so build backends resolve fresh from PyPI on every build — and a build backend is executed. Wrapping it here covers the wheel job, the 30-minute flow job and the PyPI publish workflow at once, since all three reach uv build only through this script
.github/workflows/ci.yml five uv syncuv sync --locked; the one genuine PyPI resolution (ultralytics, unversioned) goes through the wrapper
.github/dependabot.yml adds the docker and docker-compose ecosystems, which had no entry and therefore no cool-down
docker/compose.yaml minio/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 it
scripts/check.sh the two npx playwright test call sites → pnpm exec. Beyond the pnpm-only rule: npx fetches and runs a package that is not installed, which is a resolution no lockfile names and no cool-down covers
CONTRIBUTING.md, two skills, docs/releasing.md the rule, and — more importantly — when and how to override it
tests/scripts/cooldown.test.mjs new, 9 assertions

uv sync --locked also does a second job worth naming: it fails when uv.lock
has drifted from pyproject.toml, so an uncommitted lock is now a named red step
rather than a silent re-lock.

Node is pnpm only

git grep -nwE "npx|yarn" now returns only prose stating the rule. docs/releasing.md's
npm org create became pnpm login — with a note that creating the organisation
has no client-side equivalent in any package manager and is a website action.

Test plan

tests/scripts/cooldown.test.mjs holds the four spellings of "three days" to each
other, with scripts/cooldown.sh --days as the source, and asserts the structural
halves 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.sh alone, and adding a Dependabot ecosystem with no
cooldown block. The gate's own scan needed narrowing three times because it kept
reporting the prose that explains it — the annotator_boundary.test.mjs failure
mode, 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:

Stage Exit
uv sync --locked 0
ruff check + ruff format --check + mypy ×2 + lint-imports 0
pytest architecture / cli / examples / formats / jobs 0
pytest kernel / mcp 0
pytest server + test_versioning.py 0
pnpm -r build + pnpm -r lint + pnpm test (77 script tests) 0
generated: openapi drift, client drift, mcp tool drift, version sync 0
bash scripts/check.sh browserboth Playwright suites, through the new pnpm exec path 0
bash scripts/build_dist.sh — the real release build under the cool-down 0
VISIONSET_REQUIRE_WHEEL=1 pytest tests/packaging — that wheel installs and serves 0

pytest tests/packaging and pytest tests/scripts exit 5 (nothing collected) on a
laptop without the env var and for .mjs respectively; both are pre-existing and by
design. 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

  • Pinning the toolchain installers. astral-sh/setup-uv installs whatever uv is
    newest. 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 packageManager and cooled via the npm ecosystem.
  • apt (ffmpeg, in CI and docker/api.Dockerfile). Debian stable is a frozen,
    signed snapshot; three days over it would be theatre.
  • Digest-pinning the two Dockerfile base images. Stricter, and it would make every
    security patch a manual bump; docker/api.Dockerfile's header already argues for
    choosing the distribution rather than the digest.

Found, not fixed

…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 JArmandoAnaya added ci CI, tooling, repo automation tooling Developer tooling: scripts, checks, local workflow labels Aug 7, 2026
@JArmandoAnaya
JArmandoAnaya merged commit 7e5d735 into main Aug 7, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the chore/package-cooldown branch August 7, 2026 09:31
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI, tooling, repo automation tooling Developer tooling: scripts, checks, local workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cool-down: no package installs from versions younger than three days

1 participant