Repository navigation
Add min dep age 7 days #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+252
−2
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Hold back registry crate versions younger than a week. | ||
| # A malicious or broken release is usually yanked within that window, before the resolver can pick it. Versions | ||
| # already in Cargo.lock are kept. For an urgent bump, such as a security fix, run | ||
| # `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo update -p <crate> --precise <version>`. | ||
| [registry] | ||
| global-min-publish-age = "7 days" |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # The cargo and mise dependency cooldowns must name the same age, and neither may be switched off. | ||
| # mise writes the age as `7d` and cargo as `7 days`, so the two match when their numbers are equal and their units | ||
| # start with the same letter. Cargo's `months` is not accepted: its `m` would pair with mise's minutes. | ||
| package cross | ||
|
|
||
| import data.paths | ||
|
|
||
| mise_age_pattern := `^([0-9]+)([smhdw])$` | ||
|
|
||
| cargo_age_pattern := `^([0-9]+) (second|minute|hour|day|week)s?$` | ||
|
|
||
| cargo_config := file.contents if { | ||
| some file in paths.files | ||
| file.path == ".cargo/config.toml" | ||
| } | ||
|
|
||
| mise_config := file.contents if { | ||
| some file in paths.files | ||
| file.path == ".mise/config.toml" | ||
| } | ||
|
|
||
| cargo_age := cargo_config.registry["global-min-publish-age"] | ||
|
|
||
| mise_age := mise_config.settings.minimum_release_age | ||
|
|
||
| # An age as [number, first letter of its unit], undefined when the value is not in the tool's accepted form. | ||
| age_parts(value, pattern) := [to_number(match[1]), substring(match[2], 0, 1)] if { | ||
| is_string(value) | ||
| some match in regex.find_all_string_submatch_n(pattern, value, 1) | ||
| } | ||
|
|
||
| cargo_parts := age_parts(cargo_age, cargo_age_pattern) | ||
|
|
||
| mise_parts := age_parts(mise_age, mise_age_pattern) | ||
|
|
||
| # Every cargo age that outranks the global one, as [where it is set, value]. | ||
| cargo_override contains ["[registry] min-publish-age", age] if { | ||
| age := cargo_config.registry["min-publish-age"] | ||
| } | ||
|
|
||
| cargo_override contains [where, age] if { | ||
| some name, registry in cargo_config.registries | ||
| age := registry["min-publish-age"] | ||
| where := $"[registries.{name}] min-publish-age" | ||
| } | ||
|
|
||
| matches_mise(age) if age_parts(age, cargo_age_pattern) == mise_parts | ||
|
|
||
| zero_age if { | ||
| some parts in [cargo_parts, mise_parts] | ||
| parts[0] == 0 | ||
| } | ||
|
|
||
| # Cargo's publish age lives in .cargo/config.toml, so the file must exist. | ||
| deny contains "publish age: .cargo/config.toml is missing; it sets cargo's [registry] global-min-publish-age" if { | ||
| not cargo_config | ||
| } | ||
|
|
||
| # .cargo/config.toml must set cargo's publish age. | ||
| deny contains "publish age: .cargo/config.toml sets no [registry] global-min-publish-age" if { | ||
| cargo_config | ||
| not cargo_age | ||
| } | ||
|
|
||
| # .mise/config.toml must set mise's release age. | ||
| deny contains "publish age: .mise/config.toml sets no [settings] minimum_release_age" if { | ||
| mise_config | ||
| not mise_age | ||
| } | ||
|
|
||
| # Cargo's publish age must be a number and a unit word, so it can be compared with mise's. | ||
| deny contains msg if { | ||
| cargo_age | ||
| not cargo_parts | ||
| msg := $"publish age: cargo global-min-publish-age \"{cargo_age}\" is not `<n> <seconds|minutes|hours|days|weeks>`" | ||
| } | ||
|
|
||
| # mise's release age must be a number and a unit letter, so it can be compared with cargo's. | ||
| deny contains msg if { | ||
| mise_age | ||
| not mise_parts | ||
| msg := $"publish age: mise minimum_release_age \"{mise_age}\" is not `<n><s|m|h|d|w>`" | ||
| } | ||
|
|
||
| # Neither age may be zero, which turns the cooldown off. | ||
| deny contains msg if { | ||
| zero_age | ||
| msg := $"publish age: cargo \"{cargo_age}\" / mise \"{mise_age}\" must not be zero, which turns the cooldown off" | ||
| } | ||
|
|
||
| # Cargo and mise must hold dependencies back by the same age. | ||
| deny contains msg if { | ||
| cargo_parts != mise_parts | ||
| msg := $"publish age: cargo global-min-publish-age \"{cargo_age}\" != mise minimum_release_age \"{mise_age}\"" | ||
| } | ||
|
|
||
| # A registry-specific cargo age replaces the global one for that registry, so it must also match mise's. | ||
| deny contains msg if { | ||
| some override in cargo_override | ||
| [where, age] := override | ||
| not matches_mise(age) | ||
| msg := $"publish age: cargo {where} \"{age}\" != mise minimum_release_age \"{mise_age}\"" | ||
| } | ||
|
|
||
| # The resolver override that lets too-new versions through is for one-off runs, never committed. | ||
| deny contains "publish age: .cargo/config.toml must not set [resolver] incompatible-publish-age = \"allow\"" if { | ||
| cargo_config.resolver["incompatible-publish-age"] == "allow" | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # Unit tests for the cargo/mise publish-age cross-check, run by `conftest verify`. | ||
| # | ||
| # The real task only ever sees two ages that already agree, which looks the same as a rule that never matches. | ||
| package cross_test | ||
|
|
||
| import data.cross | ||
|
|
||
| age_cargo_file(cargo) := {"path": ".cargo/config.toml", "contents": cargo} | ||
|
|
||
| age_mise_file(mise) := {"path": ".mise/config.toml", "contents": mise} | ||
|
|
||
| age_input(cargo_age, mise_age) := [ | ||
| age_cargo_file({"registry": {"global-min-publish-age": cargo_age}}), | ||
| age_mise_file({"settings": {"minimum_release_age": mise_age}}), | ||
| ] | ||
|
|
||
| age_reports(msgs, fragment) if { | ||
| some msg in msgs | ||
| startswith(msg, "publish age:") | ||
| contains(msg, fragment) | ||
| } | ||
|
|
||
| age_clean(msgs) if { | ||
| not any_age_report(msgs) | ||
| } | ||
|
|
||
| any_age_report(msgs) if { | ||
| some msg in msgs | ||
| startswith(msg, "publish age:") | ||
| } | ||
|
|
||
| test_age_days_matching_is_accepted if { | ||
| age_clean(cross.deny) with input as age_input("7 days", "7d") | ||
| } | ||
|
|
||
| test_age_singular_week_matching_is_accepted if { | ||
| age_clean(cross.deny) with input as age_input("1 week", "1w") | ||
| } | ||
|
|
||
| test_age_windows_paths_are_still_read if { | ||
| files := age_input("7 days", "7d") | ||
| cargo := object.union(files[0], {"path": `.cargo\config.toml`}) | ||
| windows := [cargo, object.union(files[1], {"path": `.mise\config.toml`})] | ||
| age_clean(cross.deny) with input as windows | ||
| } | ||
|
|
||
| test_age_missing_cargo_config_is_flagged if { | ||
| msgs := cross.deny with input as [age_mise_file({"settings": {"minimum_release_age": "7d"}})] | ||
| age_reports(msgs, ".cargo/config.toml is missing") | ||
| } | ||
|
|
||
| test_age_missing_cargo_key_is_flagged if { | ||
| files := [age_cargo_file({"registry": {}}), age_mise_file({"settings": {"minimum_release_age": "7d"}})] | ||
| msgs := cross.deny with input as files | ||
| age_reports(msgs, "sets no [registry] global-min-publish-age") | ||
| } | ||
|
|
||
| test_age_missing_mise_key_is_flagged if { | ||
| files := [age_cargo_file({"registry": {"global-min-publish-age": "7 days"}}), age_mise_file({"settings": {}})] | ||
| msgs := cross.deny with input as files | ||
| age_reports(msgs, "sets no [settings] minimum_release_age") | ||
| } | ||
|
|
||
| test_age_bare_zero_is_flagged if { | ||
| msgs := cross.deny with input as age_input("0", "0") | ||
| age_reports(msgs, "global-min-publish-age \"0\" is not") | ||
| age_reports(msgs, "minimum_release_age \"0\" is not") | ||
| } | ||
|
|
||
| test_age_zero_with_unit_is_flagged if { | ||
| msgs := cross.deny with input as age_input("0 days", "0d") | ||
| age_reports(msgs, "must not be zero") | ||
| } | ||
|
|
||
| test_age_months_is_flagged if { | ||
| msgs := cross.deny with input as age_input("7 months", "7m") | ||
| age_reports(msgs, "global-min-publish-age \"7 months\" is not") | ||
| } | ||
|
|
||
| test_age_unparsable_mise_value_is_flagged if { | ||
| msgs := cross.deny with input as age_input("7 days", "2026-01-01") | ||
| age_reports(msgs, "minimum_release_age \"2026-01-01\" is not") | ||
| } | ||
|
|
||
| test_age_different_numbers_are_flagged if { | ||
| msgs := cross.deny with input as age_input("5 days", "7d") | ||
| age_reports(msgs, "\"5 days\" != mise minimum_release_age \"7d\"") | ||
| } | ||
|
|
||
| test_age_different_units_are_flagged if { | ||
| msgs := cross.deny with input as age_input("7 weeks", "7d") | ||
| age_reports(msgs, "\"7 weeks\" != mise minimum_release_age \"7d\"") | ||
| } | ||
|
|
||
| age_with_registry(registry, registries) := [ | ||
| age_cargo_file({"registry": registry, "registries": registries}), | ||
| age_mise_file({"settings": {"minimum_release_age": "7d"}}), | ||
| ] | ||
|
|
||
| test_age_matching_registry_override_is_accepted if { | ||
| files := age_with_registry({"global-min-publish-age": "7 days"}, {"crates-io": {"min-publish-age": "7 days"}}) | ||
| age_clean(cross.deny) with input as files | ||
| } | ||
|
|
||
| test_age_zero_registry_override_is_flagged if { | ||
| files := age_with_registry({"global-min-publish-age": "7 days"}, {"crates-io": {"min-publish-age": "0"}}) | ||
| msgs := cross.deny with input as files | ||
| age_reports(msgs, "[registries.crates-io] min-publish-age \"0\" != mise") | ||
| } | ||
|
|
||
| test_age_shorter_crates_io_age_is_flagged if { | ||
| registry := {"global-min-publish-age": "7 days", "min-publish-age": "5 days"} | ||
| msgs := cross.deny with input as age_with_registry(registry, {}) | ||
| age_reports(msgs, "[registry] min-publish-age \"5 days\" != mise") | ||
| } | ||
|
|
||
| test_age_committed_allow_is_flagged if { | ||
| cargo := {"registry": {"global-min-publish-age": "7 days"}, "resolver": {"incompatible-publish-age": "allow"}} | ||
| files := [age_cargo_file(cargo), age_mise_file({"settings": {"minimum_release_age": "7d"}})] | ||
| msgs := cross.deny with input as files | ||
| age_reports(msgs, "incompatible-publish-age") | ||
| } |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check per-registry overrides before accepting the Cargo age.
With the valid seven-day settings, adding
[registries.crates-io] min-publish-age = "0"produces nopublish-age denial. Cargo gives that override precedence over
registry.global-min-publish-age, socrates.io dependencies have no cooldown despite the successful check. Cargo documents this precedence
for version 1.100 and later. (doc.rust-lang.org)
Validate configured per-registry ages against the required cooldown. Add a regression test with the
zero override.