Skip to content

feat: scope uv.lock changes by dependency graph#12

Merged
vanandrew merged 1 commit into
mainfrom
feat/lock-scope
Jul 22, 2026
Merged

feat: scope uv.lock changes by dependency graph#12
vanandrew merged 1 commit into
mainfrom
feat/lock-scope

Conversation

@vanandrew

Copy link
Copy Markdown
Owner

Summary

  • New opt-in --lock-scope CLI flag (and lock-scope action input). Without it, a uv.lock change is a blanket test_all trigger exactly as before, so behavior is unchanged for existing users.
  • With it, uv.lock stops being a blanket trigger. difftrace diffs the lock against its base revision, reverse-walks each changed dependency to the workspace members that transitively use it, and unions those into the affected set. In a large monorepo most dependency bumps reach only a handful of packages, so the common bump stops fanning out to the whole matrix.
  • Conservative by design — it never under-builds. A change it can't map safely falls back to the full matrix: an unreadable base revision, a [manifest] change (members list or dependency-groups), a top-level requires-python / resolution-markers change, or a changed dependency that no member reaches. That last case is the tooling that lives only in the manifest dependency-groups (pytest, coverage, dvc, …) and runs in every package's build, so scoping it would under-build.

Design

  • graph.py: parse_full_lock is the full-package-graph mode — it skips the members-only filter so every locked package is a node, third-party packages included, and records a per-package fingerprint (version, source, artifact hashes) for the diff.
  • diff.py: read_lock_at_ref reads the base lock via git show <base>:<path>, returning None when the lock didn't exist at that ref so the caller falls back.
  • lockscope.py: diffs two locks at the [[package]] level (added / removed / fingerprint-changed), applies the manifest and top-level fall-back, and reverse-walks the changed packages to members over the union of the base and current graphs. "Reaches no member" is what distinguishes manifest-only tooling from a real member dependency.
  • cli.py: under --lock-scope, drops uv.lock from the trigger set and runs the per-workspace lock diff, unioning the result into the file-based affected set. Multi-lock stays workspace-scoped; a single-lock fall-back sets global test_all, mirroring today's root-trigger semantics. It respects --no-dev / --no-optional, and --direct-only omits lock-derived members since they are transitive.

Two approximations, both on the safe side

  • Edges are walked by name, ignoring environment markers, so a platform-specific dependency may rebuild a member it didn't truly reach — never the reverse.
  • PEP 517 build-backend requirements aren't in the lock's dependency arrays, but the tooling fall-back covers the ones that matter.

Test plan

  • New tests/test_lockscope.py (17 tests): missing / unparseable base, member-list / dependency-group / requires-python / resolution-marker fall-backs, resolution-marker reorder is not a change, tooling fall-back, scoped single / shared / added / removed dependency changes, hash-only change, member-fingerprint change, and a --no-dev edge.
  • parse_full_lock unit tests: third-party nodes present, edges unfiltered by membership, reverse edges reach third-party, fingerprint tracks version and wheel hash, --no-dev excludes dev edges.
  • read_lock_at_ref tests: returns content, missing object → None, nested path, timeout raises.
  • CLI run() integration under --lock-scope: scoped bump, shared-dep fan-out, tooling fall-back, manifest fall-back, no-op lock change rebuilds nothing, file + lock union, missing-base fall-back, --direct-only, and a regression guard that uv.lock still triggers test_all without the flag.
  • 231 tests pass; ruff check, ruff format --check, and pyright clean.
  • End-to-end against a real 347-package workspace lock: a single-member dependency bump scopes to that one member, a two-member dependency to two, a tooling bump falls back to the full matrix, and without the flag the same bump still triggers test_all.

Add an opt-in --lock-scope flag (lock-scope action input). Instead of
treating any uv.lock change as a blanket test_all trigger, diff the lock
against its base revision, reverse-walk each changed dependency to the
workspace members that use it, and fall back to test_all only for
manifest or tooling changes. Default behavior is unchanged when the flag
is off.
@vanandrew
vanandrew merged commit a2b3196 into main Jul 22, 2026
68 of 69 checks passed
@vanandrew
vanandrew deleted the feat/lock-scope branch July 22, 2026 18:06
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.12%. Comparing base (0b8d21b) to head (d58d5ee).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/difftrace/graph.py 89.47% 6 Missing ⚠️
src/difftrace/cli.py 85.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #12      +/-   ##
==========================================
- Coverage   98.16%   97.12%   -1.05%     
==========================================
  Files           6        7       +1     
  Lines         491      626     +135     
==========================================
+ Hits          482      608     +126     
- Misses          9       18       +9     

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

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