From ba476ccfe65bc69e9c9be67406558fba97887db0 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Mon, 7 Sep 2026 17:23:38 +0200 Subject: [PATCH 1/2] Auto-tag and release on merge to main Adds .github/workflows/auto-release.yml: after the Python Test workflow succeeds for a push to main, it reads the version from pyproject.toml and, if that version hasn't been released yet, creates a `vX.Y.Z` tag and GitHub Release for that commit. Publishing a Release is exactly what python-publish.yml already listens for, so merging a PR that bumps the version now publishes to PyPI with no separate manual "cut a release" step. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FPDh1XcpTJT48Tw27fD7Ms --- .github/workflows/auto-release.yml | 56 ++++++++++++++++++++++++++++++ README.md | 15 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/auto-release.yml diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..92c0110 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,56 @@ +# Auto-tags and cuts a GitHub Release whenever pyproject.toml's version changes +# on main and the Python Test workflow has passed for that commit. Publishing +# a Release (types: [published]) is exactly what python-publish.yml already +# listens for, so this is the only piece needed to make merging to main (with +# a version bump) publish to PyPI, with no separate manual release step. + +name: Auto Release + +on: + workflow_run: + workflows: ["Python Test"] + types: [completed] + +permissions: + contents: read + +jobs: + release: + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Read version from pyproject.toml + id: version + run: | + version=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/') + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Skip if this version was already released + id: check + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create tag and GitHub release + if: steps.check.outputs.exists == 'false' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "v${{ steps.version.outputs.version }}" \ + --title "v${{ steps.version.outputs.version }}" \ + --target "${{ github.event.workflow_run.head_sha }}" \ + --generate-notes diff --git a/README.md b/README.md index de6224a..e6404c7 100644 --- a/README.md +++ b/README.md @@ -294,3 +294,18 @@ async with pool.connection() as con: widget = await pg_retrieve(con, Widget, {"id": 1}) await pg_upsert(con, Widget(id=1, name="thing"), Widget) ``` + +## Releasing + +Bump `version` in `pyproject.toml` as part of your PR, same as any other +change. Once that PR merges to `main` and the `Python Test` workflow passes +for that commit, `.github/workflows/auto-release.yml` automatically tags it +`vX.Y.Z` and cuts a GitHub Release (skipping if that version was already +released, e.g. a merge that didn't touch the version) — no manual release +step needed. Publishing a Release is what `.github/workflows/python-publish.yml` +already listens for, so the new release then builds and publishes to PyPI +via trusted (OIDC) publishing automatically. + +`workflow_dispatch` on `python-publish.yml` still works as a manual +fallback if you ever need to re-publish a version without going through a +fresh tag/release. From 90746e1233116153c53de82b03a2128f1d6698d1 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Mon, 7 Sep 2026 17:25:23 +0200 Subject: [PATCH 2/2] Use a PAT to create the release, not the default GITHUB_TOKEN GitHub Actions doesn't fire other workflows' triggers (release: published included) for events performed with the automatic GITHUB_TOKEN, to prevent recursive workflow runs. Without this fix, auto-release.yml would successfully tag and "release" a version but python-publish.yml would silently never run, so nothing would actually reach PyPI. Switches the release-creation step to a RELEASE_TOKEN repo secret (a PAT), with a clear failure if it isn't set, and documents the one-time setup in the README. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FPDh1XcpTJT48Tw27fD7Ms --- .github/workflows/auto-release.yml | 13 ++++++++++++- README.md | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 92c0110..e0c7fd8 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -3,6 +3,13 @@ # a Release (types: [published]) is exactly what python-publish.yml already # listens for, so this is the only piece needed to make merging to main (with # a version bump) publish to PyPI, with no separate manual release step. +# +# The release-creation step deliberately uses the RELEASE_TOKEN secret (a +# PAT), not the default GITHUB_TOKEN: GitHub Actions does not fire other +# workflows' triggers (release: published included) for events performed +# with the automatic GITHUB_TOKEN, to prevent recursive runs. A release +# created with a PAT is attributed to a real actor instead, so it triggers +# python-publish.yml normally. See README "Releasing" for one-time setup. name: Auto Release @@ -48,8 +55,12 @@ jobs: - name: Create tag and GitHub release if: steps.check.outputs.exists == 'false' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | + if [ -z "$GH_TOKEN" ]; then + echo "::error::RELEASE_TOKEN repo secret is not set (needed so this release triggers python-publish.yml -- see README 'Releasing')." + exit 1 + fi gh release create "v${{ steps.version.outputs.version }}" \ --title "v${{ steps.version.outputs.version }}" \ --target "${{ github.event.workflow_run.head_sha }}" \ diff --git a/README.md b/README.md index e6404c7..fe908bf 100644 --- a/README.md +++ b/README.md @@ -309,3 +309,12 @@ via trusted (OIDC) publishing automatically. `workflow_dispatch` on `python-publish.yml` still works as a manual fallback if you ever need to re-publish a version without going through a fresh tag/release. + +**One-time setup:** `auto-release.yml` needs a `RELEASE_TOKEN` repository +secret — a PAT (classic, `repo` scope, or fine-grained with this repo's +Contents: Read and write) belonging to someone with write access. This is +required because GitHub Actions doesn't fire other workflows' triggers +(`release: published` included) for a release created with the automatic +`GITHUB_TOKEN`, to prevent recursive runs — a release created with a PAT is +attributed to a real actor instead, so it triggers `python-publish.yml` +normally. Add it under Settings → Secrets and variables → Actions.