From 41010f603065d28fea49954fe8a47d96cdd8009b Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 23:51:28 -0500 Subject: [PATCH] ci: auto-create GitHub Release on tag push (port mcp-synology github-release job) Adds a github-release job to publish.yml that runs after publish-pypi on every tag push. The job extracts the matching CHANGELOG section and creates (or updates) a GitHub Release with those notes. Closes the Releases-page-drift gap that surfaced after v2.4.0 / v2.5.0 / v2.5.1 all landed on PyPI without matching Releases (backfilled manually on 2026-05-06). Pattern ported from cmeans/mcp-synology publish.yml with one CHANGELOG-format adaptation: - mcp-synology CHANGELOG uses `## VERSION (DATE)` headings (no brackets), and its awk matcher anchors on `^## VERSION( |\()`. - mcp-clipboard CHANGELOG uses Keep-a-Changelog `## [VERSION] - DATE` headings (literal brackets). The awk matcher here anchors on `^## \[VERSION\]` to match. Verified locally against the live CHANGELOG: extraction returns the right notes block for v2.5.1, v2.5.0, v2.4.0, and v2.3.0. Job shape: - needs: publish-pypi (Release only lands after the wheel is on PyPI) - permissions: contents: write (enables gh release create/edit) - Step 1: awk-extract the matching CHANGELOG section to release_notes.md. Strip leading blank lines so the body doesn't start with an empty paragraph. Set use_changelog=true/false output. - Step 2: Idempotent gh release create / edit. If a Release already exists for the tag (hand-crafted, or re-running a partially-failed publish), edit it in place rather than failing with HTTP 422. Fall through to --generate-notes (auto commit-list) when the CHANGELOG has no matching entry. CHANGELOG entry under [Unreleased] / Added. No runtime code change. 599 tests still pass; ruff clean; sync-server- json --check passes. Closes #126. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish.yml | 75 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 18 +++++++++ 2 files changed, 93 insertions(+) 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