From 8406e98cba2987df4e4b65857a0b813a8e94dc30 Mon Sep 17 00:00:00 2001 From: lukeocodes Date: Mon, 4 May 2026 23:38:58 +0100 Subject: [PATCH] ci: add publish-on-release workflow for release-please recovery Adds a workflow that fires on the release-published event so a human manually creating a GitHub release (e.g. when release-please aborts with 'untagged, merged release PRs outstanding') can drive the PyPI publish through CI without resorting to workflow_dispatch or local twine uploads. GITHUB_TOKEN-created releases (release-please's normal flow) do not trigger this workflow due to GitHub Actions' loop-prevention, so the existing release.yml publish job continues to handle the standard flow with no double-publish race. Intentionally no workflow_dispatch trigger. Manual deploys outside a real release-published event are how cli.deepgram.com ended up advertising 0.2.20 while PyPI was still on 0.2.19. Production publish must always anchor to a real release artifact. --- .github/workflows/publish-on-release.yml | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/publish-on-release.yml diff --git a/.github/workflows/publish-on-release.yml b/.github/workflows/publish-on-release.yml new file mode 100644 index 0000000..5556d5c --- /dev/null +++ b/.github/workflows/publish-on-release.yml @@ -0,0 +1,59 @@ +name: Publish to PyPI on release + +# Fires when a human publishes a GitHub release with `gh release create +# v0.X.Y` (or via the GitHub UI) to recover from a stuck release-please +# state. Typical trigger: release-please aborts with `untagged, merged +# release PRs outstanding` because a previous merged release PR didn't +# get tagged, and `autorelease: pending` is stuck on it. +# +# Releases created by GITHUB_TOKEN (release-please's normal flow) do NOT +# trigger this workflow because GitHub Actions intentionally blocks that +# loop, so the standard release-please path keeps using release.yml's +# existing publish job. This workflow ONLY fires on human-driven +# recovery releases, so there is no double-publish race. +# +# Intentionally NO workflow_dispatch trigger. Manual deploys outside a +# real release-published event have shipped landing pages and packages +# that pointed at unreleased versions. Production publish must always +# be anchored to a real GitHub release artifact. + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + # Only fire on root-package releases (v0.2.4, v1.0.0, …). + # Sub-package tags look like deepctl-cmd-listen-v0.0.3, which we skip; + # release-please's normal flow handles those via release.yml. + if: startsWith(github.event.release.tag_name, 'v') + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.11" + + - name: Install build dependencies + run: pip install build twine + + - name: Build all packages + run: make build + + - name: Verify built packages + run: make verify-packages + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: dist/ + skip-existing: true