From 8791642847dc9fe8aac37b03339fcf53806296af Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 8 Oct 2026 14:24:59 -0400 Subject: [PATCH] ci: keep uv.lock in step with release versions and allow republishing a tag release-please bumped pyproject.toml to 0.1.0 but not the package's own version in uv.lock, so uv sync --locked failed and the v0.1.0 publish stopped before building. Bump uv.lock in the release PR through extra-files, refresh it on main, and add a workflow_dispatch path that publishes an existing tag. --- .github/workflows/release.yml | 29 ++++++++++++--- AGENTS.md | 4 +++ release-please-config.json | 67 +++++++++++++++++++++++++++++------ uv.lock | 2 +- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 545df56..dfc7dbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,9 @@ # repository. The project's trusted publisher on PyPI names this repository, this # workflow file and the pypi environment. # +# A release whose publish failed can be published again by hand: run this +# workflow with the release's tag. It publishes that tag and nothing else. +# # Release PRs are opened with the default GITHUB_TOKEN, so CI waits for approval # on them. They change only the version and the changelog, and the publish job # runs the full checks before it uploads. @@ -17,6 +20,11 @@ name: release on: push: branches: [main] + workflow_dispatch: + inputs: + tag: + description: An existing release tag to publish (vX.Y.Z) + required: true permissions: contents: read @@ -25,7 +33,7 @@ jobs: release-please: name: Release PR runs-on: ubuntu-latest - if: github.repository_owner == 'fells-code' + if: github.repository_owner == 'fells-code' && github.event_name == 'push' permissions: contents: write pull-requests: write @@ -43,7 +51,9 @@ jobs: publish: name: Publish to PyPI needs: release-please - if: needs.release-please.outputs.release_created == 'true' + if: >- + !cancelled() && github.repository_owner == 'fells-code' && + (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest environment: name: pypi @@ -54,14 +64,25 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.release-please.outputs.tag_name }} + ref: ${{ inputs.tag || needs.release-please.outputs.tag_name }} persist-credentials: false + - name: The tag is a release of this version + env: + TAG: ${{ inputs.tag || needs.release-please.outputs.tag_name }} + run: | + echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || { echo "not a release tag: $TAG"; exit 1; } + git rev-parse -q --verify "refs/tags/$TAG" > /dev/null || { echo "no such tag: $TAG"; exit 1; } + version=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2) + [ "v$version" = "$TAG" ] || { echo "pyproject.toml says $version, tag is $TAG"; exit 1; } - uses: astral-sh/setup-uv@v10.2.0 with: python-version: '3.13' - name: Checks + # Frozen: the locked dependencies exactly, without checking the project's + # own version in the lockfile, which a tag cut before release-please bumped + # uv.lock (v0.1.0) still has as the previous one. CI holds main to --locked. run: | - uv sync --locked + uv sync --frozen uv run ruff check . uv run mypy uv run pytest diff --git a/AGENTS.md b/AGENTS.md index c218f5d..fe132e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,5 +101,9 @@ bump the version, edit the changelog, or tag by hand: minor version while pre-1.0. `ci`, `chore`, `test`, `style` and `build` commits make no release. - Merging that PR tags `vX.Y.Z`, creates the GitHub release, then checks, builds and publishes the package to PyPI through trusted publishing (no token in the repository). +- The release PR also bumps the package's own version in `uv.lock` (`extra-files` in + `release-please-config.json`), or `uv sync --locked` fails on main after every release. +- If a publish fails after the release is tagged, fix the cause on main, then publish that tag by + hand: `gh workflow run release.yml -f tag=vX.Y.Z`. It checks the tag matches `pyproject.toml`. Pre-1.0: a breaking change is a minor bump, and 1.0 is a deliberate decision, not a side effect. diff --git a/release-please-config.json b/release-please-config.json index 3e6f378..ce418c2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -12,17 +12,62 @@ "pull-request-header": "## Release", "pull-request-footer": "Merging this PR tags the release, creates the GitHub release and publishes the package to PyPI.", "changelog-sections": [ - { "type": "feat", "section": "Added" }, - { "type": "fix", "section": "Fixed" }, - { "type": "perf", "section": "Performance" }, - { "type": "refactor", "section": "Changed" }, - { "type": "docs", "section": "Documentation" }, - { "type": "revert", "section": "Reverted" }, - { "type": "build", "section": "Build", "hidden": true }, - { "type": "chore", "section": "Chores", "hidden": true }, - { "type": "ci", "section": "CI", "hidden": true }, - { "type": "style", "section": "Style", "hidden": true }, - { "type": "test", "section": "Tests", "hidden": true } + { + "type": "feat", + "section": "Added" + }, + { + "type": "fix", + "section": "Fixed" + }, + { + "type": "perf", + "section": "Performance" + }, + { + "type": "refactor", + "section": "Changed" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "revert", + "section": "Reverted" + }, + { + "type": "build", + "section": "Build", + "hidden": true + }, + { + "type": "chore", + "section": "Chores", + "hidden": true + }, + { + "type": "ci", + "section": "CI", + "hidden": true + }, + { + "type": "style", + "section": "Style", + "hidden": true + }, + { + "type": "test", + "section": "Tests", + "hidden": true + } + ], + "extra-files": [ + { + "type": "toml", + "path": "uv.lock", + "jsonpath": "$.package[?(@.name.value=='seamless-auth')].version" + } ] } } diff --git a/uv.lock b/uv.lock index 9077a08..9516978 100644 --- a/uv.lock +++ b/uv.lock @@ -891,7 +891,7 @@ wheels = [ [[package]] name = "seamless-auth" -version = "0.0.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "httpx2" },