diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2802994..e64f0f5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,36 +23,58 @@ jobs: contents: write steps: - # Checkout the repository at the branch that triggered the workflow. - # Python Semantic Release will automatically convert shallow clones to - # full clones if needed. We reset to workflow sha to prevent accidentally - # releasing un-evaluated changes if branch was updated during workflow. - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + # Note: We checkout the repository at the branch that triggered the workflow. + # Python Semantic Release will automatically convert shallow clones to full clones + # if needed to ensure proper history evaluation. However, we forcefully reset the + # branch to the workflow sha because it is possible that the branch was updated + # while the workflow was running, which prevents accidentally releasing un-evaluated + # changes. + - name: Setup | Checkout Repository on Release Branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.ref_name }} - - name: Force branch to workflow sha - run: git reset --hard ${{ github.sha }} + - name: Setup | Force release branch to be at workflow sha + run: | + git reset --hard ${{ github.sha }} - - name: Run Semantic Release + - name: Action | Semantic Version Release id: release - uses: python-semantic-release/python-semantic-release@8f4d8c4e3e4fcd2b8df16444b148aa49920cc4d3 # v10.6.1 + uses: python-semantic-release/python-semantic-release@9a026e9303981c866c3425723009becb2437c757 # v10.6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} git_committer_name: "github-actions" git_committer_email: "actions@users.noreply.github.com" changelog: "false" + # Commit, tag, push and build, but don't create the GitHub release. + # We create it ourselves in the next step so that the distributions + # are attached before the release is published. See that step for why. + vcs_release: "false" - - name: Upload to GitHub Release Assets - uses: python-semantic-release/publish-action@98e6c872368aff4290ef8444db095e5bcd3fba6c # v10.6.1 + # This repo has immutable releases enabled, which freezes a release's + # assets the moment it is published, so assets cannot be attached + # afterwards. `gh release create` handles this by creating the release as + # a draft, uploading the assets, and only then publishing it: + # https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases + - name: Publish | Create GitHub Release with Assets if: steps.release.outputs.released == 'true' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ steps.release.outputs.tag }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Reuse the release notes python-semantic-release generated for us. + RELEASE_NOTES: ${{ steps.release.outputs.release_notes }} + TAG: ${{ steps.release.outputs.tag }} + run: | + # Output the release notes to a file + printf '%s' "$RELEASE_NOTES" > "$RUNNER_TEMP/release_notes.md" + # Create a draft release, upload the assets, then publish it. + gh release create "$TAG" \ + --verify-tag \ + --title "$TAG" \ + --notes-file "$RUNNER_TEMP/release_notes.md" \ + dist/* - - name: Upload distribution artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v7.0.1 + - name: Upload | Distribution Artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: steps.release.outputs.released == 'true' with: name: distribution-artifacts @@ -64,19 +86,25 @@ jobs: version: ${{ steps.release.outputs.version }} publish_to_pypi: + # 1. Separate out the publish step from the github release step to run each step at + # the least amount of token privilege. + # 2. Also, publishing can fail, and its better to have a separate job if you need to + # retry, and it won't require reversing the release. runs-on: ubuntu-latest needs: release if: github.ref_name == 'master' && needs.release.outputs.released == 'true' permissions: + contents: read id-token: write steps: - - name: Download build artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v8.0.1 + - name: Setup | Download Build Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: distribution-artifacts path: dist + # Publishes to PyPI via OIDC trusted publishing (no API token required). - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.12.x + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2