Skip to content

release-age: 7-day minimum release age for npm/pnpm/bun/uv/pip/cargo - #185

Merged
DJRHails merged 1 commit into
mainfrom
release-age
Oct 9, 2026
Merged

DJRHails merged 1 commit into
mainfrom
release-age

Conversation

@DJRHails

@DJRHails DJRHails commented Oct 9, 2026

Copy link
Copy Markdown
Owner

What

A new release-age module that makes npm, pnpm, bun, uv, pip and cargo refuse any package version published less than 7 days ago. It is the counterpart of sfw: sfw blocks versions already known to be malicious, the age gate covers the ones nobody has looked at yet.

tool setting needs applied by
npm min-release-age=7 (days) npm 11.10 setup.sh, npm config set --location=user
pnpm minimum-release-age=10080 (minutes) pnpm 10.16 setup.sh, pnpm config set -g
bun [install] minimumReleaseAge = 604800 (s) bun 1.3 symlink to ~/.bunfig.toml
uv exclude-newer = "7 days" uv 0.9.17 symlink to ~/.config/uv/uv.toml
pip [global] uploaded-prior-to = P7D pip 26.1 symlink to ~/.config/pip/pip.conf (+ ~/Library/Application Support/pip/ on macOS)
cargo [registry] global-min-publish-age = "7 days" cargo 1.100 symlink to ~/.cargo/config.toml
  • npm and pnpm are set in place rather than symlinked because their user config files also hold per-machine registry tokens. setup.sh reads the value back first, so it is idempotent, and it fails loudly with the fix on hosts whose npm (Node 22 bundles npm 10) or pnpm predate the key.
  • cargo's key is stabilised for 1.100 (feat(resolver): Stabilize min-publish-age rust-lang/cargo#17335); stable cargo ignores it silently today, so the file activates itself at the next rustup update.
  • Added to the --cli profile, the README module and dependency tables, and the lefthook / pre-commit suites.

Verification

  • tests/release-age.test.sh (new, 34 checks): setup.sh against stub npm/pnpm that record every call. Fresh host writes each key once in the user/global location, rerun writes nothing, old npm / old pnpm / missing npm fail with the fix named and still configure the other tool, version floor is inclusive and numeric, each symlinked file carries 7 days in its own unit, uv and cargo load their files.
  • tests/module-deps.test.sh, tests/link-symlinks.test.sh, scripts/check-crypt-patterns.sh: pass.
  • Each gate proven against the installed tools (npm 11.19, pnpm 10.32, bun 1.3.10, uv 0.11.8, pip 26.2.1, cargo 1.97) under a throwaway HOME, using daily-published packages: typescript@next resolved to a build a week older under npm, pnpm and bun, boto3 dropped from 1.43.110 to 1.43.107 under uv and pip, and each documented bypass restored the latest. cargo loads the file without a warning. pnpm 10 and pnpm 12 both accept the kebab-case key through config set -g.
  • setup.sh run twice against the real npm and pnpm under a throwaway HOME seeded with a fake auth token: the token line survived, the second run made no writes.

via claude

@DJRHails
DJRHails merged commit f7a6cc7 into main Oct 9, 2026
@DJRHails
DJRHails deleted the release-age branch October 9, 2026 16:25

@DJRHails DJRHails left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — findings posted inline. This PR is already merged, so the P1/P2 fixes go to a follow-up PR off main. P1/P2 threads resolve as their fixes land; P3s are left for the author.

via gantry

# `uv tool` and `uvx`; versions already in uv.lock are kept. A project uv.toml
# or [tool.uv] table overrides this file. Exempt a package with
# exclude-newer-package = { <name> = false }.
exclude-newer = "7 days"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — correctness / claims vs reality: the user-level exclude-newer does not leave existing locks alone. uv records it in uv.lock, so the "Lockfiles win" bullet in the README and the "versions already in uv.lock are kept" line here are wrong for uv, in both directions:

  • On a gated host, every lock made without the gate is out of date. I reproduced this on uv 0.12.23 with a zero-dependency project locked with UV_NO_CONFIG=1. With this file as UV_CONFIG_FILE, both uv lock --check and uv sync --locked fail with Resolving despite existing lockfile due to addition of global exclude newer … / error: The lockfile at uv.lock needs to be updated, but --locked was provided. Plain uv run / uv sync silently re-resolve and rewrite the lock.
  • A lock rewritten here breaks hosts without the gate. The re-lock adds [options] exclude-newer-span = "P7D". Run UV_NO_CONFIG=1 uv lock --check against that lock and it fails the same way. So CI runners, collaborators and fleet hosts that lack ~/.config/uv/uv.toml go red on --locked for any repo last locked on a gated machine. On shared-org repos this keeps rewriting other people's locks back and forth.
  • The relative span is at least stable: I moved the lock's timestamp two days back and it still passed --check, so there's no daily churn.

Fix pushed (docs only): the uv.toml comment and README now describe this, with the --frozen / per-project exclude-newer escape hatches. Whether to keep uv gated at user level is your call. The alternatives are dropping uv.toml from symlinks.conf, or gating per repo in [tool.uv] so the setting is committed alongside the lock. I didn't make that design change.

via gantry

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs corrected in 49e496b (#186): the README's "Lockfiles win" bullet and the uv.toml comment now describe the lock interaction and the --frozen / per-repo [tool.uv] options. I'm leaving this thread open because the behaviour itself, a user-level uv gate that invalidates existing locks and makes re-made locks fail --locked on ungated hosts, is a design decision for the author.

via gantry

Comment thread tests/release-age.test.sh
log::success "npm min-release-age=7"
else
npm config set min-release-age=7 --location=user
log::result $? "npm min-release-age=7 -> $(npm config get userconfig)"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — exit code: if npm config set (or pnpm config set -g at L48) fails, for example because ~/.npmrc isn't writable, log::result prints ✖ but release_age_failed stays 0. setup.sh then returns 0 and bootstrap logs setup completed ✔, even though the missing-npm and old-version paths deliberately return 1. A one-line || release_age_failed=1 on both log::result calls would make the exit code consistent. Left for the author.

via gantry

@DJRHails

DJRHails commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Review Summary

#185 was already merged when this review ran, so the fixes are in a follow-up PR, #186.

# Severity Finding Resolution
F1 P1 The user-level uv exclude-newer is written into uv.lock. Locks made without it fail --locked / lock --check on gated hosts, and locks re-made there fail --locked on ungated hosts. "Lockfiles win" is false for uv. Docs corrected in 49e496b (#186). The behaviour is left for the author: keep it at user level, or move to a per-repo [tool.uv] setting.
F2 P2 The test's no-npm case puts /usr/bin on PATH. It fails on hosts with a system npm, which breaks the pre-commit hook, and it can write the real ~/.npmrc because HOME isn't isolated. Fixed in 49e496b (#186)
F3 P3 A failed npm/pnpm config set is logged ✖, but setup.sh still exits 0. Left for the author

Verified: I reproduced F1 on uv 0.12.23 with a zero-dependency project: uv lock --check and uv sync --locked fail when this module's uv.toml is the config, and the re-made lock fails UV_NO_CONFIG=1 uv lock --check. The relative span does not churn from day to day. I reproduced F2 by running the original suite under a throwaway HOME: exit 1, FAIL no-npm-message against /usr/bin/npm 10.9.9. After the fix the suite gives 33 ok, 1 skip (no cargo on this host), and HOME is untouched. shellcheck and shfmt -d are clean, and every repo commit hook passed. pip and pnpm behaviour was not exercised: there's no pip here, and corepack pnpm can't download under this egress policy.
Fix commit: 49e496b (branch release-age-review-fixes, #186)
Reviewed head: 43cddd2 (merged as f7a6cc7). The fix is on the follow-up branch.

Verdict: request_changes

via gantry

DJRHails added a commit that referenced this pull request Oct 9, 2026
- F1 (P2, fixed): release-age.test.sh put /usr/bin on run_setup's PATH, so
  on a host with a system npm (NodeSource, Debian's npm) the real npm
  answered the no-npm case and the suite failed; it runs unconditionally
  as a pre-commit hook, so every commit on such a host was blocked. The
  sandbox now gets a filtered system bin dir without npm/npx/pnpm/pnpx/
  corepack. Origin: f7a6cc7 (#185).
- F2 (P3, left for the author): README presents npm config set -g as the
  way to edit the tracked npmrc, but npm rewrites it without its comments.
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