Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading