Skip to content

feat(resolver): Stabilize min-publish-age - #17335

Merged
weihanglo merged 1 commit into
rust-lang:masterfrom
epage:min-publish-age
Aug 28, 2026
Merged

feat(resolver): Stabilize min-publish-age#17335
weihanglo merged 1 commit into
rust-lang:masterfrom
epage:min-publish-age

Conversation

@epage

@epage epage commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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:

[registry]
global-min-publish-age = "7 days"

To force a critical update through, a user can

$ CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo update -p foo

That will be preserved within the lockfile.

To ensure users can observe what is going on and address concerns,

  • Locking messages notify of:
    • That min-publish-age is in use and what the age is if there is a single one
    • a newer, unpicked version is available and its age
    • a version is being used that is incompatible with min-publish-age (either through allow or an unchange dep shown through -v)
  • Error messages notify of:
    • a newer, unpicked version is available and its age
    • a compatible version requirement to downgrade to
    • how to use CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE

Fixes #17009

How to test and review this PR?

Items from the tracking issue:

  • deny precedence between this and incompatible-rust-version: we can always adjust this over time
  • cargo install behavior
    • there was some confusion over what was being stabilized due to edits that happened during the FCP that weren't noticed
    • resolver is defined as not affecting cargo install which this preserves, just like incompatible-rust-version
  • cargo update --breaking: this feature is being stabilized first and the other is being considered for removal (fix(update)!: Remove unstable --breaking #17333)

@rustbot rustbot added A-dependency-resolution Area: dependency resolution and the resolver A-documenting-cargo-itself Area: Cargo's documentation A-unstable Area: nightly unstable support A-workspaces Area: workspaces S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 7, 2026
@rustbot

rustbot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

r? @weihanglo

rustbot has assigned @weihanglo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @epage, @weihanglo
  • @epage, @weihanglo expanded to epage, weihanglo

@weihanglo weihanglo Aug 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Before diving into other aspects of the stabilization, have we got any feedback for this using in CI / production?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not that I know of.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. cargo build --locked produces identical Cargo.lock with and without the cooldown. I diffed Cargo.lock byte-for-byte from Firecracker builds with vs without cooldown.
  2. 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.
  3. Project-local override work. .cargo/config.toml with global-min-publish-age = "0 days" overrides the system default.
  4. Upgrade/update path works. cargo update -p <crate> upgrades to the newest cooldown-compatible version. CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow was accepted on this nightly for bypassing the policy.
  5. 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is exactly the kind of real world testing I was looking for. Thanks a lot for taking time to test and document it!

Comment thread doc/book/src/reference/config.md Outdated
Comment thread doc/book/src/reference/config.md
@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

pull Bot pushed a commit to Mattlk13/cargo that referenced this pull request Aug 10, 2026
pull Bot pushed a commit to Mattlk13/cargo that referenced this pull request Aug 10, 2026
### What does this PR try to resolve?

As identified at
rust-lang#17335 (comment)

### How to test and review this PR?
weihanglo added a commit to weihanglo/cargo that referenced this pull request Aug 11, 2026
# `[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)).
weihanglo added a commit to weihanglo/cargo that referenced this pull request Aug 12, 2026
# `[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)).
@weihanglo weihanglo added the T-cargo Team: Cargo label Aug 12, 2026
@rustbot

This comment has been minimized.

@weihanglo

Copy link
Copy Markdown
Member

@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:

  • registry.min-publish-age is removed. There are confusion between [registry] and [registries.crates-io] tables and to avoid adding more things onto the confusions, we defer the field.
  • cargo install not respecting min-publish-age. There was a miscommunication in the RFC over what was being stabilized due to edits that happened during the FCP that weren't noticed. [resolver] is defined as not affecting cargo install which this preserves, just like incompatible-rust-version.

@rust-rfcbot

rust-rfcbot commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@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.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period An FCP proposal has started, but not yet signed off. disposition-merge FCP with intent to merge labels Aug 12, 2026
@rustbot

This comment has been minimized.

This was referenced Sep 2, 2026
rust-bors Bot pushed a commit to rust-lang/rust that referenced this pull request Sep 2, 2026
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)
@rustbot rustbot added this to the 1.100.0 milestone Sep 2, 2026
shikhar pushed a commit to s2-streamstore/tailsurf that referenced this pull request Sep 4, 2026
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>
Booyaka101 added a commit to Booyaka101/cargo-witness that referenced this pull request Sep 8, 2026
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>
Booyaka101 added a commit to Booyaka101/cargo-witness that referenced this pull request Sep 8, 2026
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>
nidor1998 added a commit to nidor1998/s7cmd that referenced this pull request Sep 11, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-dependency-resolution Area: dependency resolution and the resolver A-documenting-cargo-itself Area: Cargo's documentation A-unstable Area: nightly unstable support A-workspaces Area: workspaces disposition-merge FCP with intent to merge finished-final-comment-period FCP complete T-cargo Team: Cargo to-announce Z-min-publish-age Nightly: min-publish-age (RFC 3923)

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Tracking Issue for min-publish-age RFC 3923

7 participants