Skip to content

Relock uv.lock daily and propose the upgrades as a pull request - #3994

Draft
anth-volk wants to merge 4 commits into
api-tests-from-lockfrom
auto-relock-dependencies-workflow
Draft

anth-volk wants to merge 4 commits into
api-tests-from-lockfrom
auto-relock-dependencies-workflow

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3992

Summary

Adds .github/workflows/relock-dependencies.yml, which relocks uv.lock every day at 07:00 UTC (and on manual dispatch) and opens or updates a pull request with the result. Once CI installs from uv.lock instead of the newest PyPI versions, this pull request is how upgrades get tested and adopted.

  • Relock (.github/scripts/relock_dependencies.sh, helper locked_packages.py): upgrades every locked package to the newest version pyproject.toml allows by passing uv lock --upgrade-package for each one, except policyengine (the .py bundle) and the packages its locked extras pin (today policyengine-core, policyengine-us, policyengine-uk, spm-calculator). uv 0.12 has no option to exclude packages from --upgrade, so the script lists the packages to upgrade instead. Then it runs uv lock --check, and fails if a held package moved anyway. It resolves with Python 3.12 (--python 3.12; .python-version names 3.14). The resolution still covers every version requires-python = ">=3.11" allows.
  • No change: when uv.lock is byte-identical after the relock, the step reports changed=false and the pull-request step is skipped.
  • Pull request (.github/scripts/open_relock_pull_request.sh): commits uv.lock and changelog.d/relock-dependencies.changed.md (the changelog check needs a fragment) on auto/relock-dependencies, rebuilt from master on each run. It pushes with --force-with-lease on the commit it last saw, and skips the push when the branch already holds the same files. It reuses the oldest open issue titled "Relock dependencies" or opens one. If no relock PR is open, it opens one with Fixes #N as the first line, then the version changes (old → new) and the held packages. If one is open, it updates that PR's description instead of opening another.
  • Tokens: the workflow token is contents: read. The policyengine App's token checks out, pushes, and writes the issue and pull request, so pull_request CI runs on the result.
  • A manual run on a branch other than master relocks that branch and proposes it to that branch from auto/relock-dependencies-<branch>, so it doesn't touch the master relock PR.

How the bundle is pinned: pyproject.toml pins policyengine[models]==6.7.5, and policyengine 6.7.5's models extra pins policyengine-core==3.33.0, policyengine-us==2.2.1, policyengine-uk==2.130.1 and spm-calculator==1.0.0. With today's exact pins a plain uv lock --upgrade locks the same versions (checked locally; only the order of 12 resolution-marker entries differed). The exclusion keeps those packages held if a pin is ever loosened, because the "Ensure PolicyEngine bundle is supported by simulation API" check runs only when the policyengine[models] pin changes. policyengine-canada, policyengine-ng and policyengine-il aren't in the bundle. Their exact pins in pyproject.toml hold them.

The workflow edits no file the CI-from-lock work touches (pr.yml, push.yml, Makefile). The rules registry needs no entry: [discovery].workflows covers only pr.yml, and the ci-scripts guard (single-line run: steps, tested scripts, shellcheck) passes for the new workflow and scripts.

Testing

Red-green per commit (uv run --quiet --frozen --extra dev --python 3.12 pytest ...):

Commit Red Green
Add a relock script that holds the PolicyEngine bundle 9 failed: relock_dependencies.sh not found (exit 127), locked_packages.py not found (exit 2) 9 passed
Add a script that opens or updates the relock pull request 6 failed: open_relock_pull_request.sh not found (exit 127). The failure-path test was tightened to assert that the issue lookup ran, so it doesn't pass vacuously. Then a missing-summary test: 1 failed, because the script exited 0 without the summary 6 passed, then 7 passed
Relock uv.lock daily and propose the result as a pull request 6 errors: relock-dependencies.yml not found 6 passed
Ignore fork pull requests when finding the relock pull request 1 failed: the lookup requested only number, so a fork PR with the same head branch name would match 8 passed

The tests fake uv and gh. The pull-request tests push to a local bare repository, with no network access. One change has no test: the third commit fetches the old relock branch with --depth=1, so a shallow checkout doesn't download its history. The tests use a full local repository.

  • A real relock with uv 0.12.23 against master's lock upgraded 142 packages, held the five bundle packages at their versions, kept requires-python = ">=3.11", and passed uv lock --check. The lock wasn't committed here.
  • tests/unit: 2964 passed (run on the third commit). On the final commit the 23 relock tests pass; the fourth commit changes only the relock pull-request script and its test.
  • ruff format --check . and ruff check . (locked ruff), python scripts/run_quality_guards.py, python -m scripts.rules guard --base-ref origin/master, python -m scripts.rules render (no changes), uv lock --check, and shellcheck on both new scripts: all pass. No inline suppressions (noqa) added.
  • Self-review (not an independent review): one finding fixed in the fourth commit. gh pr list --head also matches fork branches with the same name, so the lookup now selects only same-repository pull requests. Two coverage gaps remain: the --depth=1 fetch and the summary format for a package locked at more than one version have no tests.
  • Not run live: workflow_dispatch returns 404 until the workflow file is on master, so the workflow hasn't run on GitHub yet.

🤖 Generated with Claude Code

anth-volk and others added 4 commits October 22, 2026 00:29
relock_dependencies.sh upgrades every package in uv.lock to the newest
version pyproject.toml allows, passing --upgrade-package for each locked
package except policyengine and the packages its extras pin (Core, the
country models and the SPM calculator), which change only through the
bundle update workflow. It resolves with Python 3.12 by default, runs
uv lock --check, fails if a held package moved anyway, and reports
changed=true or changed=false with a Markdown summary of the version
changes. locked_packages.py reads the lock for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
open_relock_pull_request.sh commits the relocked uv.lock with a
changelog fragment on the fixed relock branch, rebuilt from the base
branch on every run. It pushes with a lease on the commit it last saw,
and skips the push when the branch already holds the same files. It
reuses the oldest open "Relock dependencies" issue or opens one, then
opens the pull request with "Fixes #N" as the first line of its
description and the version changes below it, or updates the
description of the open relock pull request instead of opening another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
relock-dependencies.yml runs at 07:00 UTC every day and on manual
dispatch. It checks out the branch with the policyengine App's token,
relocks with Python 3.12 and the uv version push.yml locks releases
with, and runs open_relock_pull_request.sh only when uv.lock changed,
so pull_request CI runs on the result. A run on master proposes the
relock from auto/relock-dependencies; a manual run on another branch
proposes it to that branch from a branch of its own. The workflow token
is read-only.

The relock branch is now fetched with --depth=1, so a shallow checkout
does not download the history behind an older relock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gh pr list --head matches the head branch name in forks too, so a fork
pull request from a branch named auto/relock-dependencies would have
been treated as the relock pull request: its description edited, and no
pull request opened for the pushed branch. The lookup now selects only
pull requests whose head is in this repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.24%. Comparing base (3b12e1c) to head (a203765).

Additional details and impacted files
@@                 Coverage Diff                  @@
##           api-tests-from-lock    #3994   +/-   ##
====================================================
  Coverage                49.24%   49.24%           
====================================================
  Files                      243      243           
  Lines                    14398    14398           
  Branches                  2343     2343           
====================================================
  Hits                      7090     7090           
  Misses                    6682     6682           
  Partials                   626      626           

☔ View full report in Codecov by Harness.
📢 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.

This branch was successfully deployed

1 active deployment
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.

Relock uv.lock daily and propose the upgrades as a pull request

1 participant