Skip to content

Open a weekly PR that refreshes uv.lock - #2569

Merged
MaxGhenis merged 3 commits into
mainfrom
weekly-uv-lock
Oct 22, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
weekly-uv-lock

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2568

What and why

CI's test suite installs uv.lock exactly as committed (uv sync --frozen in .github/actions/test-environment), so a new dependency release reaches CI only when someone updates the lock. The lock was last refreshed on 10/11 (a6050ea). Today, uv lock --upgrade on main updates 26 packages, including policyengine-core 3.33.2 → 3.33.12.

This adds .github/workflows/weekly-uv-lock.yaml, modelled on policyengine-us's weekly-uv-lock.yaml (which has been disabled there since March). It runs uv lock --upgrade on main every Monday at 06:00 UTC, or on workflow_dispatch, and opens a PR with the result. The PR body lists every Updated/Added/Removed line uv prints. The PR carries a .changed fragment, so merging it makes a patch release, as policyengine-us's does. It also adds a "Dependencies" section to .github/CONTRIBUTING.md.

How it works

Two jobs, because resolving can run a dependency's build backend:

  • lock: checks out github.sha with persist-credentials: false, then runs uv lock --upgrade with no secrets, a read-only GITHUB_TOKEN and no uv cache. It uploads the new uv.lock and uv's summary lines as an artifact.
  • publish: starts on a fresh runner and mints the org GitHub App token first (APP_ID / APP_PRIVATE_KEY, as versioning.yaml uses), scoped to contents and pull requests. GitHub starts no workflows for a push made with GITHUB_TOKEN and holds the runs for a PR opened with it until someone approves them, so the App token is what lets the PR's CI start on its own. This job runs no dependency code. It treats the artifact as data: it copies uv.lock, and keeps only well-formed summary lines for the body. It makes every decision itself.

Both jobs run only in PolicyEngine/policyengine-uk and only on main.

At most one update PR (branch bot/weekly-uv-lock-update). This departs from policyengine-us, which force-pushes the branch every week and skips creating a PR if one exists. Here:

  • Refresh: the open PR is rebuilt from main with the newest lock while it is the workflow's own. That means every commit except merges of main is authored by the bot, and the PR changes only uv.lock and the fragment. So a red PR turns green by itself once upstream fixes its release, and "Update branch" doesn't stop the refresh.
  • Hands off: once anyone else commits to it (say, a fix for a test a new release broke), the workflow leaves it alone.
  • No-op: if the PR already has this exact lock, nothing is pushed, so its CI doesn't rerun.
  • Close: if main's lock is already current, the PR is closed with a comment. The branch is deleted by a git push with --force-with-lease on the head that was checked, so a push that lands in between keeps it.
  • The push uses --force-with-lease on the head it inspected. Fork PRs that reuse the branch name are ignored.

PR writes are REST calls (gh api). gh pr edit also reads project fields, which a token scoped to contents and pull requests can't read. GitHub's "Permissions required for GitHub Apps" page lists the endpoints used under Pull requests (write) and Contents (write).

Behaviour that must hold (tested below)

  • At most one open update PR from this repository's branch.
  • Never overwrites or deletes a commit someone else made on an open update PR: the ownership check, plus the lease on the inspected head for both the push and the branch delete. (Without an open PR, a branch left over from a merged or closed PR is replaced, by design.)
  • Touches only its own PR and branch. The artifact can't steer the PR number, branch, base or close target.
  • No secret is present in the job that runs dependency code.
  • A failed GitHub API read stops the run. It never reads as "nobody else committed".

Test plan

  • actionlint 1.7.12 (with shellcheck): clean.

  • A local harness runs both jobs' run: steps in order, against a bare git remote and a REST gh stub, with the artifact handed between fresh clones. It passes 69/69 checks over 19 scenarios:

    • create, rerun with nothing new, refresh after main moves;
    • someone else commits → skip; "Update branch" merge → still refreshes; a merge that changes another file → skip; a hand edit of the lock → skip;
    • main already current → close; a push before the close step, or at the moment of the branch delete → branch kept;
    • leftover branch from a merged PR → replaced; push race → lease refuses;
    • uv failure, API failures, and a failed PR create each fail the job; a failed label call only warns;
    • fork PR with the same branch name → ignored; tampered artifact summary lines → filtered.

    Four deliberate workflow mutations each turn at least one check red. The harness needs network access (it runs uv lock against PyPI) and isn't committed.

  • Review: an Opus reviewer requested changes in round 1 and again in round 2. Round 2's must-fix was that the App private key was used in the same job that had already run dependency code, which is why there are now two jobs. Round 3 approved; its should-fix (no uv cache in lock) and nits are applied. Round 4 (GPT-6.1 Sol, which ran the harness and reproduced the bug) requested changes: the close step checked the branch head and deleted the branch in two separate calls, so a push landing in between was deleted. That is fixed in the latest commit, with a harness case that injects the push at the moment of the delete.

  • Not testable before merge: the real App token, the push to github.com and the REST permissions. After merge, one workflow_dispatch run will open the first update PR. I expect about 26 updates, including policyengine-core 3.33.12, and that PR's CI is the full suite on them.

Interactions

axiom: n/a: CI workflow

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 22, 2026 06:52
CI's test suite installs uv.lock exactly as committed, so a new upstream
release reaches it only when the lock changes. This workflow runs
`uv lock --upgrade` on main every Monday and opens a PR with the result.

It runs as two jobs because resolving can run a dependency's build backend.
`lock` resolves with no secrets, a read-only GITHUB_TOKEN and no uv cache,
and passes the new uv.lock on as an artifact. `publish` starts on a fresh
runner, mints the org GitHub App token (scoped to contents and pull
requests) so the PR gets CI, runs no dependency code, and makes every
decision itself.

It keeps at most one update PR open. It refreshes that PR while every commit
on it, other than merges of main, is the workflow's own and it changes only
the lock and the fragment; it leaves the PR alone once anyone else commits,
and closes it when main's lock is already current.

Fixes #2568

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The close step compared the branch head and then deleted it over REST, so
a push landing between the two was deleted. A git push delete with
--force-with-lease on the inspected head does both in one step. Also say
that GITHUB_TOKEN PR runs wait for approval rather than not running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 22, 2026 14:08
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merging at 050d96d81 through the merge queue (merge commit).

  • CI: all 16 checks pass at this head.
  • Independent review: six rounds, verdicts kept outside the repo.
    • Rounds 1–2 (Opus) requested changes. Round 2 led to the two-job split.
    • Round 3 (Opus) approved.
    • Round 4 (GPT-6.1 Sol) found the close-step race; fixed in 416f458f1.
    • Round 5 (GPT-6.1 Sol) approved.
    • Round 6 (GPT-6.1 Sol) confirmed the last commit at this exact head.
  • Local harness: 69/69 over 19 scenarios; actionlint clean.

After merge I'll dispatch weekly-uv-lock.yaml once and watch it. That run tests the real App token, the push and the REST calls, and it should open the first update PR.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 4301e6a Oct 22, 2026
17 of 18 checks passed
@MaxGhenis
MaxGhenis deleted the weekly-uv-lock branch October 22, 2026 18:27
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.

Refresh uv.lock weekly so CI tests current dependency releases

1 participant