diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bede81b..b07f5d2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -98,3 +98,78 @@ jobs: exit 0 fi exit $status + + github-release: + # Auto-create (or update) a GitHub Release matching the tag, with notes + # pulled from CHANGELOG.md. Closes #126. The Releases page is the + # canonical surface most users discover the project through; without + # this job it drifts behind tags / PyPI / MCP-registry state. Pattern + # ported from cmeans/mcp-synology publish.yml. + needs: publish-pypi + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Extract release notes from CHANGELOG + id: notes + env: + TAG: ${{ github.ref_name }} + run: | + # CHANGELOG.md is the authoritative release narrative. Extract the + # section for the current tag and use it as the Release body so we + # don't duplicate content or rely on low-quality auto-generated + # commit lists. Falls back to --generate-notes if the CHANGELOG + # has no matching entry (e.g. an emergency tag without doc prep). + VERSION="${TAG#v}" + # mcp-clipboard CHANGELOG uses Keep-a-Changelog format: + # `## [2.5.1] - 2026-05-05`. The literal `[VERSION]` headings + # distinguish actual releases from `## [Unreleased]` (also a + # bracketed heading but the version-string never matches). + awk -v version="$VERSION" ' + $0 ~ "^## \\["version"\\]" {flag=1; next} + flag && /^## / {exit} + flag + ' CHANGELOG.md > release_notes.md + + # Strip leading blank lines so the body does not start with an + # empty paragraph. + sed -i '/./,$!d' release_notes.md + + if [ -s release_notes.md ]; then + echo "Found CHANGELOG entry for $VERSION ($(wc -l < release_notes.md) lines)" + echo "use_changelog=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::No CHANGELOG entry for $VERSION — falling back to auto-generated notes" + echo "use_changelog=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create or update GitHub Release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + USE_CHANGELOG: ${{ steps.notes.outputs.use_changelog }} + run: | + # Idempotent: if a release for the tag already exists (e.g. it was + # hand-crafted before the publish workflow ran, or the workflow is + # being re-run after a partial failure), update its notes in place + # with `gh release edit` rather than failing with a 422. + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists — updating notes in place" + if [ "$USE_CHANGELOG" = "true" ]; then + gh release edit "$TAG" --title "$TAG" --notes-file release_notes.md + else + # Cannot regenerate auto notes on edit — leave existing body. + gh release edit "$TAG" --title "$TAG" + fi + else + echo "Creating new release $TAG" + if [ "$USE_CHANGELOG" = "true" ]; then + gh release create "$TAG" --title "$TAG" --notes-file release_notes.md + else + gh release create "$TAG" --title "$TAG" --generate-notes + fi + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0efcc..4b7266e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented here. ## [Unreleased] +### Added +- New `github-release` job in `publish.yml` auto-creates (or updates) a + GitHub Release matching the pushed tag, with notes pulled from the + matching `## [VERSION]` section of `CHANGELOG.md`. Idempotent: if a + Release for the tag already exists (hand-crafted, or a re-run of a + partially-failed publish), the job edits the notes in place via + `gh release edit` rather than failing with HTTP 422. Falls through to + `--generate-notes` (auto commit-list) when the CHANGELOG has no + matching entry. `needs: publish-pypi` so the Release only lands after + the wheel is on PyPI; `permissions: contents: write` enables + `gh release create`. Closes the GitHub-Releases-page-drift gap that + surfaced after v2.4.0/v2.5.0/v2.5.1 all landed on PyPI without + matching Releases (manually backfilled on 2026-05-06). Pattern ported + from mcp-synology with a CHANGELOG-format adaptation: mcp-clipboard + uses Keep-a-Changelog `## [VERSION]` brackets, so the awk anchor is + `^## \[VERSION\]` rather than mcp-synology's `^## VERSION( |\()`. + (#127) - closes #126. + ## [2.5.1] - 2026-05-05 ### Fixed