feat(resolver): Stabilize min-publish-age - #17335
Conversation
|
r? @weihanglo rustbot has assigned @weihanglo. Use Why was this reviewer chosen?The reviewer was selected based on:
|
There was a problem hiding this comment.
Before diving into other aspects of the stabilization, have we got any feedback for this using in CI / production?
There was a problem hiding this comment.
As I did not see this answered, I can share my testing here.
I tested the min-publish-age feature as a system-wide default on Amazon Linux 2023 (x86_64 + arm64) VMs with rustc/cargo 1.100.0-nightly (2026-08-27).
Setup
- rustc 1.100.0-nightly (e457a7b0d 2026-08-27) / cargo 1.100.0-nightly (e8cb624 2026-08-22)
- System-wide /etc/cargo/config.toml with global-min-publish-age = "1 day"
- Validated across 16 test scenarios including a real-world project build (Firecracker)
Findings
cargo build --lockedproduces identical Cargo.lock with and without the cooldown. I diffed Cargo.lock byte-for-byte from Firecracker builds with vs without cooldown.- Large dependency graphs can be processed. An 192-crate workspace (tokio + reqwest + axum) resolves and builds with no issues when the 1-day delay is active. The cooldown actively held back packages: Cargo reported Locking 160 packages to highest Rust 1.100.0-nightly compatible versions as of 24 hours ago. We still obtained a working dependency set.
- Project-local override work.
.cargo/config.toml with global-min-publish-age = "0 days"overrides the system default. - Upgrade/update path works.
cargo update -p <crate>upgrades to the newest cooldown-compatible version.CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allowwas accepted on this nightly for bypassing the policy. - I find the diagnostic message is clear enough:
Locking 7 packages to highest Rust 1.100.0-nightly compatible versions as of 24 hours ago-- having a UTC timestamp would be better if it's in logs, but that's fine.
There was a problem hiding this comment.
This is exactly the kind of real world testing I was looking for. Thanks a lot for taking time to test and document it!
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
As identified at rust-lang#17335 (comment)
### What does this PR try to resolve? As identified at rust-lang#17335 (comment) ### How to test and review this PR?
# `[registry]` vs `[registries.<name>]` per-key semantics Review notes for [rust-lang#17335] (min-publish-age stabilization), comparing how each config key behaves across the two tables. Verified against PR head `7b009184` with repro tests in `tests/testsuite/registry_table_confusion.rs` and the PR's own `tests/testsuite/min_publish_age.rs`. [rust-lang#17335]: rust-lang#17335 (comment) | Key | `[registry]` | `[registries.crates-io]` | `[registries.<alt>]` | Evidence | |---|---|---|---|---| | `token` | works for crates.io; does **not** follow `registry.default` | **silently ignored** (config and `CARGO_REGISTRIES_CRATES_IO_TOKEN` env) | works | `registries_crates_io_token_ignored_for_crates_io`, `registry_token_does_not_follow_registry_default` | | `credential-provider` | works for crates.io | **silently ignored** | works | `registries_crates_io_credential_provider_ignored` | | `secret-key` / `secret-key-subject` | works for crates.io | silently ignored (same code path as `token`) | works | code: `src/util/auth/mod.rs:212` returns `[registry]` for crates.io before any name lookup | | `min-publish-age` (new) | crates.io only; ignored for alt registries even with `registry.default` pointing at them | **honored; overrides `registry.min-publish-age`** | honored | `registry_alt_ignores_min_publish_age`, `registries_crates_io_overrides_registry_default` | | `global-min-publish-age` (new) | fallback for **all** registries | silently ignored (not a `RegistryConfig` field) | silently ignored | code: `src/context/schema.rs:530` | | `protocol` | silently ignored (not a `GlobalRegistryConfig` field) | honored | parsed but unused (`_protocol`) | code: `SourceId::crates_io_is_sparse` reads `registries.crates-io.protocol` only | | `index` | hard error: "no longer supported" | silently ignored (`SourceId::alt_registry` short-circuits `crates-io`) | required; defines the registry | code: `check_registry_index_not_set`, `src/workspace/source_id.rs:296` | | `default` | selects target registry for `publish`/`login`/`owner`/`yank` when `--registry`/`--index` absent | silently ignored | n/a | code: `src/util/command_prelude.rs:919` | | `global-credential-providers` | fallback providers for **all** registries | silently ignored | silently ignored | code: `src/util/auth/mod.rs:57` | Key asymmetries: * `min-publish-age` is the first key where `[registries.crates-io]` both works and overrides its `[registry]` counterpart; for the auth keys, `[registries.crates-io]` is dead config. * Auth resolves crates.io **by URL first** (`is_crates_io()`, `src/util/auth/mod.rs:212`); min-publish-age resolves **by name first** (`alt_registry_key()`, `src/resolver/version_prefs.rs:279`). A named mirror of crates.io's index gets `[registry]` credentials but `registries.<mirror>.min-publish-age`. * `registries.<name>.global-min-publish-age` and `registry.protocol` are silent no-ops — the same "subtle `s` distinction" concern raised in [rust-lang#12334](rust-lang#12334 (comment)).
# `[registry]` vs `[registries.<name>]` per-key semantics Review notes for [rust-lang#17335] (min-publish-age stabilization), comparing how each config key behaves across the two tables. Verified against PR head `7b009184` with repro tests in `tests/testsuite/registry_table_confusion.rs` and the PR's own `tests/testsuite/min_publish_age.rs`. [rust-lang#17335]: rust-lang#17335 (comment) | Key | `[registry]` | `[registries.crates-io]` | `[registries.<alt>]` | Evidence | |---|---|---|---|---| | `token` | works for crates.io; does **not** follow `registry.default` | **silently ignored** (config and `CARGO_REGISTRIES_CRATES_IO_TOKEN` env) | works | `registries_crates_io_token_ignored_for_crates_io`, `registry_token_does_not_follow_registry_default` | | `credential-provider` | works for crates.io | **silently ignored** | works | `registries_crates_io_credential_provider_ignored` | | `secret-key` / `secret-key-subject` | works for crates.io | silently ignored (same code path as `token`) | works | code: `src/util/auth/mod.rs:212` returns `[registry]` for crates.io before any name lookup | | `min-publish-age` (new) | crates.io only; ignored for alt registries even with `registry.default` pointing at them | **honored; overrides `registry.min-publish-age`** | honored | `registry_alt_ignores_min_publish_age`, `registries_crates_io_overrides_registry_default` | | `global-min-publish-age` (new) | fallback for **all** registries | silently ignored (not a `RegistryConfig` field) | silently ignored | code: `src/context/schema.rs:530` | | `protocol` | silently ignored (not a `GlobalRegistryConfig` field) | honored | parsed but unused (`_protocol`) | code: `SourceId::crates_io_is_sparse` reads `registries.crates-io.protocol` only | | `index` | hard error: "no longer supported" | silently ignored (`SourceId::alt_registry` short-circuits `crates-io`) | required; defines the registry | code: `check_registry_index_not_set`, `src/workspace/source_id.rs:296` | | `default` | selects target registry for `publish`/`login`/`owner`/`yank` when `--registry`/`--index` absent | silently ignored | n/a | code: `src/util/command_prelude.rs:919` | | `global-credential-providers` | fallback providers for **all** registries | silently ignored | silently ignored | code: `src/util/auth/mod.rs:57` | Key asymmetries: * `min-publish-age` is the first key where `[registries.crates-io]` both works and overrides its `[registry]` counterpart; for the auth keys, `[registries.crates-io]` is dead config. * Auth resolves crates.io **by URL first** (`is_crates_io()`, `src/util/auth/mod.rs:212`); min-publish-age resolves **by name first** (`alt_registry_key()`, `src/resolver/version_prefs.rs:279`). A named mirror of crates.io's index gets `[registry]` credentials but `registries.<mirror>.min-publish-age`. * `registries.<name>.global-min-publish-age` and `registry.protocol` are silent no-ops — the same "subtle `s` distinction" concern raised in [rust-lang#12334](rust-lang#12334 (comment)).
This comment has been minimized.
This comment has been minimized.
|
@rfcbot fcp merge T-cargo This stabilizes rust-lang/rfcs#3923 the min-publish-age. See the PR description for details. Drifts from the RFC:
|
|
@weihanglo has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
This comment has been minimized.
This comment has been minimized.
Update cargo submodule 26 commits in e8cb624d5701824f46a2ec5873cfd59ee3d2f66c..b2e9d5f9db3fb1c454ab84f10c16508984a266e2 2026-08-22 00:23:45 +0000 to 2026-09-02 14:49:16 +0000 - fix(parser): Resolve theoretical use-after-free (rust-lang/cargo#17428) - fix(trim-paths)!: remove default scope from release profile (rust-lang/cargo#17424) - fix(git): Use git's 429 retry, when available (rust-lang/cargo#17422) - Avoid passing search path (-L) args when they are passed as --extern (rust-lang/cargo#17410) - chore(deps): update crate-ci/typos action to v1.50.0 (rust-lang/cargo#17417) - test: Move -Z onto its own line (rust-lang/cargo#17416) - chore(triagebot): enable `@rustbot merge/delegate` (rust-lang/cargo#17415) - Micro-optimize two package dir functions (rust-lang/cargo#17413) - perf: Do not build SBOM if user has not set build.sbom (rust-lang/cargo#17412) - feat(manifest)!: implement feature-metadata RFC3416 (rust-lang/cargo#15056) - Cargo profiling improvements (rust-lang/cargo#17411) - test(git): Remove gix override run in CI and the mode in code (rust-lang/cargo#17405) - perf(git): Reduce extra work when using git-cli (rust-lang/cargo#17406) - feat(resolver): Stabilize min-publish-age (rust-lang/cargo#17335) - fix(git): Remove ref status update when showing progress (rust-lang/cargo#17400) - revert: refactor: move sysroot lookup to GlobalContext (rust-lang/cargo#17401) - fix(run): Printing a new line to avoid overwriting error code after \r (rust-lang/cargo#17373) - fix(trim-paths): custom workspace-relative member paths remap (rust-lang/cargo#17366) - fix(home): rustdoc lint (rust-lang/cargo#17394) - feat(diag): Stabilize cargo-lints (rust-lang/cargo#17298) - chore(deps): Update partial_ref to v0.3.4 (rust-lang/cargo#17392) - refactor: remove ad-hoc `subslice_range` (rust-lang/cargo#17390) - docs(changelog): move build-dir new layout to Changed (rust-lang/cargo#17387) - chore(deps): update msrv (1 version) to v1.98 (rust-lang/cargo#17386) - docs: Use mdbook admonitions (rust-lang/cargo#17384) - chore(ci): exclude resolver-tests from intra doc link checks (rust-lang/cargo#17385)
Adds a seven-day publication cooldown for new Rust dependencies and gates compiling CI jobs on the shared dependency check. Also documents the nightly Cargo dependency workflow and uses locked resolution for normal Cargo commands. rust-lang/cargo#17335 Co-authored-by: Codex GPT-5.6 Sol <noreply@openai.com>
arrayref 0.3.10 was online for 86 minutes, internment 0.8.7 for 90 and append-only-vec 0.1.9 for 107. Every detection this tool has needs both the published artifact and the git source, which is right for artifact-versus-source divergence and blind to a shape nobody has catalogued yet, and DEP_INJECTED honestly suppresses itself under five conditions. A publish-age gate needs neither side. It asks how old the pinned version is, so any threshold at all would have sat all three of those compromises out. It detects nothing. It declines to go first. --min-publish-age "24 hours" reads every registry+ entry in the committed Cargo.lock, compares its crates.io created_at against the threshold, and flags each pin that is younger as PUBLISH_AGE at medium severity. The created_at comes from the version metadata the scanner already fetches, so there is no new endpoint and no extra request. The duration grammar is RFC 3923's, parsed exactly as cargo's own src/util/time_span.rs parses it, down to accepting singular units, rejecting a leading or trailing space, and a month being 2,629,746 seconds. One policy string serves both cargo's resolver and this audit. The gate is decided from registry metadata alone, before any artifact download or git resolution, so it still fires when the git side cannot be resolved. That is the whole point, and it is the case tatara-kube@0.2.595 hits live: NO_GIT_TAG with the flag absent, PUBLISH_AGE with it set. Cargo has this feature from 1.100 (rust-lang/cargo#17335). What cargo gates is resolution: its own rule ignores young versions "unless they already exist in the Cargo.lock file", and "once the versions are recorded in Cargo.lock, subsequent resolves will keep them". A young version that is already pinned, or that was forced through with CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow, is invisible to the resolver from then on. That committed lockfile is what this reads. --write-cargo-config writes cargo's half into .cargo/config.toml line by line, keeping every other key, comment, blank line and the EOL style, in whichever of the two spellings cargo accepts the file already uses, and reads the installed cargo rather than asserting anything about it. Edge cases, each handled rather than guessed: no created_at is unchecked and never blocked, since mirrors and alternate registries legitimately have none and the RFC exempts them; a version exactly at the threshold has cleared it; git and path dependencies never reach the gate; a packument that 404s stays VERSION_REMOVED's finding; an unparseable duration exits 2 naming the units. A crate that trips PUBLISH_AGE and a high flag is reported as one entry, not two, and its SARIF result is raised to error. With the flag absent nothing changes. Verified by capturing the whole test run on 1.4.0 and diffing it against this tree: zero pre-existing lines changed or removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arrayref 0.3.10 was online for 86 minutes, internment 0.8.7 for 90 and append-only-vec 0.1.9 for 107. Every detection this tool has needs both the published artifact and the git source, which is right for artifact-versus-source divergence and blind to a shape nobody has catalogued yet, and DEP_INJECTED honestly suppresses itself under five conditions. A publish-age gate needs neither side. It asks how old the pinned version is, so any threshold at all would have sat all three of those compromises out. It detects nothing. It declines to go first. --min-publish-age "24 hours" reads every registry+ entry in the committed Cargo.lock, compares its crates.io created_at against the threshold, and flags each pin that is younger as PUBLISH_AGE at medium severity. The created_at comes from the version metadata the scanner already fetches, so there is no new endpoint and no extra request. The duration grammar is RFC 3923's, parsed exactly as cargo's own src/util/time_span.rs parses it, down to accepting singular units, rejecting a leading or trailing space, and a month being 2,629,746 seconds. One policy string serves both cargo's resolver and this audit. The gate is decided from registry metadata alone, before any artifact download or git resolution, so it still fires when the git side cannot be resolved. That is the whole point, and it is the case tatara-kube@0.2.595 hits live: NO_GIT_TAG with the flag absent, PUBLISH_AGE with it set. Cargo has this feature from 1.100 (rust-lang/cargo#17335). What cargo gates is resolution: its own rule ignores young versions "unless they already exist in the Cargo.lock file", and "once the versions are recorded in Cargo.lock, subsequent resolves will keep them". A young version that is already pinned, or that was forced through with CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow, is invisible to the resolver from then on. That committed lockfile is what this reads. --write-cargo-config writes cargo's half into .cargo/config.toml line by line, keeping every other key, comment, blank line and the EOL style, in whichever of the two spellings cargo accepts the file already uses, and reads the installed cargo rather than asserting anything about it. Edge cases, each handled rather than guessed: no created_at is unchecked and never blocked, since mirrors and alternate registries legitimately have none and the RFC exempts them; a version exactly at the threshold has cleared it; git and path dependencies never reach the gate; a packument that 404s stays VERSION_REMOVED's finding; an unparseable duration exits 2 naming the units. A crate that trips PUBLISH_AGE and a high flag is reported as one entry, not two, and its SARIF result is raised to error. With the flag absent nothing changes. Verified by capturing the whole test run on 1.4.0 and diffing it against this tree: zero pre-existing lines changed or removed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds `.cargo/min-publish-age.toml` with `global-min-publish-age = "7 days"` so dependency resolution can skip freshly published crates: cargo +nightly --config .cargo/min-publish-age.toml update Kept out of `.cargo/config.toml` because the setting is unstable on stable Cargo, which ignores it and warns on every command. Move it into `config.toml` once Rust 1.100 stabilizes it (rust-lang/cargo#17335). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Y74MMTaRN8VcitemacLqG
View all comments
FCP
What does this PR try to resolve?
To ensure dependencies have had a chance to be scanned, a user can set:
To force a critical update through, a user can
$ CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo update -p fooThat will be preserved within the lockfile.
To ensure users can observe what is going on and address concerns,
allowor an unchange dep shown through-v)CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGEFixes #17009
How to test and review this PR?
Items from the tracking issue:
denyprecedence between this andincompatible-rust-version: we can always adjust this over timecargo installbehaviorresolveris defined as not affectingcargo installwhich this preserves, just likeincompatible-rust-versioncargo update --breaking: this feature is being stabilized first and the other is being considered for removal (fix(update)!: Remove unstable--breaking#17333)