From 2a7a77151717fcc445d1fb97d886566ef1f0e7e6 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Wed, 6 May 2026 14:23:23 -0400 Subject: [PATCH] ci(release): auto-create GitHub Release after PyPI publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `create-release` job to `python-publish.yml` that runs after `publish-to-pypi` succeeds and creates a GitHub Release with body extracted from the corresponding CHANGELOG.md section. Eliminates the manual `gh release create` step every release. Previously, v0.4.0 → v0.4.1 each required a separate manual release-creation pass after the tag-triggered PyPI publish. Behavior: - Triggered by tag push only (not manual workflow_dispatch republishes). - Extracts the CHANGELOG section between `## [VERSION]` and the next `## [` heading using a portable string-based awk script (no regex escaping issues across awk variants). - If no CHANGELOG entry is found for the version, falls back to a generic "see commit history" body — does not fail the job. - Title is the tag name (e.g. `v0.4.1`); the descriptive suffix on prior releases ("v0.4.0 — Dynamic subject and action fields...") is convention, not required, and the user can edit titles after the fact. - `prerelease: ${{ contains(github.ref_name, '-') }}` flags any tag with a hyphen (e.g. `v0.5.0-rc.1`) as prerelease automatically. Pinned action SHAs: - softprops/action-gh-release@b430933... (v3.0.0) - actions/checkout@de0fac2... (v6, matches existing job) Permissions: - `contents: write` on this job only; default `contents: read` preserved for the workflow-level minimum. Tested locally: - Successful extraction for v0.4.1 produces clean body (CHANGELOG section without heading or next-version boundary). - Missing version (e.g. v9.9.9) takes the fallback path without erroring. This means future releases will be one-step: `git tag vX.Y.Z` + `git push origin vX.Y.Z` and the workflow handles both PyPI publish and GitHub Release creation. --- .github/workflows/python-publish.yml | 44 +++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 009b7df..542616f 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -88,4 +88,46 @@ jobs: path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 \ No newline at end of file + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + + create-release: + name: Create GitHub Release + needs: publish-to-pypi + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - name: Check out source + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Extract release notes from CHANGELOG + id: notes + run: | + version="${GITHUB_REF_NAME#v}" + # Extract the section between this version's heading and the next "## [" heading. + # Uses string functions instead of regex to stay portable across awk variants. + notes=$(awk -v v="$version" ' + BEGIN { start = "## [" v "]"; flag = 0 } + substr($0, 1, length(start)) == start { flag = 1; next } + substr($0, 1, 4) == "## [" { flag = 0 } + flag { print } + ' CHANGELOG.md) + if [ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then + echo "::warning::No CHANGELOG entry found for v${version} — using fallback body" + notes="Release v${version}. See commit history for changes." + fi + { + echo "notes<> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + with: + name: ${{ github.ref_name }} + body: ${{ steps.notes.outputs.notes }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} \ No newline at end of file