From 829034e6710138e102cc60683ff227bf482cf9a0 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Mon, 7 Sep 2026 18:04:50 +0200 Subject: [PATCH] Fix auto-release: dispatch publish instead of a reusable-workflow call The just-merged fix (calling python-publish.yml's deploy job directly via workflow_call) avoided needing a PAT, but broke PyPI's OIDC trusted publishing: PyPI does not support trusted publishing from reusable/called workflows and rejected the token, confirmed by gh-action-pypi-publish's own warning annotation on the run (https://docs.pypi.org/trusted-publishers/troubleshooting/#reusable-workflows-on-github). Fix: python-publish.yml goes back to being a plain, directly-triggered top-level workflow (workflow_call trigger removed). auto-release.yml instead dispatches it via `gh workflow run` (workflow_dispatch) using the default GITHUB_TOKEN -- workflow_dispatch is the documented exception to GitHub Actions' "GITHUB_TOKEN can't trigger other workflows" rule, so this needs no PAT either. Also: gate the "already done" check on whether the version is actually on PyPI (not just whether a GitHub Release exists for it) -- today's v0.4.0 already has a release from the earlier partial failure, but never reached PyPI, and the old exists-check would have skipped it forever. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FPDh1XcpTJT48Tw27fD7Ms --- .github/workflows/auto-release.yml | 68 ++++++++++++++++------------ .github/workflows/python-publish.yml | 1 - README.md | 24 ++++++---- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index f4ea0a2..d4e5c96 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -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 @@ -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: @@ -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 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8c35b0d..00325f5 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -12,7 +12,6 @@ on: release: types: [published] workflow_dispatch: - workflow_call: jobs: deploy: diff --git a/README.md b/README.md index 5ec238c..c72e2fe 100644 --- a/README.md +++ b/README.md @@ -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