From a99d3ac8483f3771b722c4ccc9b40250c8227734 Mon Sep 17 00:00:00 2001 From: Reynier Ortiz Vega Date: Wed, 10 Jun 2026 14:17:42 -0400 Subject: [PATCH] Add release-notes automation with Slack announce Cutting a ToolHive release is followed by two manual chores: writing polished release notes and announcing the release in Slack. Both are repeatable and were being done by hand each time. This adds a workflow that fires on the `release: published` event (the same event that already drives releaser.yml) and: - Runs the existing `release-notes` Claude skill to analyze every merged PR in the range, cross-reference issues, and dispatch expert subagents for breaking-change assessment, producing copy-pasteable markdown. - Delivers it review-then-publish: the public release notes are not overwritten. The markdown is uploaded as an artifact, written to the job summary, and posted as a comment on the release PR for a maintainer to review and apply. - Posts a single Slack announcement reusing the theme summary the skill already emits, plus a link to the full release notes, via the same incoming-webhook pattern as the existing failure notification. Also supports workflow_dispatch with a tag input for dry-running against an already-published release. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-notes.yml | 231 ++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .github/workflows/release-notes.yml diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000000..8179a568d3 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,231 @@ +# Generate Release Notes workflow +# +# When a GitHub Release is published (by create-release-tag.yml after a release +# PR merges), this workflow runs the `release-notes` Claude skill to produce +# polished, copy-pasteable release notes: +# - analyzes every merged PR between the previous tag and this one +# - cross-references linked issues +# - dispatches expert subagents to assess breaking changes +# +# Delivery is REVIEW-THEN-PUBLISH: the public release notes are NOT overwritten. +# The generated markdown is uploaded as a build artifact, written to the job +# summary, and posted as a comment on the release PR for a maintainer to review +# and copy-paste (or apply via `gh release edit`). +# +# Can also be triggered manually against any existing tag for testing. + +name: Generate Release Notes + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag to generate notes for (e.g. v0.29.2)' + required: true + type: string + +permissions: + contents: read + pull-requests: write + issues: read + +jobs: + release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + # Mapped to a job-level env var so the Slack step can gate on it: the + # `secrets` context is not available in step-level `if:` conditions. + SLACK_WEBHOOK: ${{ secrets.SLACK_TOOLHIVE_RELEASE_WEBHOOK_URL }} + steps: + - name: Resolve release tag + id: tag + env: + EVENT_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ inputs.tag }} + run: | + TAG="${EVENT_TAG:-$INPUT_TAG}" + if [ -z "$TAG" ]; then + echo "::error::No release tag resolved from event or input" + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Generating release notes for: $TAG" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + # Full history + tags are required: the skill lists/sorts tags and + # compares ranges between the current and previous release. + fetch-depth: 0 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + with: + go-version: 'stable' + + - name: Setup helm-docs + run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@latest + + - name: Generate release notes with Claude + id: claude + uses: anthropics/claude-code-action@fbda2eb1bdc90d319b8d853f5deb53bca199a7c1 # v1 + env: + # gh CLI used by the skill (read-only PR/issue lookups) authenticates + # via GH_TOKEN. Read access is sufficient — publishing happens in a + # later, deterministic step, not by the model. + GH_TOKEN: ${{ github.token }} + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + You are generating GitHub release notes for the ToolHive release ${{ steps.tag.outputs.tag }}. + + Follow the procedure in `.claude/skills/release-notes/SKILL.md` exactly, + using the template at `.claude/skills/release-notes/TEMPLATE.md`. + + This runs UNATTENDED in CI. Apply these automation overrides: + - Complete Phases 1-4 in full: analyze every merged PR between the + previous tag and ${{ steps.tag.outputs.tag }}, cross-reference linked + issues, and dispatch the Phase 3 expert subagents for breaking-change + assessment. + - The GitHub auto-generated "What's Changed" block already exists on the + release; read it from `gh release view ${{ steps.tag.outputs.tag }}` + and preserve it verbatim as instructed by the skill. + - For Phase 5, do NOT wait for approval and do NOT publish or edit the + GitHub release. ONLY write the final notes to + `release-notes-${{ steps.tag.outputs.tag }}.md` in the repo root. + - Do not post comments or run `gh release edit`. Stop after writing the file. + # Tools: gh/git for read-only data gathering, Task for the expert + # subagents, and file tools for reading the skill and writing the output. + claude_args: | + --allowedTools "Bash(gh *),Bash(git *),Task,Read,Write,Edit,Glob,Grep,TodoWrite" + --max-turns 60 + + - name: Verify notes were generated + id: verify + run: | + FILE="release-notes-${{ steps.tag.outputs.tag }}.md" + if [ ! -s "$FILE" ]; then + echo "::error::Expected $FILE was not generated by the release-notes skill" + exit 1 + fi + echo "file=$FILE" >> "$GITHUB_OUTPUT" + echo "Generated $(wc -l < "$FILE") lines into $FILE" + + - name: Extract theme summary + id: summary + env: + FILE: ${{ steps.verify.outputs.file }} + run: | + # The skill's template opens with a one-to-two sentence theme summary + # as the first paragraph after the H1 title. Grab that single paragraph + # for the Slack announcement — it is plain prose (Slack mrkdwn renders + # its inline `code` spans fine), so it needs no conversion. + SUMMARY=$(awk '/^# /{f=1;next} f&&NF==0{next} f&&NF{print;exit}' "$FILE") + if [ -z "$SUMMARY" ]; then + echo "::warning::Could not extract a theme summary; falling back to a generic line" + SUMMARY="A new ToolHive release is available." + fi + echo "text=$SUMMARY" >> "$GITHUB_OUTPUT" + + - name: Upload release notes artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: release-notes-${{ steps.tag.outputs.tag }} + path: ${{ steps.verify.outputs.file }} + if-no-files-found: error + + - name: Write notes to job summary + run: | + { + echo "## 📝 Generated release notes for \`${{ steps.tag.outputs.tag }}\`" + echo "" + echo "Review below, then copy-paste into the release or run:" + echo "" + echo '```' + echo "gh release edit ${{ steps.tag.outputs.tag }} --notes-file ${{ steps.verify.outputs.file }}" + echo '```' + echo "" + echo "---" + echo "" + cat "${{ steps.verify.outputs.file }}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Post notes as a comment on the release PR + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + FILE: ${{ steps.verify.outputs.file }} + run: | + # The release PR is titled "Release ". Best-effort: if found, post + # the generated notes as a comment so the maintainer who merged it gets + # notified. If not found, the artifact + job summary still carry the notes. + PR=$(gh pr list --search "Release ${TAG} in:title" --state merged \ + --json number --jq '.[0].number' 2>/dev/null || true) + if [ -z "$PR" ] || [ "$PR" = "null" ]; then + echo "No merged release PR found for ${TAG}; skipping comment." + echo "Notes are available in the job summary and as a build artifact." + exit 0 + fi + + { + echo "## 📝 Generated release notes for \`${TAG}\`" + echo "" + echo "Auto-generated by the \`release-notes\` skill. Review and, if good, apply with:" + echo "" + echo '```' + echo "gh release edit ${TAG} --notes-file .md" + echo '```' + echo "" + echo "
Click to expand release notes" + echo "" + cat "$FILE" + echo "" + echo "
" + } > /tmp/comment.md + + gh pr comment "$PR" --body-file /tmp/comment.md + echo "Posted release notes to PR #${PR}" + + - name: Announce release in Slack + # Single channel message (no thread): header + the skill's theme summary + + # a link to the full GitHub release notes. Uses the same incoming-webhook + # pattern as the failure notification in releaser.yml. + # + # NOTE: SLACK_TOOLHIVE_RELEASE_WEBHOOK_URL currently backs the + # release-failure alert. If it does not target #toolhive, point this at a + # #toolhive-specific incoming webhook secret instead. + if: ${{ env.SLACK_WEBHOOK != '' }} + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + with: + webhook: ${{ env.SLACK_WEBHOOK }} + webhook-type: incoming-webhook + payload: | + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 ToolHive ${{ steps.tag.outputs.tag }} is live!", + "emoji": true + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ${{ toJSON(steps.summary.outputs.text) }} + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":memo: " + } + } + ] + }