ci: regenerate uv.lock on the release PR (#95)

Fixes the release PR failure at [run
33109222328](https://github.com/svnscha/mcp-windbg/actions/runs/33109222328/job/98647266760):

```
error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided.
```

## Not a false alarm

`uv.lock` records the **project`s own version**, so release-please
bumping `pyproject.toml` to 1.2.0 leaves the lockfile behind. This is
exactly the drift that stranded `uv.lock` at 1.0.1 through the entire
1.1.0 release - the check added in #93 is doing its job, it just
surfaced that nothing maintains the lockfile across a release.

## Why not an `extra-files` entry

That was my first instinct and it is the wrong tool. release-please`s
updaters **parse and re-serialise** the file they edit rather than
patching it in place - which is exactly why `plugin.json`s inline
`keywords` array reflowed to one entry per line in the current release
PR. Harmless for a small hand-written manifest. Applying it to a
900-line generated lockfile, with a tool that does not own the format,
is not a trade worth making, and the TOML updater would additionally
need a filter expression (`$.package[?(@.name=="mcp-windbg")].version`)
whose support release-please`s docs do not confirm - a naive
`$.package[*].version` would rewrite the version of all 67 packages.

## What this does instead

A `sync-lockfile` job runs `uv lock` against the open release PR and
commits the result only if it moved. uv owns the file, so uv regenerates
it.

The PR branch comes from release-please`s `pr` output. It is a JSON
string, so the expression short-circuits rather than handing `fromJson`
an empty value:

```yaml
pr_branch: ${{ steps.release.outputs.pr && fromJson(steps.release.outputs.pr).headBranchName }}
```

## Verified

The bump is a single field and needs no dependency re-resolution -
setting the `mcp-windbg` entry in `uv.lock` to 1.2.0 by hand makes the
check pass:

```
Resolved 67 packages in 1ms
exit: 0
```

I deliberately did **not** hand-fix `uv.lock` on the release branch, so
that merging this exercises the new job for real against #91 rather than
leaving it untested.
This commit is contained in:
Sven Scharmentke
2026-08-27 21:46:21 +02:00
committed by GitHub
parent 92f73c2494
commit 4cb7f753db
2 changed files with 48 additions and 1 deletions
+36
View File
@@ -75,6 +75,9 @@ jobs:
tag_name: ${{ steps.release.outputs.tag_name }}
# The release commit on develop. main is fast-forwarded onto exactly this.
sha: ${{ steps.release.outputs.sha }}
# Branch of the open release PR, empty when there is none. `pr` is a JSON
# string, so the && short-circuits rather than handing fromJson nothing.
pr_branch: ${{ steps.release.outputs.pr && fromJson(steps.release.outputs.pr).headBranchName }}
steps:
- uses: googleapis/release-please-action@v5
id: release
@@ -90,6 +93,39 @@ jobs:
# token here if you want the PR gated by the test matrix before merge.
# token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
# uv.lock records the project's own version, so every version bump leaves it a
# release behind and fails `uv lock --check`. release-please cannot fix that:
# its updaters re-serialise the file they edit, which is safe for a small hand
# written manifest and emphatically not for a generated 900-line lockfile. So
# uv regenerates it, on the release PR, before anyone merges it.
sync-lockfile:
needs: release-please
if: ${{ needs.release-please.outputs.pr_branch != '' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.release-please.outputs.pr_branch }}
- uses: astral-sh/setup-uv@v7
with:
version: "latest"
- name: Regenerate uv.lock and commit it if it moved
run: |
uv lock
if git diff --quiet -- uv.lock; then
echo "uv.lock is already in step with pyproject.toml."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore: sync uv.lock with the release version" -- uv.lock
git push
echo "uv.lock synced onto the release PR."
# release-please is configured with skip-changelog, so CHANGELOG.md stays
# hand-written - but it still creates the GitHub release with its own notes,
# generated from commit subjects. Those are thinner than the entry a human
+12 -1
View File
@@ -181,7 +181,18 @@ started by hand.
release type updates `pyproject.toml`'s `[project] version` natively. `server.json` is not
something it knows about, so it is handled by two `extra-files` JSONPath entries (`$.version` and
`$.packages[*].version`) - the top-level and per-package versions are separate fields, and one
entry carries one JSONPath.
entry carries one JSONPath. The plugin manifest, the marketplace entry and the `uvx` pin in the
plugin's `.mcp.json` are three more `extra-files` entries.
Expect an `extra-files` target to come back **re-serialised**, not patched in place: the updater
parses the file and writes it out again, which is why `plugin.json`'s inline `keywords` array
reflowed to one entry per line on the first release that touched it. Harmless for a small
hand-written manifest; the reason `uv.lock` is not an `extra-files` target.
`uv.lock` is instead regenerated by the `sync-lockfile` job, which runs `uv lock` on the release
PR and commits the result. It records the project's own version, so every bump leaves it a
release behind - which is what `uv lock --check` in the build now fails on. Left to `extra-files`
a 900-line generated lockfile would be re-serialised by a tool that does not own its format.
**Two setup requirements**, easy to lose track of: