Skip to content

ci: enforce cargo lockfile age policy on PRs - #2549

Open
scottsand-db wants to merge 1 commit into
delta-io:mainfrom
scottsand-db:stack/upgrade_lockfile_script
Open

ci: enforce cargo lockfile age policy on PRs#2549
scottsand-db wants to merge 1 commit into
delta-io:mainfrom
scottsand-db:stack/upgrade_lockfile_script

Conversation

@scottsand-db

@scottsand-db scottsand-db commented May 12, 2026

Copy link
Copy Markdown
Collaborator

What changes are proposed in this pull request?

Adds a script + CI gate that enforces a "no newer than 7 days" supply-chain policy on every crates.io-sourced package in Cargo.lock. Gives malicious uploads a week to be detected and yanked before they can land via a dependency bump.

Main CUJs

scripts/upgrade_lockfile.py (Python 3.7+, no third-party deps):

  1. Audit (CI uses this): ./scripts/upgrade_lockfile.py --check
    Read-only; exits 1 on any package younger than --days N (default 7).
  2. Bulk upgrade: ./scripts/upgrade_lockfile.py
    cargo update everything, then walk back any package that came in too new.
  3. Upgrade ONE crate to newest compliant: ./scripts/upgrade_lockfile.py object_store
    Auto-picks newest version >= --days old, bumps it, walks back transitive fallout.
  4. Upgrade ONE crate to a specific version: ./scripts/upgrade_lockfile.py object_store --precise 0.13.2
    Fails fast if the target version is itself too new.

CI integration

A lockfile-age-policy job in build.yml runs --check --strict on every PR. dorny/paths-filter gates the actual work to PRs that touch Cargo.toml, Cargo.lock, any workspace Cargo.toml, or the script itself. --strict makes the run fail closed if crates.io is unreachable (escape hatch documented inline).

How was this change tested?

Each CUJ exercised manually. Some outputs below show the script's earlier name fix_lockfile.py (renamed mid-iteration; behavior is identical).

CUJ 1: --check on clean main (passes)
$ python3.11 ./scripts/upgrade_lockfile.py --check
upgrade_lockfile  |  cutoff: 2026-05-05 (7 days)

Mode: audit-only (read-only)

  Auditing 492 package version(s) across 429 crate(s).
  Fetching audit metadata (429 crate(s) from crates.io)...
    25/429
    50/429
    75/429
    100/429
    125/429
    150/429
    175/429
    200/429
    225/429
    250/429
    275/429
    300/429
    325/429
    350/429
    375/429
    400/429
    425/429
    429/429
Lockfile complies with the 7-day policy.
CUJ 1: --check --days 200 (220 violators)
$ python3.11 ./scripts/fix_lockfile.py --check --days 200
fix_lockfile  |  cutoff: 2025-10-24 (200 days)

Mode: audit-only (read-only)

  Auditing 492 package version(s) across 429 crate(s).
  Fetching audit metadata (429 crate(s) from crates.io)...
    25/429
    50/429
    ...
    429/429
220 package(s) violate the 200-day policy:

  aho-corasick 1.1.4 (published 195d ago, 2025-10-28)
  anstream 1.0.0 (published 90d ago, 2026-02-11)
  anstyle 1.0.14 (published 59d ago, 2026-03-13)
  anstyle-parse 1.0.0 (published 90d ago, 2026-02-11)
  anstyle-query 1.1.5 (published 180d ago, 2025-11-13)
  anstyle-wincon 3.0.11 (published 180d ago, 2025-11-13)
  anyhow 1.0.102 (published 81d ago, 2026-02-20)
  arrow 57.3.0 (published 95d ago, 2026-02-06)
  ... (212 more)
CUJ 2: bulk upgrade + walk-back (no args)
$ python3.11 ./scripts/fix_lockfile.py
fix_lockfile  |  cutoff: 2026-05-05 (7 days)

Mode: update all packages (then walk back any that overshoot the policy)

Step 1/2 - running cargo update...
  $ cargo update
Updating `databricks-proxy` index
     Locking 115 packages to latest Rust 1.88 compatible versions
    Removing anstream v0.6.21
    Removing anstyle-parse v0.2.7
    Updating arrow v58.1.0 -> v58.2.0
    ...
    Updating zerovec-derive v0.11.2 -> v0.11.3
note: pass `--verbose` to see 9 unchanged dependencies behind latest
  Downloaded 69 crates (18.3MiB) in 1.51s (largest was `aws-lc-sys` at 9.1MiB)

Step 2/2 - checking 113 change(s)...
  Fetching change metadata (111 crate(s) from crates.io)...
    25/111
    50/111
    75/111
    100/111
    111/111

  Package                                           Old             New  Released      Status
  --------------------------------------  --------------  --------------  ------------  ------
  arrow                                  57.3.0, 58.1.0          58.2.0  2026-05-02    ok
  arrow-arith                            57.3.0, 58.1.0          58.2.0  2026-05-02    ok
  ...
  zerovec-derive                                 0.11.2          0.11.3  2026-04-01    ok

  All changes are within the 7-day policy.

Done. Tip: commit both Cargo.toml and Cargo.lock.
CUJ 3: auto-pick newest compliant (object_store)

Lock already had the newest compliant version, so the bump was a no-op. Confirms the resolution + disambiguation path:

$ python3.11 ./scripts/fix_lockfile.py object_store
fix_lockfile  |  cutoff: 2026-05-05 (7 days)

Resolving newest compliant version of object_store...
  -> using object_store 0.13.2

Mode: update object_store to 0.13.2, fixing too-new transitives

Step 1/3 - validating object_store 0.13.2...
  object_store 0.13.2 released 2026-03-24 - within policy.
  Disambiguating: object_store -> object_store@0.13.2

Step 2/3 - running cargo update object_store@0.13.2 --precise 0.13.2...
  $ cargo update object_store@0.13.2 --precise 0.13.2
Updating `databricks-proxy` index
note: pass `--verbose` to see 113 unchanged dependencies behind latest

Step 3/3 - checking 0 change(s)...
  No packages changed.

Done. Tip: commit both Cargo.toml and Cargo.lock.
CUJ 4: explicit version (object_store --precise 0.13.2)
$ python3.11 ./scripts/fix_lockfile.py object_store --precise 0.13.2
fix_lockfile  |  cutoff: 2026-05-05 (7 days)

Mode: update object_store to 0.13.2, fixing too-new transitives

Step 1/3 - validating object_store 0.13.2...
  object_store 0.13.2 released 2026-03-24 - within policy.
  Disambiguating: object_store -> object_store@0.13.2

Step 2/3 - running cargo update object_store@0.13.2 --precise 0.13.2...
  $ cargo update object_store@0.13.2 --precise 0.13.2
Updating `databricks-proxy` index
note: pass `--verbose` to see 113 unchanged dependencies behind latest

Step 3/3 - checking 0 change(s)...
  No packages changed.

Done. Tip: commit both Cargo.toml and Cargo.lock.

@scottsand-db scottsand-db changed the title chore: add upgrade_lockfile.py for cargo dep age policy ci: enforce cargo lockfile age policy on PRs May 12, 2026
@scottsand-db
scottsand-db requested a review from nicklan May 12, 2026 19:06
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.65%. Comparing base (46a6d73) to head (b8a34c2).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2549      +/-   ##
==========================================
+ Coverage   88.61%   88.65%   +0.03%     
==========================================
  Files         179      179              
  Lines       60472    60600     +128     
  Branches    60472    60600     +128     
==========================================
+ Hits        53589    53726     +137     
+ Misses       4828     4816      -12     
- Partials     2055     2058       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds scripts/upgrade_lockfile.py (Python 3.7+, no third-party deps) and a
lockfile-age-policy job in build.yml that runs `--check --strict` on every
PR/push/merge_group. Gates the actual work via dorny/paths-filter so it
only runs when Cargo.toml, Cargo.lock, any workspace member Cargo.toml,
or the script itself changed.

The script supports four CUJs:
  - --check: read-only audit; exits 1 on any package younger than --days.
  - (no args): cargo update everything, walk back too-new fallout.
  - PKG: auto-pick newest compliant version of PKG, walk back transitives.
  - PKG --precise VER: bump PKG to VER, walk back transitives.

Uses `cargo metadata --locked` for lock parsing (no tomllib required) and
concurrently fetches publish dates from crates.io. --strict makes CI fail
closed if crates.io is unreachable.

All output and comments are ASCII-only to satisfy the repo's PR body
validator when contributors paste script output as evidence.
@scottsand-db
scottsand-db force-pushed the stack/upgrade_lockfile_script branch from 1b717fd to b8a34c2 Compare May 12, 2026 19:09
@scottsand-db
scottsand-db requested a review from thinh2 May 12, 2026 19:10

@thinh2 thinh2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Generally looks good for me ! Thank you! I have one question regarding the failure handling.

rc = 1

if rc == 0 and not dry_run:
print("\nDone. [OK] Tip: commit both Cargo.toml and Cargo.lock.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

when updating dependency/walk back dependency failed, the Cargo.toml and Cargo.lock will be in a bad state (e.g mixed of new version satisfied 7 days cutoff and less than 7 days cutoff). I think we need to revert the changes automatically or warn the user about it.

Furthermore, the cargo update does not run any build/test command. I think the suggestion for user when updating is Done is to run the build/test script verifying the new package works.

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.

2 participants