From 5a8a12e9ce66902eafcda61abafe3ac39d3001a5 Mon Sep 17 00:00:00 2001 From: Chris Krough <461869+ckrough@users.noreply.github.com> Date: Mon, 11 May 2026 08:19:09 -0400 Subject: [PATCH] ci: replace release-PR workflow with canonical PSR push-to-main pattern Move from the sticky chore/release branch with hand-rolled tag-and-publish to the canonical python-semantic-release shape: one workflow on push to main that lets PSR own version, changelog, tag, GitHub Release, and asset upload in a single action. - Delete .github/workflows/release-pr.yml - Replace .github/workflows/release.yml with the PSR docs' canonical YAML (python-semantic-release@v10.5.3 + publish-action@v10.5.3, push: main trigger, concurrency group, contents: write only on the release job) - Add build_command = "uv build" so sdist and wheel attach to the GH Release via PSR's upload_to_vcs_release (default true) Source: https://python-semantic-release.readthedocs.io/en/stable/configuration/automatic-releases/github-actions.html --- .github/workflows/release-pr.yml | 111 ------------------------------- .github/workflows/release.yml | 85 +++++------------------ pyproject.toml | 1 + 3 files changed, 19 insertions(+), 178 deletions(-) delete mode 100644 .github/workflows/release-pr.yml diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml deleted file mode 100644 index f80ec4c..0000000 --- a/.github/workflows/release-pr.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Release PR - -# Rolling release-PR generator. Uses python-semantic-release to compute -# the next version, bumps pyproject.toml + CHANGELOG.md on a sticky -# "chore/release" branch, and opens or updates a PR labeled "release". -# Merging the labeled PR triggers release.yml, which tags and publishes. - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: {} - -concurrency: - group: release-pr - cancel-in-progress: false - -jobs: - release-pr: - name: Open or update release PR - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - env: - RELEASE_BRANCH: chore/release - PSR_VERSION: "10.5.3" - - steps: - - name: Checkout main - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: main - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: '3.13' - - - name: Install python-semantic-release - run: pip install "python-semantic-release==${PSR_VERSION}" - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Compute next version - id: psr - run: | - set +e - next=$(semantic-release version --print 2>/dev/null) - rc=$? - set -e - if [ $rc -ne 0 ] || [ -z "$next" ]; then - echo "No release-worthy commits since last tag; nothing to do." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - current=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/') - if [ "$next" = "$current" ]; then - echo "Next version equals current ($current); nothing to do." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "next=$next" >> "$GITHUB_OUTPUT" - - - name: Bump version and changelog - if: steps.psr.outputs.skip != 'true' - run: | - semantic-release version --no-push --no-tag --no-vcs-release --skip-build - - - name: Push release branch - if: steps.psr.outputs.skip != 'true' - env: - BRANCH: ${{ env.RELEASE_BRANCH }} - run: | - git branch -f "$BRANCH" - git push --force-with-lease origin "$BRANCH" - - - name: Write PR description - if: steps.psr.outputs.skip != 'true' - env: - NEXT: ${{ steps.psr.outputs.next }} - run: | - { - echo "Automated release pull request." - echo - echo "- Version: v${NEXT}" - echo "- Bumps pyproject.toml and CHANGELOG.md via python-semantic-release." - echo - echo "Merging this PR triggers release.yml, which creates the v${NEXT} tag and a GitHub Release. No PyPI publish step is configured." - } > pr_description.txt - - - name: Open or update release PR - if: steps.psr.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: ${{ env.RELEASE_BRANCH }} - NEXT: ${{ steps.psr.outputs.next }} - run: | - title="chore(release): v${NEXT}" - existing=$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[0].number') - if [ -n "$existing" ]; then - gh pr edit "$existing" --title "$title" --body-file pr_description.txt --add-label release - else - gh pr create --base main --head "$BRANCH" --title "$title" --body-file pr_description.txt --label release - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de67a24..0e2f59b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,86 +1,37 @@ name: Release -# Triggered when a release PR (label "release", opened by release-pr.yml) is -# merged into main. Tags the merge commit and publishes a GitHub Release. -# No PyPI publish step. - on: - pull_request: - types: [closed] + push: branches: [main] -permissions: {} +permissions: + contents: read concurrency: - group: release + group: ${{ github.workflow }}-release-${{ github.ref_name }} cancel-in-progress: false jobs: release: - name: Tag and publish GitHub Release runs-on: ubuntu-latest - if: >- - github.event.pull_request.merged == true && - contains(github.event.pull_request.labels.*.name, 'release') permissions: contents: write - steps: - - name: Checkout main - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v4 with: - ref: main - fetch-depth: 0 - - - name: Extract version from pyproject.toml - id: version - run: | - version=$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/') - if [ -z "$version" ]; then - echo "Could not extract version from pyproject.toml" >&2 - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=v$version" >> "$GITHUB_OUTPUT" + ref: ${{ github.ref_name }} - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - run: git reset --hard ${{ github.sha }} - - name: Create and push tag - env: - TAG: ${{ steps.version.outputs.tag }} - run: | - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Tag $TAG already exists; nothing to do." - exit 0 - fi - git tag -a "$TAG" -m "Release $TAG" - git push origin "$TAG" - - - name: Extract release notes from CHANGELOG - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - # CHANGELOG.md sections look like "## v0.2.0 (YYYY-MM-DD)" or similar. - # Capture lines after the matching header up to the next "## " header. - awk -v ver="$VERSION" ' - /^## / { - if (in_section) { exit } - if (index($0, ver) > 0) { in_section = 1; next } - } - in_section { print } - ' CHANGELOG.md > release_notes.md - if [ ! -s release_notes.md ]; then - echo "(no CHANGELOG entry matched v${VERSION}; release notes generated by GitHub)" > release_notes.md - fi + - id: release + uses: python-semantic-release/python-semantic-release@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: "github-actions" + git_committer_email: "actions@users.noreply.github.com" - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.version.outputs.tag }} - run: | - gh release create "$TAG" \ - --title "$TAG" \ - --notes-file release_notes.md + - uses: python-semantic-release/publish-action@v10.5.3 + if: steps.release.outputs.released == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.release.outputs.tag }} diff --git a/pyproject.toml b/pyproject.toml index 6e3601a..85b2625 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,7 @@ branch = "main" # Strict semver: breaking changes bump major even pre-1.0. major_on_zero = true allow_zero_version = true +build_command = "uv build" [tool.semantic_release.changelog.default_templates] changelog_file = "CHANGELOG.md"