-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat: add goose-powered release notes generator workflow #7503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
2c1887a
feat: add goose-powered release notes generator workflow
blackgirlbytes dc220ef
feat: add Discord bot integration for release announcements
blackgirlbytes 3f5daff
fix: suppress Discord embeds for cleaner release announcements
blackgirlbytes acd5f7c
fix: use secrets for Discord channel ID instead of variables
blackgirlbytes ec67bad
fix: check Discord config inside script instead of job-level if
blackgirlbytes d0898bc
debug: add Discord API error logging
blackgirlbytes dd57a0e
feat: improve Discord message format with release notes preview
blackgirlbytes d1cde06
fix: correct YAML indentation in multiline strings
blackgirlbytes 5bd6abd
fix: simplify Discord posting - header only in main msg, full notes i…
blackgirlbytes c8ee0f4
fix: show first section (Features) in main msg, rest in thread withou…
blackgirlbytes ebe0df1
fix: fit as many complete bullet points in main msg, continue rest in…
blackgirlbytes 4c32660
fix: add @everyone ping and change Download to Release
blackgirlbytes 7822f79
feat: switch to Anthropic with claude-opus-4-5 model
blackgirlbytes 958d9c1
feat: use RELEASE_BOT_ANTHROPIC_KEY secret
blackgirlbytes 98f3425
feat: rename Discord secrets to DISCORD_RELEASE_BOT_TOKEN and DISCORD…
blackgirlbytes 2e1f22c
fix: move @everyone to before 'More in thread' footer
blackgirlbytes 720a6a1
fix: use workflow_run trigger instead of release:published
blackgirlbytes f388632
chore: remove verbose comments from header
blackgirlbytes 5b02152
fix: prevent shell injection via environment variables
blackgirlbytes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,350 @@ | ||
| # goose Release Notes Generator | ||
| # | ||
| # Automatically generates release notes using goose AI agent when a new release is published. | ||
| # Updates the GitHub release with AI-generated categorized notes and posts to Discord. | ||
| # | ||
| # Uses workflow_run instead of release:published because GitHub doesn't trigger | ||
| # workflows for events created by GITHUB_TOKEN (to prevent infinite loops). | ||
|
|
||
| name: goose Release Notes Generator | ||
|
|
||
| on: | ||
| # Trigger when Release workflow completes (works around GITHUB_TOKEN limitation) | ||
| workflow_run: | ||
| workflows: | ||
| - Release | ||
| types: | ||
| - completed | ||
|
|
||
| # Allow manual trigger for testing | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: 'Release tag to generate notes for (e.g., v1.25.0)' | ||
| required: true | ||
| type: string | ||
|
|
||
| env: | ||
| GOOSE_RECIPE: | | ||
| version: "1.0.0" | ||
| title: "Release Notes Generator" | ||
| description: "Generate release notes for ${RELEASE_TAG}" | ||
|
|
||
| extensions: | ||
| - type: builtin | ||
| name: developer | ||
|
|
||
| instructions: | | ||
| Generate release notes for the goose release. | ||
|
|
||
| ## Process | ||
| 1. You are already in the goose repository. Do NOT clone or checkout anything. | ||
| 2. Get the previous release tag by running: git describe --tags --abbrev=0 ${RELEASE_TAG}^ | ||
| 3. Get commits between tags: git log <previous_tag>..${RELEASE_TAG} --oneline --no-merges | ||
| 4. Analyze the commits and categorize changes | ||
|
|
||
| ## Output Format | ||
| Categorize changes into these sections (skip empty sections): | ||
| - ✨ **Features** - New functionality | ||
| - 🐛 **Bug Fixes** - Bug fixes | ||
| - 🔧 **Improvements** - Enhancements to existing features | ||
| - 📚 **Documentation** - Documentation updates | ||
|
|
||
| Format each item as: | ||
| - Concise description [#XXXX](https://github.com/block/goose/pull/XXXX) | ||
|
|
||
| Rules: | ||
| - Extract PR numbers from commit messages (look for (#XXXX) pattern) | ||
| - Remove redundant words like "Added", "Fixed", "Documented" - the category headers make these clear | ||
| - Keep descriptions user-friendly and concise | ||
| - Order: Features → Bug Fixes → Improvements → Documentation | ||
|
|
||
| ## Final Step | ||
| Write ONLY the release notes content to /tmp/release_notes.md (no extra commentary) | ||
|
|
||
| prompt: | | ||
| Generate release notes for ${RELEASE_TAG} in the goose repository. | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| concurrency: | ||
| group: release-notes-${{ github.event.workflow_run.head_branch || inputs.tag }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| generate-release-notes: | ||
| name: Generate Release Notes | ||
| runs-on: ubuntu-latest | ||
| # For workflow_run: only run if Release succeeded and tag is not 'stable' | ||
| # For workflow_dispatch: only run if tag is not 'stable' | ||
| if: | | ||
| (github.event_name == 'workflow_dispatch' && inputs.tag != 'stable') || | ||
| (github.event_name == 'workflow_run' && | ||
| github.event.workflow_run.conclusion == 'success' && | ||
| github.event.workflow_run.head_branch != 'stable') | ||
|
|
||
| container: | ||
| image: ghcr.io/block/goose:latest | ||
| options: --user root | ||
| env: | ||
| GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} | ||
| GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'claude-opus-4-5' }} | ||
| ANTHROPIC_API_KEY: ${{ secrets.RELEASE_BOT_ANTHROPIC_KEY }} | ||
| HOME: /tmp/goose-home | ||
|
|
||
| outputs: | ||
| release_notes: ${{ steps.read-notes.outputs.notes }} | ||
| notes_length: ${{ steps.read-notes.outputs.length }} | ||
| release_tag: ${{ steps.get-tag.outputs.tag }} | ||
|
|
||
| steps: | ||
| - name: Get release tag | ||
| id: get-tag | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| INPUT_TAG: ${{ inputs.tag }} | ||
| WORKFLOW_RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} | ||
| run: | | ||
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | ||
| echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "tag=$WORKFLOW_RUN_BRANCH" >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Checkout repository | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| fetch-depth: 0 | ||
| fetch-tags: true | ||
|
|
||
| - name: Install tools | ||
| run: | | ||
| apt-get update | ||
| apt-get install -y gettext curl ripgrep git jq | ||
|
|
||
| - name: Run goose to generate release notes | ||
| env: | ||
| RELEASE_TAG: ${{ steps.get-tag.outputs.tag }} | ||
| run: | | ||
| mkdir -p $HOME/.local/share/goose/sessions | ||
| mkdir -p $HOME/.config/goose | ||
| git config --global --add safe.directory "$GITHUB_WORKSPACE" | ||
|
|
||
| # Checkout the release tag | ||
| git checkout "${RELEASE_TAG}" | ||
|
|
||
| # Create recipe from env var with variable substitution | ||
| echo "$GOOSE_RECIPE" | envsubst '$RELEASE_TAG' > /tmp/recipe.yaml | ||
|
|
||
| goose run --recipe /tmp/recipe.yaml | ||
|
|
||
| - name: Read release notes | ||
| id: read-notes | ||
| run: | | ||
| if [ -f /tmp/release_notes.md ]; then | ||
| # Use random delimiter to prevent injection | ||
| DELIMITER="EOF_$(openssl rand -hex 8)" | ||
| echo "notes<<$DELIMITER" >> $GITHUB_OUTPUT | ||
| cat /tmp/release_notes.md >> $GITHUB_OUTPUT | ||
| echo "" >> $GITHUB_OUTPUT | ||
| echo "$DELIMITER" >> $GITHUB_OUTPUT | ||
|
|
||
| LENGTH=$(wc -c < /tmp/release_notes.md) | ||
| echo "length=$LENGTH" >> $GITHUB_OUTPUT | ||
|
|
||
| echo "::notice::Release notes generated successfully (${LENGTH} chars)" | ||
| else | ||
| echo "::error::Release notes file not found at /tmp/release_notes.md" | ||
| echo "notes=Release notes generation failed." >> $GITHUB_OUTPUT | ||
| echo "length=0" >> $GITHUB_OUTPUT | ||
| exit 1 | ||
| fi | ||
|
|
||
| update-github-release: | ||
| name: Update GitHub Release | ||
| runs-on: ubuntu-latest | ||
| needs: generate-release-notes | ||
| permissions: | ||
| contents: write | ||
|
|
||
| steps: | ||
| - name: Update release body | ||
| uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 | ||
| with: | ||
| tag: ${{ needs.generate-release-notes.outputs.release_tag }} | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| body: ${{ needs.generate-release-notes.outputs.release_notes }} | ||
| allowUpdates: true | ||
| omitNameDuringUpdate: true | ||
| omitPrereleaseDuringUpdate: true | ||
|
|
||
| post-to-discord: | ||
| name: Post to Discord | ||
| runs-on: ubuntu-latest | ||
| needs: generate-release-notes | ||
|
|
||
| steps: | ||
| - name: Post release announcement | ||
| env: | ||
| DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_RELEASE_BOT_TOKEN }} | ||
| DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_BOT_CHANNEL_ID }} | ||
| RELEASE_TAG: ${{ needs.generate-release-notes.outputs.release_tag }} | ||
| RELEASE_URL: https://github.com/block/goose/releases/tag/${{ needs.generate-release-notes.outputs.release_tag }} | ||
| RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }} | ||
| NOTES_LENGTH: ${{ needs.generate-release-notes.outputs.notes_length }} | ||
| shell: bash | ||
| run: | | ||
| # Skip if Discord is not configured | ||
| if [ -z "$DISCORD_CHANNEL_ID" ] || [ -z "$DISCORD_BOT_TOKEN" ]; then | ||
| echo "::notice::Discord not configured, skipping" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Discord message character limit is ~2000 for regular messages | ||
| SAFE_LIMIT=1800 | ||
|
|
||
| # Function to send Discord message via bot API, returns message ID | ||
| send_discord() { | ||
| local content="$1" | ||
| local channel="$2" | ||
|
|
||
| content=$(echo "$content" | jq -Rs .) | ||
|
|
||
| response=$(curl -s -X POST "https://discord.com/api/v10/channels/${channel}/messages" \ | ||
| -H "Authorization: Bot ${DISCORD_BOT_TOKEN}" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"content\": $content, \"flags\": 4}") | ||
|
|
||
| # Debug: show response if there's an error | ||
| if echo "$response" | jq -e '.code' > /dev/null 2>&1; then | ||
| echo "::warning::Discord API error: $(echo "$response" | jq -c '.')" >&2 | ||
| fi | ||
|
|
||
| echo "$response" | jq -r '.id // empty' | ||
| } | ||
|
|
||
| # Function to create a thread from a message | ||
| create_thread() { | ||
| local channel="$1" | ||
| local message_id="$2" | ||
| local thread_name="$3" | ||
|
|
||
| response=$(curl -s -X POST "https://discord.com/api/v10/channels/${channel}/messages/${message_id}/threads" \ | ||
| -H "Authorization: Bot ${DISCORD_BOT_TOKEN}" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "{\"name\": \"${thread_name}\"}") | ||
|
|
||
| echo "$response" | jq -r '.id // empty' | ||
| } | ||
|
|
||
| # Build the announcement header | ||
| HEADER="## 🎉 goose ${RELEASE_TAG} is here! | ||
|
|
||
| 📦 **Release:** ${RELEASE_URL}" | ||
|
|
||
| FOOTER=" | ||
|
|
||
| @everyone | ||
| 📝 *More in thread...*" | ||
| HEADER_LEN=${#HEADER} | ||
| FOOTER_LEN=${#FOOTER} | ||
| AVAILABLE=$((SAFE_LIMIT - HEADER_LEN - FOOTER_LEN - 10)) | ||
|
|
||
| # Check if everything fits in one message (no thread needed) | ||
| FULL_MSG="${HEADER} | ||
|
|
||
| ${RELEASE_NOTES}" | ||
| if [ ${#FULL_MSG} -le "$SAFE_LIMIT" ]; then | ||
| send_discord "$FULL_MSG" "$DISCORD_CHANNEL_ID" | ||
| echo "::notice::Discord notification sent successfully" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Need to split - use awk to fit complete lines in main vs thread | ||
| echo "$RELEASE_NOTES" > /tmp/release_notes_full.txt | ||
|
|
||
| awk -v avail="$AVAILABLE" ' | ||
| BEGIN { main_len = 0; in_thread = 0 } | ||
| { | ||
| line_len = length($0) + 1 # +1 for newline | ||
| if (!in_thread && (main_len + line_len) <= avail) { | ||
| print > "/tmp/main_content.txt" | ||
| main_len += line_len | ||
| } else { | ||
| in_thread = 1 | ||
| print > "/tmp/thread_content.txt" | ||
| } | ||
| } | ||
| ' /tmp/release_notes_full.txt | ||
|
|
||
| # Read the split content | ||
| MAIN_CONTENT="" | ||
| [ -f /tmp/main_content.txt ] && MAIN_CONTENT=$(cat /tmp/main_content.txt) | ||
|
|
||
| THREAD_CONTENT="" | ||
| [ -f /tmp/thread_content.txt ] && THREAD_CONTENT=$(cat /tmp/thread_content.txt) | ||
|
|
||
| # Build and send main message | ||
| MAIN_MESSAGE="${HEADER} | ||
|
|
||
| ${MAIN_CONTENT} | ||
| ${FOOTER}" | ||
|
|
||
| MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") | ||
|
|
||
| if [ -z "$MESSAGE_ID" ]; then | ||
| echo "::error::Failed to send Discord message" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Created message $MESSAGE_ID, creating thread..." | ||
| sleep 1 | ||
|
|
||
| # Create thread | ||
| THREAD_ID=$(create_thread "$DISCORD_CHANNEL_ID" "$MESSAGE_ID" "Release Notes ${RELEASE_TAG}") | ||
| if [ -z "$THREAD_ID" ]; then | ||
| echo "::warning::Failed to create thread" | ||
| THREAD_ID="$DISCORD_CHANNEL_ID" | ||
| fi | ||
| sleep 1 | ||
|
|
||
| # Post thread content in chunks (by complete lines) | ||
| if [ -n "$THREAD_CONTENT" ]; then | ||
| # Split thread content into chunks using awk | ||
| awk -v limit="$SAFE_LIMIT" ' | ||
| BEGIN { chunk = ""; chunk_len = 0; chunk_num = 0 } | ||
| { | ||
| line_len = length($0) + 1 | ||
| if (chunk_len + line_len > limit && chunk_len > 0) { | ||
| # Save current chunk and start new one | ||
| print chunk > "/tmp/chunk_" chunk_num ".txt" | ||
| chunk_num++ | ||
| chunk = $0 "\n" | ||
| chunk_len = line_len | ||
| } else { | ||
| chunk = chunk $0 "\n" | ||
| chunk_len += line_len | ||
| } | ||
| } | ||
| END { | ||
| if (chunk_len > 0) { | ||
| print chunk > "/tmp/chunk_" chunk_num ".txt" | ||
| } | ||
| } | ||
| ' /tmp/thread_content.txt | ||
|
|
||
| # Send each chunk | ||
| for chunk_file in /tmp/chunk_*.txt; do | ||
| [ -f "$chunk_file" ] || continue | ||
| CHUNK=$(cat "$chunk_file") | ||
| [ -n "$CHUNK" ] && send_discord "$CHUNK" "$THREAD_ID" | ||
| sleep 1 | ||
| rm -f "$chunk_file" | ||
| done | ||
| fi | ||
|
|
||
| # Cleanup | ||
| rm -f /tmp/release_notes_full.txt /tmp/main_content.txt /tmp/thread_content.txt | ||
|
|
||
| echo "::notice::Discord notification sent successfully" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the short-message branch, the workflow calls
send_discordand immediately reports success without checking whether Discord actually accepted the post. If the API returns an error (for example invalid bot permissions, bad channel ID, or rate limiting),send_discordemits a warning and returns an empty ID, but this path still exits0, so release announcements can be silently dropped even though the job appears green.Useful? React with 👍 / 👎.