Skip to content

feat: support uv.lock files in the Python strategy#2693

Open
jlopez wants to merge 4 commits into
googleapis:mainfrom
jlopez:feat/uv-lock-support
Open

feat: support uv.lock files in the Python strategy#2693
jlopez wants to merge 4 commits into
googleapis:mainfrom
jlopez:feat/uv-lock-support

Conversation

@jlopez

@jlopez jlopez commented Mar 2, 2026

Copy link
Copy Markdown

Summary

Closes #2561

The Python strategy now automatically updates the project's entry in uv.lock when cutting a release, without requiring any extra-files configuration.

What changed

  • New UvLock updater (src/updaters/python/uv-lock.ts) — mirrors the existing CargoLock pattern: parses the lockfile, finds the matching [[package]] entry by name, and uses replaceTomlValue() to update the version while preserving all formatting and comments.
  • Python strategy (src/strategies/python.ts) — adds uv.lock to buildUpdates() alongside the other Python files. Gated on projectName being known (same condition as __init__.py updates). createIfMissing: false makes it a safe no-op for projects that don't use uv.
  • Package name matching applies PEP 503 normalization (My_Packagemy-package) so names are compared case-insensitively with -, _, and . treated as equivalent.
  • Virtual workspace members (entries without a version field) are skipped with a specific warning distinguishing them from packages that are genuinely absent from the lockfile.

Test plan

  • test/updaters/uv-lock.ts — unit tests covering: version update with formatting preserved (snapshot), PEP 503 name normalization (case, underscores, dots), virtual package warning, not touching unrelated dependencies, empty lockfile, missing package
  • test/strategies/python.ts — asserts uv.lock appears in the update list for both the common-files and skip-changelog paths, in addition to when pyproject.toml is present
  • npm test — all passing, 0 failing

🤖 Generated with Claude Code

@jlopez jlopez requested review from a team as code owners March 2, 2026 18:06
@product-auto-label product-auto-label Bot added the size: l Pull request size is large. label Mar 2, 2026
@google-cla

google-cla Bot commented Mar 2, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

jlopez added 4 commits March 20, 2026 19:36
Automatically update the project's entry in uv.lock when releasing a
Python package, without requiring extra-files configuration.

Closes googleapis#2561
- Warn when target package is not found in uv.lock (silent no-op bug)
- Add comment explaining warn-not-throw vs CargoLock for empty lockfiles
- Add comment explaining double-parse is inherent to replaceTomlValue API
- Add test for uv.lock with no [[package]] entries (100% coverage)
- Add test for target package not found in lockfile
- Add tests for PEP 503 underscore/dot normalization
- Distinguish virtual packages (present but version-less) from absent
  packages: emit a dedicated warning instead of the misleading "not
  found" message (fixes false debugging trail in uv mono-repos)
- Rename test 'silently skips packages without a version field' to
  accurately reflect that a warning is now emitted; add logger spy to
  assert the warning text
- Assert uv.lock is included in updates for 'builds common files' and
  'omits changelog if skipChangelog=true' strategy tests, catching any
  regression that moves the uv.lock push inside the pyproject.toml branch
- Correct PEP 508 → PEP 503 in normalizePackageName docstring (PEP 503
  is the authoritative spec for canonical name normalisation)
- Document that replaceTomlValue may throw on a malformed lockfile
@jlopez jlopez force-pushed the feat/uv-lock-support branch from b7914df to 9b22cfb Compare March 21, 2026 01:37
igorlg added a commit to igorlg/cfn-handler that referenced this pull request May 22, 2026
…#21)

* chore: sync uv.lock with pyproject.toml v1.2.0

The cfn-handler self-version entry in uv.lock had drifted from
pyproject.toml: release-please bumped pyproject.toml to 1.2.0 in PR #16
but cannot run `uv lock` to update the corresponding entry in uv.lock.
This commit clears that drift in isolation, ahead of the substantive
fix that prevents it from recurring.

See follow-up commit on this branch for the release-please-config
change that automates this sync going forward.

* ci(release): sync uv.lock from release-please and flip CI to --locked

Configures release-please to update the cfn-handler self-version entry
in uv.lock alongside pyproject.toml on every release, eliminating the
post-merge drift that surfaced as a dirty working tree on every `git
pull` + `uv sync`. With drift fixed at the source, flips CI and the
.envrc dev shell back from `uv sync --frozen` to `uv sync --locked`,
which also catches a contributor editing pyproject.toml dependencies
without running `uv lock` (previously a known foot-gun documented as a
tradeoff).

The release-please-config.json change uses the workaround discovered in
googleapis/release-please#2455's comment thread:

    "extra-files": [
      {
        "type": "toml",
        "path": "uv.lock",
        "jsonpath": "$.package[?(@.name.value=='cfn-handler')].version"
      }
    ]

The `.value` accessor descends into release-please's TOML AST node
shape (string nodes are exposed as {value, kind} rather than bare
strings). Tracked upstream:
- googleapis/release-please#2561 (feature request to make this native)
- googleapis/release-please#2455 (bug behind the .value workaround)
- googleapis/release-please#2693 (proposed upstream fix)

Catch-up commit (preceding this one on the branch) cleared the existing
1.1.1 -> 1.2.0 drift in uv.lock so the very PR introducing --locked
doesn't fail its own CI.

OpenSpec change: openspec/changes/release-please-sync-uv-lock/
Updates the ci-infrastructure spec's lockfile-drift requirement.

Files touched:
- release-please-config.json: add extra-files block
- .github/workflows/ci.yml: --frozen -> --locked (x2); rewrite comment
- .github/workflows/examples-lint.yml: --frozen -> --locked
- .envrc: --frozen -> --locked; rewrite comment block
- .github/CONTRIBUTING.md: rewrite lockfile policy paragraph
- docs/CI.md: replace 'Lockfile drift and --frozen' subsection;
  add 'Status: resolved' note to the v1.0.0 postmortem

* chore(test): add local validator for release-please uv.lock updater

Adds tests/release-please/, a Node.js validator that loads release-
please's GenericToml updater locally and exercises it against the real
uv.lock + the jsonpath from release-please-config.json. Catches:

  1. Configured jsonpath stops matching the cfn-handler entry
     (someone removed/edited the extra-files block by mistake)
  2. release-please starts re-serialising uv.lock instead of doing
     a surgical byte-range edit (would cause whole-file rewrites
     in every release PR — see the misleading docstring on
     GenericToml that prompted this validator)
  3. Bare `@.name` jsonpath unexpectedly starts working, which
     would mean googleapis/release-please#2693 has landed and we
     can drop the .value workaround and simplify the config

Pinned to release-please 17.3.0 — the version bundled in the action
SHA pinned in release.yml. README documents the version-linkage
policy: bumping the action SHA also bumps this pin.

Layout:
  tests/release-please/
  \u251c\u2500\u2500 README.md                      purpose, usage, version-pin policy
  \u251c\u2500\u2500 package.json                   release-please pin (one dep)
  \u251c\u2500\u2500 package-lock.json              committed for repro
  \u2514\u2500\u2500 validate-uv-lock-updater.js   the actual checks (positive + negative)

node_modules/ is gitignored. pytest doesn't collect non-Python files.
ruff doesn't lint non-Python files. mypy/pyright/coverage scope is
src/ only. Adding this directory is fully isolated from existing
tooling.

Run manually:

  cd tests/release-please && npm install && node validate-uv-lock-updater.js

A `just` recipe wiring this in as a pre-push check is a follow-up.

* chore(just): wire release-please uv.lock validator into gha-pre-release

Adds a new step [3/7] to the gha-pre-release recipe that runs the
local Node validator added in the previous commit. The step is fast
(~5s including `npm ci --silent`) and gates on the same invariants
the validator script asserts:

  - configured jsonpath matches the cfn-handler self-version entry
  - release-please does NOT re-serialise uv.lock (surgical edit only)
  - bare `@.name` jsonpath still doesn't match (workaround necessary)

Step renumbering: existing [3a..4] became [4a..5]. Header comment
block updated to describe the new step. Adds a `_check-npm` recipe
helper following the existing pattern of `_check-act`/`_check-gh-token`/
`_check-docker`.

Also updates openspec/changes/release-please-sync-uv-lock/tasks.md:
ticks off everything done so far (catch-up commit, release-please
config, workflow flips, .envrc update, doc rewrites, validator
tooling, all local validation steps), removes the assumption that
sections 8-11 require human intervention, and adds explicit
section 6 covering the validator scope-creep so future readers see
why `tests/release-please/` exists in the archive.

* fix(test): scope release-please as devDependency; tick off PR-open tasks

The dependency-review-action on PR #21 failed because release-please's
transitive deps include packages with licenses outside the allowlist
(BlueOak-1.0.0, CC-BY-3.0). Those are unavoidable in the release-
please dep tree, but they don't matter here: this is a development-only
local validator, never installed at runtime.

The workflow's 'fail-on-scopes: runtime' setting correctly gates only
runtime deps; the bug was on my side — release-please was declared
under 'dependencies' (which dependency-review-action treats as
runtime) instead of 'devDependencies'. Moves the pin to
devDependencies; npm regenerates the lockfile with 'dev: true'
markers on every transitive entry. Same exact resolved tree, same
hashes; only metadata for dependency-review-action's scope filter.

Also updates README.md ('dependencies.release-please' →
'devDependencies.release-please' in the version-pin policy
walkthrough) and ticks off the section 8 tasks in the OpenSpec
change since the PR is now open.

* chore(openspec): tick off section 9; mark 10.2/11.x as deferred

CI is green on PR #21:
  - secure-workflows.yml      pass
  - ci.yml matrix (10 entries) pass — proves --locked works post-fix
  - lint + typecheck          pass
  - cfn-lint over examples    pass
  - review dependencies       pass (after the devDependencies fix)
  - analyze (python)          pass

10.2 (validate the extra-files behaviour against a real release-
please PR) and 11.x (archive the change) are deferred: they require
the next feat:/fix: merge to actually exercise release-please's
release-PR generation. This PR's commit prefix (ci:) does not
trigger a release.

Annotates each deferred task with the explicit blocker so a future
reader doesn't think they're outstanding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support uv.lock files out-of-the-box.

2 participants