From 291f541fa6a652d472c81750dc3f9a85f4d7f4b2 Mon Sep 17 00:00:00 2001 From: Byron Williams Date: Wed, 27 May 2026 21:30:46 -0700 Subject: [PATCH] fix(release): apply PSR v10.5.3 bug mitigations to reusable workflow PSR v10.5.3 has two bugs affecting the no-PyPI release pattern: 1. 422 on GitHub Release creation: github3 library serializes draft/prerelease as JSON strings rather than booleans. Fix: vcs_release: "false" + separate gh release create step that types booleans correctly. 2. Detached HEAD prevents branch matching: actions/checkout with ref: leaves HEAD detached; PSR needs an attached branch to match branches config. Fix: git checkout -B "$HEAD_BRANCH" after checkout. Also adds: - Fork verification step for workflow_run callers (S7631) - SHA-pinned checkout (ref: workflow_run.head_sha || github.sha) - commit: "false" so PSR only pushes tags (branch rulesets exempt tag refs) - publish-to-pypi default changed from true to false Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/python-release.yml | 47 ++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 655e9d9..b61318d 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -66,7 +66,7 @@ on: description: 'Publish to PyPI after release' required: false type: boolean - default: true + default: false pypi-package-name: description: 'PyPI package name' required: false @@ -249,6 +249,17 @@ jobs: with: egress-policy: audit + - name: Verify trusted workflow_run source + if: github.event_name == 'workflow_run' + env: + HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} + REPO_OWNER: ${{ github.repository_owner }} + run: | + if [ "$HEAD_OWNER" != "$REPO_OWNER" ]; then + echo "::error::Refusing: triggering workflow originated from a fork (owner: $HEAD_OWNER)" + exit 1 + fi + - name: Validate skip-tests opt-out if: ${{ !inputs.run-tests }} env: @@ -266,6 +277,16 @@ jobs: with: fetch-depth: 0 token: ${{ github.token }} + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + + - name: Attach HEAD to branch + if: github.event_name == 'workflow_run' + env: + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + run: | + # Checking out a SHA leaves the repo in detached HEAD state. + # PSR needs an attached branch to match against branches config. + git checkout -B "$HEAD_BRANCH" - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -347,6 +368,15 @@ jobs: git_committer_name: "github-actions[bot]" git_committer_email: "github-actions[bot]@users.noreply.github.com" force_level: ${{ inputs.force-release }} + # Branch ruleset blocks direct commits; tag refs are exempt. Skipping + # the version-bump commit means PSR only pushes a tag, which succeeds + # without any bypass actor. + commit: "false" + # PSR v10.5.3 serializes draft/prerelease as JSON strings ("false") + # instead of booleans, which GitHub's Releases API rejects with 422. + # Release creation is handled by the step below via gh, which types + # booleans correctly. + vcs_release: "false" - name: Sign artifacts with Sigstore if: ${{ inputs.sign-artifacts && (steps.semantic-release.outputs.released == 'true' || !inputs.semantic-release) }} @@ -355,12 +385,17 @@ jobs: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true - - name: Upload to GitHub Release (Semantic) + - name: Create GitHub Release if: ${{ inputs.semantic-release && steps.semantic-release.outputs.released == 'true' }} - uses: python-semantic-release/publish-action@310a9983a0ae878b29f3aac778d7c77c1db27378 # v10.5.3 - with: - github_token: ${{ github.token }} - tag: ${{ steps.semantic-release.outputs.tag }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.semantic-release.outputs.tag }} + run: | + gh release create "$TAG" \ + --title "$TAG" \ + --generate-notes \ + --repo "$GITHUB_REPOSITORY" \ + dist/* # Manual Release Mode - name: Manual Release