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
68 changes: 40 additions & 28 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
# Auto-tags, cuts a GitHub Release, and publishes to PyPI whenever
# Auto-tags, cuts a GitHub Release, and dispatches a PyPI publish whenever
# pyproject.toml's version changes on main and the Python Test workflow has
# passed for that commit. No manual "cut a release" step needed.
#
# The publish job calls python-publish.yml directly via workflow_call rather
# than relying on the release: published event to cascade into it: GitHub
# Actions doesn't fire other workflows' triggers for events performed with
# the automatic GITHUB_TOKEN (anti-recursion safeguard), so a release created
# here wouldn't otherwise trigger it. Calling the job directly sidesteps that
# entirely -- no PAT/secret needed, since it's one run, not two.
# Two GitHub Actions quirks shaped this design -- both hit and confirmed the
# hard way, so read before "simplifying" this:
#
# 1. GitHub Actions doesn't fire other workflows' triggers (release:
# published included) for events performed with the automatic
# GITHUB_TOKEN, to prevent recursive runs. So creating the release here
# would NOT, on its own, trigger python-publish.yml's `release: published`
# listener. workflow_dispatch is the documented exception to that rule --
# an API-triggered workflow_dispatch DOES start a new run even when
# triggered by GITHUB_TOKEN -- so this dispatches python-publish.yml
# directly instead of relying on the release event to cascade.
#
# 2. The obvious alternative -- calling python-publish.yml's job directly via
# workflow_call instead of dispatching it as a separate run -- avoids (1)
# entirely, but breaks PyPI's OIDC trusted publishing: PyPI explicitly
# does not support trusted publishing from reusable/called workflows (the
# token's claims show a "reusable workflow" parent chain that PyPI's
# trusted-publisher matching rejects). So python-publish.yml must stay a
# plain, directly-triggered top-level workflow -- no workflow_call trigger
# on it, and nothing here should invoke it as `uses:`.

name: Auto Release

Expand All @@ -28,8 +42,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
released: ${{ steps.check.outputs.exists == 'false' }}
actions: write
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -41,31 +54,30 @@ jobs:
version=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/')
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Skip if this version was already released
- name: Skip if this version is already on PyPI
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"
version="${{ steps.version.outputs.version }}"
if curl -fsS https://pypi.org/pypi/pgdevkit/json | jq -e --arg v "$version" '.releases[$v] != null' >/dev/null; then
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "published=false" >> "$GITHUB_OUTPUT"
fi

- name: Create tag and GitHub release
if: steps.check.outputs.exists == 'false'
- name: Create tag and GitHub release, if missing
if: steps.check.outputs.published == '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
if ! gh release view "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
gh release create "v${{ steps.version.outputs.version }}" \
--title "v${{ steps.version.outputs.version }}" \
--target "${{ github.event.workflow_run.head_sha }}" \
--generate-notes
fi

publish:
needs: release
if: needs.release.outputs.released == 'true'
permissions:
id-token: write
contents: read
uses: ./.github/workflows/python-publish.yml
- name: Dispatch PyPI publish
if: steps.check.outputs.published == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run python-publish.yml --ref main
1 change: 0 additions & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ on:
release:
types: [published]
workflow_dispatch:
workflow_call:

jobs:
deploy:
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,15 +301,21 @@ 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`, cuts a GitHub Release (skipping if that version was already
released, e.g. a merge that didn't touch the version), and publishes to
PyPI via trusted (OIDC) publishing — no manual release step, and no extra
secret to configure. It calls `python-publish.yml`'s `deploy` job directly
(`workflow_call`) rather than relying on the release it just created to
trigger that workflow on its own — GitHub Actions doesn't fire other
workflows' triggers for events performed with the automatic `GITHUB_TOKEN`,
so a manually-created-via-Action release wouldn't otherwise cascade into a
publish; calling the job directly sidesteps that instead of working around
it with a PAT.
released, e.g. a merge that didn't touch the version), and dispatches
`python-publish.yml` to publish it to PyPI — no manual release step, and no
extra secret to configure. Two non-obvious GitHub Actions quirks shaped
this (see the comments at the top of `auto-release.yml` for the full
reasoning, since both were hit and confirmed the hard way):

- A release created with the default `GITHUB_TOKEN` does **not** trigger
other workflows' `release: published` listeners (an anti-recursion
safeguard) — `workflow_dispatch` is the documented exception, so
`auto-release.yml` dispatches `python-publish.yml` directly (`gh workflow
run`) instead of relying on the release to cascade into it.
- `python-publish.yml` deliberately stays a plain, directly-triggered
top-level workflow rather than something `auto-release.yml` calls via
`workflow_call`: PyPI's OIDC trusted publishing does not support
reusable/called workflows and silently rejects the token in that shape.

`workflow_dispatch` (or an actual GitHub UI release) on `python-publish.yml`
still works as a manual fallback if you ever need to re-publish a version
Expand Down
Loading