diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..e0c7fd8 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,67 @@ +# 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. +# +# 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 + +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: ${{ 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 }}" \ + --generate-notes diff --git a/README.md b/README.md index de6224a..fe908bf 100644 --- a/README.md +++ b/README.md @@ -294,3 +294,27 @@ 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. + +**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.