From 2c1887a3bea6ea03e535bcda47bdc94ddb7cccac Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 05:20:57 -0500 Subject: [PATCH 01/19] feat: add goose-powered release notes generator workflow Automatically generates categorized release notes using goose AI agent when a new release is published. Updates the GitHub release page with AI-generated notes organized into Features, Bug Fixes, Improvements, and Documentation sections. - Triggers on release publish or manual workflow_dispatch - Uses goose with developer extension to analyze git history - Extracts PR numbers and creates linked release notes - Skips 'stable' tag releases (just pointers to versioned releases) --- .github/workflows/goose-release-notes.yml | 163 ++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .github/workflows/goose-release-notes.yml diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml new file mode 100644 index 000000000000..6c0bbba1d7f8 --- /dev/null +++ b/.github/workflows/goose-release-notes.yml @@ -0,0 +1,163 @@ +# 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. +# +# Trigger: When a GitHub release is published (after release.yml completes) +# +# Required Secrets: +# - OPENROUTER_API_KEY: API key for OpenRouter +# +# Optional Variables: +# - GOOSE_PROVIDER: LLM provider (default: openrouter) +# - GOOSE_MODEL: Model name (default: anthropic/claude-sonnet-4) + +name: goose Release Notes Generator + +on: + release: + types: [published] + + # 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 ..${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.release.tag_name || inputs.tag }} + cancel-in-progress: true + +jobs: + generate-release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + # Skip stable tag releases - they're just pointers to versioned releases + if: ${{ (github.event.release.tag_name || inputs.tag) != 'stable' }} + + container: + image: ghcr.io/block/goose:latest + options: --user root + env: + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openrouter' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'anthropic/claude-sonnet-4' }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + HOME: /tmp/goose-home + + outputs: + release_notes: ${{ steps.read-notes.outputs.notes }} + notes_length: ${{ steps.read-notes.outputs.length }} + + steps: + - 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: ${{ github.event.release.tag_name || inputs.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: ${{ github.event.release.tag_name || inputs.tag }} + token: ${{ secrets.GITHUB_TOKEN }} + body: ${{ needs.generate-release-notes.outputs.release_notes }} + allowUpdates: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true From dc220ef1e186d7f7935494bae5ba873921c70ead Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 05:22:58 -0500 Subject: [PATCH 02/19] feat: add Discord bot integration for release announcements - Posts release announcement to Discord channel - Creates thread for long release notes (>1800 chars) - Uses Discord bot API for thread creation support - Requires DISCORD_BOT_TOKEN secret and DISCORD_RELEASE_CHANNEL_ID variable --- .github/workflows/goose-release-notes.yml | 124 +++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 6c0bbba1d7f8..9dfe57177e16 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -1,16 +1,20 @@ # 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. +# Updates the GitHub release with AI-generated categorized notes and posts to Discord. # # Trigger: When a GitHub release is published (after release.yml completes) # # Required Secrets: # - OPENROUTER_API_KEY: API key for OpenRouter # +# Optional Secrets (for Discord): +# - DISCORD_BOT_TOKEN: Discord bot token for posting release announcements +# # Optional Variables: # - GOOSE_PROVIDER: LLM provider (default: openrouter) # - GOOSE_MODEL: Model name (default: anthropic/claude-sonnet-4) +# - DISCORD_RELEASE_CHANNEL_ID: Discord channel ID for release announcements name: goose Release Notes Generator @@ -161,3 +165,121 @@ jobs: allowUpdates: true omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true + + post-to-discord: + name: Post to Discord + runs-on: ubuntu-latest + needs: generate-release-notes + # Only run if Discord bot token is configured + if: ${{ vars.DISCORD_RELEASE_CHANNEL_ID != '' }} + + steps: + - name: Post release announcement + env: + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} + DISCORD_CHANNEL_ID: ${{ vars.DISCORD_RELEASE_CHANNEL_ID }} + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} + RELEASE_URL: ${{ github.event.release.html_url || format('https://github.com/block/goose/releases/tag/{0}', inputs.tag) }} + RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }} + NOTES_LENGTH: ${{ needs.generate-release-notes.outputs.notes_length }} + shell: bash + run: | + # Discord message character limit is ~2000 for regular messages + SAFE_LIMIT=1800 + + # Build header message + HEADER="🚀 **Goose ${RELEASE_TAG} Released!** + + 📦 **Download:** ${RELEASE_URL}" + + # 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}") + + 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' + } + + # Check if we need to thread (notes too long) + HEADER_LENGTH=${#HEADER} + TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 10)) + + if [ "$TOTAL_LENGTH" -le "$SAFE_LIMIT" ]; then + # Single message - notes fit + FULL_MESSAGE="${HEADER} + + ${RELEASE_NOTES}" + send_discord "$FULL_MESSAGE" "$DISCORD_CHANNEL_ID" + else + # Need to thread - send header first, create thread, then post notes in thread + echo "Release notes exceed Discord limit (${TOTAL_LENGTH} chars), using thread..." + + # Send header message + MESSAGE_ID=$(send_discord "${HEADER} + + 📝 *Full release notes in thread below*" "$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 from the message + THREAD_ID=$(create_thread "$DISCORD_CHANNEL_ID" "$MESSAGE_ID" "Release Notes ${RELEASE_TAG}") + + if [ -z "$THREAD_ID" ]; then + echo "::warning::Failed to create thread, posting notes as follow-up messages" + THREAD_ID="$DISCORD_CHANNEL_ID" + else + echo "Created thread $THREAD_ID" + fi + + sleep 1 + + # Post release notes in chunks to the thread + REMAINING="$RELEASE_NOTES" + + while [ -n "$REMAINING" ]; do + CHUNK="${REMAINING:0:$SAFE_LIMIT}" + REMAINING="${REMAINING:$SAFE_LIMIT}" + + # Try to break at a newline for cleaner splits + if [ -n "$REMAINING" ]; then + LAST_NEWLINE=$(echo "$CHUNK" | tail -c +1000 | grep -bo $'\n' | tail -1 | cut -d: -f1 || echo "") + if [ -n "$LAST_NEWLINE" ]; then + BREAK_POINT=$((1000 + LAST_NEWLINE)) + REMAINING="${CHUNK:$BREAK_POINT}${REMAINING}" + CHUNK="${CHUNK:0:$BREAK_POINT}" + fi + fi + + send_discord "$CHUNK" "$THREAD_ID" + sleep 1 + done + fi + + echo "::notice::Discord notification sent successfully" From 3f5daffc9cb8735c9002f7e62cd47b458fe9e935 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 15:32:38 -0500 Subject: [PATCH 03/19] fix: suppress Discord embeds for cleaner release announcements --- .github/workflows/goose-release-notes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 9dfe57177e16..3705ce138428 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -202,7 +202,7 @@ jobs: 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}") + -d "{\"content\": $content, \"flags\": 4}") echo "$response" | jq -r '.id // empty' } From acd5f7c34e04e96ac1131678c2f28eeaf320b60c Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 15:39:46 -0500 Subject: [PATCH 04/19] fix: use secrets for Discord channel ID instead of variables --- .github/workflows/goose-release-notes.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 3705ce138428..6acfe162eea0 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -171,13 +171,13 @@ jobs: runs-on: ubuntu-latest needs: generate-release-notes # Only run if Discord bot token is configured - if: ${{ vars.DISCORD_RELEASE_CHANNEL_ID != '' }} + if: ${{ secrets.DISCORD_RELEASE_CHANNEL_ID != '' }} steps: - name: Post release announcement env: DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} - DISCORD_CHANNEL_ID: ${{ vars.DISCORD_RELEASE_CHANNEL_ID }} + DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_CHANNEL_ID }} RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} RELEASE_URL: ${{ github.event.release.html_url || format('https://github.com/block/goose/releases/tag/{0}', inputs.tag) }} RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }} From ec67bad18ebf71d6667d35476a7cf9265d4e91fe Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 15:40:26 -0500 Subject: [PATCH 05/19] fix: check Discord config inside script instead of job-level if --- .github/workflows/goose-release-notes.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 6acfe162eea0..d21c71a04ebf 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -170,8 +170,6 @@ jobs: name: Post to Discord runs-on: ubuntu-latest needs: generate-release-notes - # Only run if Discord bot token is configured - if: ${{ secrets.DISCORD_RELEASE_CHANNEL_ID != '' }} steps: - name: Post release announcement @@ -184,6 +182,12 @@ jobs: 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 From d0898bccdbcfaf779edff792a1dc95219a459570 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 15:42:57 -0500 Subject: [PATCH 06/19] debug: add Discord API error logging --- .github/workflows/goose-release-notes.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index d21c71a04ebf..667e8b281470 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -208,6 +208,11 @@ jobs: -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' } From dd57a0ec3e734083efb7e3b6070c0f621ae7408c Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 17:49:44 -0500 Subject: [PATCH 07/19] feat: improve Discord message format with release notes preview --- .github/workflows/goose-release-notes.yml | 45 +++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 667e8b281470..41fd64b57bf6 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -191,11 +191,6 @@ jobs: # Discord message character limit is ~2000 for regular messages SAFE_LIMIT=1800 - # Build header message - HEADER="🚀 **Goose ${RELEASE_TAG} Released!** - - 📦 **Download:** ${RELEASE_URL}" - # Function to send Discord message via bot API, returns message ID send_discord() { local content="$1" @@ -230,24 +225,50 @@ jobs: echo "$response" | jq -r '.id // empty' } + # Build the header + HEADER="## 🎉 goose ${RELEASE_TAG} is here! + +📦 **Download:** ${RELEASE_URL}" + # Check if we need to thread (notes too long) HEADER_LENGTH=${#HEADER} - TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 10)) + TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 50)) if [ "$TOTAL_LENGTH" -le "$SAFE_LIMIT" ]; then - # Single message - notes fit + # Single message - full notes fit FULL_MESSAGE="${HEADER} - ${RELEASE_NOTES}" +${RELEASE_NOTES}" send_discord "$FULL_MESSAGE" "$DISCORD_CHANNEL_ID" else - # Need to thread - send header first, create thread, then post notes in thread + # Notes too long - include preview in main message, full notes in thread echo "Release notes exceed Discord limit (${TOTAL_LENGTH} chars), using thread..." - # Send header message - MESSAGE_ID=$(send_discord "${HEADER} + # Extract first section (usually Features) for preview + # Look for content up to the second ## header or take first ~800 chars + PREVIEW=$(echo "$RELEASE_NOTES" | awk ' + /^## / { if (seen++) exit } + { print } + ') + + # If preview is too long or empty, just take first ~800 chars at a newline + if [ ${#PREVIEW} -gt 800 ] || [ -z "$PREVIEW" ]; then + PREVIEW="${RELEASE_NOTES:0:800}" + # Break at last newline + LAST_NL=$(echo "$PREVIEW" | grep -bo $'\n' | tail -1 | cut -d: -f1 || echo "") + if [ -n "$LAST_NL" ] && [ "$LAST_NL" -gt 200 ]; then + PREVIEW="${PREVIEW:0:$LAST_NL}" + fi + fi + + # Build main message with preview + MAIN_MESSAGE="${HEADER} + +${PREVIEW} + +📝 *Full release notes in thread below...*" - 📝 *Full release notes in thread below*" "$DISCORD_CHANNEL_ID") + MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") if [ -z "$MESSAGE_ID" ]; then echo "::error::Failed to send Discord message" From d1cde06b221f4251ac71d999d15b5d6543608c9d Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 18:10:13 -0500 Subject: [PATCH 08/19] fix: correct YAML indentation in multiline strings --- .github/workflows/goose-release-notes.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 41fd64b57bf6..84a8fcaf392e 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -228,7 +228,7 @@ jobs: # Build the header HEADER="## 🎉 goose ${RELEASE_TAG} is here! -📦 **Download:** ${RELEASE_URL}" + 📦 **Download:** ${RELEASE_URL}" # Check if we need to thread (notes too long) HEADER_LENGTH=${#HEADER} @@ -238,7 +238,7 @@ jobs: # Single message - full notes fit FULL_MESSAGE="${HEADER} -${RELEASE_NOTES}" + ${RELEASE_NOTES}" send_discord "$FULL_MESSAGE" "$DISCORD_CHANNEL_ID" else # Notes too long - include preview in main message, full notes in thread @@ -264,9 +264,9 @@ ${RELEASE_NOTES}" # Build main message with preview MAIN_MESSAGE="${HEADER} -${PREVIEW} + ${PREVIEW} -📝 *Full release notes in thread below...*" + 📝 *Full release notes in thread below...*" MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") From 5bd6abdd54b265c577d4341d2291032d155b474f Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 18:17:12 -0500 Subject: [PATCH 09/19] fix: simplify Discord posting - header only in main msg, full notes in thread by section --- .github/workflows/goose-release-notes.yml | 94 ++++++++++++----------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 84a8fcaf392e..4e1fbc19981c 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -225,48 +225,29 @@ jobs: echo "$response" | jq -r '.id // empty' } - # Build the header + # Build the announcement header HEADER="## 🎉 goose ${RELEASE_TAG} is here! 📦 **Download:** ${RELEASE_URL}" - # Check if we need to thread (notes too long) + # Check total length to decide strategy HEADER_LENGTH=${#HEADER} - TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 50)) + TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 10)) if [ "$TOTAL_LENGTH" -le "$SAFE_LIMIT" ]; then - # Single message - full notes fit + # Single message - everything fits FULL_MESSAGE="${HEADER} ${RELEASE_NOTES}" send_discord "$FULL_MESSAGE" "$DISCORD_CHANNEL_ID" else - # Notes too long - include preview in main message, full notes in thread + # Notes too long - use header + thread with full notes echo "Release notes exceed Discord limit (${TOTAL_LENGTH} chars), using thread..." - # Extract first section (usually Features) for preview - # Look for content up to the second ## header or take first ~800 chars - PREVIEW=$(echo "$RELEASE_NOTES" | awk ' - /^## / { if (seen++) exit } - { print } - ') - - # If preview is too long or empty, just take first ~800 chars at a newline - if [ ${#PREVIEW} -gt 800 ] || [ -z "$PREVIEW" ]; then - PREVIEW="${RELEASE_NOTES:0:800}" - # Break at last newline - LAST_NL=$(echo "$PREVIEW" | grep -bo $'\n' | tail -1 | cut -d: -f1 || echo "") - if [ -n "$LAST_NL" ] && [ "$LAST_NL" -gt 200 ]; then - PREVIEW="${PREVIEW:0:$LAST_NL}" - fi - fi - - # Build main message with preview + # Main message is just the announcement header with pointer to thread MAIN_MESSAGE="${HEADER} - ${PREVIEW} - - 📝 *Full release notes in thread below...*" + 📝 *See full release notes in thread below*" MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") @@ -290,25 +271,52 @@ jobs: sleep 1 - # Post release notes in chunks to the thread - REMAINING="$RELEASE_NOTES" - - while [ -n "$REMAINING" ]; do - CHUNK="${REMAINING:0:$SAFE_LIMIT}" - REMAINING="${REMAINING:$SAFE_LIMIT}" - - # Try to break at a newline for cleaner splits - if [ -n "$REMAINING" ]; then - LAST_NEWLINE=$(echo "$CHUNK" | tail -c +1000 | grep -bo $'\n' | tail -1 | cut -d: -f1 || echo "") - if [ -n "$LAST_NEWLINE" ]; then - BREAK_POINT=$((1000 + LAST_NEWLINE)) - REMAINING="${CHUNK:$BREAK_POINT}${REMAINING}" - CHUNK="${CHUNK:0:$BREAK_POINT}" - fi + # Split notes into sections by ## headers, save to temp files + echo "$RELEASE_NOTES" | awk ' + BEGIN { n=0; file="/tmp/section_0.txt" } + /^## / { + if (n > 0) close(file) + n++ + file = "/tmp/section_" n ".txt" + } + { print > file } + END { close(file) } + ' + + # Post each section file to the thread + for section_file in /tmp/section_*.txt; do + [ -f "$section_file" ] || continue + SECTION_CONTENT=$(cat "$section_file") + + # Skip empty sections + [ -z "$SECTION_CONTENT" ] && continue + + # If section fits in one message, send it + if [ ${#SECTION_CONTENT} -le "$SAFE_LIMIT" ]; then + send_discord "$SECTION_CONTENT" "$THREAD_ID" + else + # Section too long, chunk it at line boundaries + REMAINING="$SECTION_CONTENT" + while [ -n "$REMAINING" ]; do + CHUNK="${REMAINING:0:$SAFE_LIMIT}" + REMAINING="${REMAINING:$SAFE_LIMIT}" + + # Break at last newline for clean splits + if [ -n "$REMAINING" ]; then + # Find last newline in chunk + BEFORE_LAST_NL="${CHUNK%$'\n'*}" + if [ "$BEFORE_LAST_NL" != "$CHUNK" ] && [ ${#BEFORE_LAST_NL} -gt 200 ]; then + REMAINING="${CHUNK:${#BEFORE_LAST_NL}}${REMAINING}" + CHUNK="$BEFORE_LAST_NL" + fi + fi + + send_discord "$CHUNK" "$THREAD_ID" + sleep 1 + done fi - - send_discord "$CHUNK" "$THREAD_ID" sleep 1 + rm -f "$section_file" done fi From c8ee0f47134b06720c4f9b125de2c44cbbe4d049 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 18:25:14 -0500 Subject: [PATCH 10/19] fix: show first section (Features) in main msg, rest in thread without repeating --- .github/workflows/goose-release-notes.yml | 140 +++++++++++++++------- 1 file changed, 95 insertions(+), 45 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 4e1fbc19981c..d30eb5abc082 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -230,24 +230,50 @@ jobs: 📦 **Download:** ${RELEASE_URL}" - # Check total length to decide strategy - HEADER_LENGTH=${#HEADER} - TOTAL_LENGTH=$((HEADER_LENGTH + NOTES_LENGTH + 10)) + # Split notes into sections by ## headers + # Section 0 = any content before first ##, Section 1+ = each ## section + rm -f /tmp/section_*.txt + echo "$RELEASE_NOTES" | awk ' + BEGIN { n=0; file="/tmp/section_0.txt" } + /^## / { + if (n > 0) close(file) + n++ + file = "/tmp/section_" n ".txt" + } + { print > file } + END { close(file) } + ' + + # Get first section (usually Features) + FIRST_SECTION="" + for f in /tmp/section_0.txt /tmp/section_1.txt; do + if [ -f "$f" ] && [ -s "$f" ]; then + FIRST_SECTION=$(cat "$f") + FIRST_SECTION_FILE="$f" + break + fi + done - if [ "$TOTAL_LENGTH" -le "$SAFE_LIMIT" ]; then - # Single message - everything fits - FULL_MESSAGE="${HEADER} + # Check if header + first section fits in one message + MAIN_CONTENT="${HEADER} - ${RELEASE_NOTES}" - send_discord "$FULL_MESSAGE" "$DISCORD_CHANNEL_ID" - else - # Notes too long - use header + thread with full notes - echo "Release notes exceed Discord limit (${TOTAL_LENGTH} chars), using thread..." + ${FIRST_SECTION}" + MAIN_LENGTH=${#MAIN_CONTENT} - # Main message is just the announcement header with pointer to thread - MAIN_MESSAGE="${HEADER} + # Count remaining sections + REMAINING_SECTIONS=0 + for f in /tmp/section_*.txt; do + [ -f "$f" ] && [ "$f" != "$FIRST_SECTION_FILE" ] && REMAINING_SECTIONS=$((REMAINING_SECTIONS + 1)) + done - 📝 *See full release notes in thread below*" + if [ "$MAIN_LENGTH" -le "$SAFE_LIMIT" ] && [ "$REMAINING_SECTIONS" -eq 0 ]; then + # Everything fits in one message + send_discord "$MAIN_CONTENT" "$DISCORD_CHANNEL_ID" + elif [ "$MAIN_LENGTH" -le "$SAFE_LIMIT" ]; then + # Header + first section fits, put rest in thread + MAIN_MESSAGE="${MAIN_CONTENT} + + 📝 *More in thread...*" MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") @@ -259,65 +285,89 @@ jobs: echo "Created message $MESSAGE_ID, creating thread..." sleep 1 - # Create thread from the message THREAD_ID=$(create_thread "$DISCORD_CHANNEL_ID" "$MESSAGE_ID" "Release Notes ${RELEASE_TAG}") - if [ -z "$THREAD_ID" ]; then - echo "::warning::Failed to create thread, posting notes as follow-up messages" + echo "::warning::Failed to create thread" THREAD_ID="$DISCORD_CHANNEL_ID" - else - echo "Created thread $THREAD_ID" fi - sleep 1 - # Split notes into sections by ## headers, save to temp files - echo "$RELEASE_NOTES" | awk ' - BEGIN { n=0; file="/tmp/section_0.txt" } - /^## / { - if (n > 0) close(file) - n++ - file = "/tmp/section_" n ".txt" - } - { print > file } - END { close(file) } - ' - - # Post each section file to the thread + # Post remaining sections to thread for section_file in /tmp/section_*.txt; do [ -f "$section_file" ] || continue + [ "$section_file" = "$FIRST_SECTION_FILE" ] && continue + SECTION_CONTENT=$(cat "$section_file") + [ -z "$SECTION_CONTENT" ] && continue - # Skip empty sections + if [ ${#SECTION_CONTENT} -le "$SAFE_LIMIT" ]; then + send_discord "$SECTION_CONTENT" "$THREAD_ID" + else + # Chunk large sections at line boundaries + REMAINING="$SECTION_CONTENT" + while [ -n "$REMAINING" ]; do + CHUNK="${REMAINING:0:$SAFE_LIMIT}" + REMAINING="${REMAINING:$SAFE_LIMIT}" + if [ -n "$REMAINING" ]; then + BEFORE_NL="${CHUNK%$'\n'*}" + if [ "$BEFORE_NL" != "$CHUNK" ] && [ ${#BEFORE_NL} -gt 200 ]; then + REMAINING="${CHUNK:${#BEFORE_NL}}${REMAINING}" + CHUNK="$BEFORE_NL" + fi + fi + send_discord "$CHUNK" "$THREAD_ID" + sleep 1 + done + fi + sleep 1 + done + else + # First section too long - just header in main, everything in thread + MAIN_MESSAGE="${HEADER} + + 📝 *See full release notes in thread below*" + + MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") + + if [ -z "$MESSAGE_ID" ]; then + echo "::error::Failed to send Discord message" + exit 1 + fi + + sleep 1 + THREAD_ID=$(create_thread "$DISCORD_CHANNEL_ID" "$MESSAGE_ID" "Release Notes ${RELEASE_TAG}") + [ -z "$THREAD_ID" ] && THREAD_ID="$DISCORD_CHANNEL_ID" + sleep 1 + + # Post all sections to thread + for section_file in /tmp/section_*.txt; do + [ -f "$section_file" ] || continue + SECTION_CONTENT=$(cat "$section_file") [ -z "$SECTION_CONTENT" ] && continue - # If section fits in one message, send it if [ ${#SECTION_CONTENT} -le "$SAFE_LIMIT" ]; then send_discord "$SECTION_CONTENT" "$THREAD_ID" else - # Section too long, chunk it at line boundaries REMAINING="$SECTION_CONTENT" while [ -n "$REMAINING" ]; do CHUNK="${REMAINING:0:$SAFE_LIMIT}" REMAINING="${REMAINING:$SAFE_LIMIT}" - - # Break at last newline for clean splits if [ -n "$REMAINING" ]; then - # Find last newline in chunk - BEFORE_LAST_NL="${CHUNK%$'\n'*}" - if [ "$BEFORE_LAST_NL" != "$CHUNK" ] && [ ${#BEFORE_LAST_NL} -gt 200 ]; then - REMAINING="${CHUNK:${#BEFORE_LAST_NL}}${REMAINING}" - CHUNK="$BEFORE_LAST_NL" + BEFORE_NL="${CHUNK%$'\n'*}" + if [ "$BEFORE_NL" != "$CHUNK" ] && [ ${#BEFORE_NL} -gt 200 ]; then + REMAINING="${CHUNK:${#BEFORE_NL}}${REMAINING}" + CHUNK="$BEFORE_NL" fi fi - send_discord "$CHUNK" "$THREAD_ID" sleep 1 done fi sleep 1 - rm -f "$section_file" done fi + # Cleanup + rm -f /tmp/section_*.txt + echo "::notice::Discord notification sent successfully" From ebe0df1d21f7ec0733754efded90934b8c87751f Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 18:45:28 -0500 Subject: [PATCH 11/19] fix: fit as many complete bullet points in main msg, continue rest in thread --- .github/workflows/goose-release-notes.yml | 207 +++++++++------------- 1 file changed, 85 insertions(+), 122 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index d30eb5abc082..66df8cb25543 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -230,144 +230,107 @@ jobs: 📦 **Download:** ${RELEASE_URL}" - # Split notes into sections by ## headers - # Section 0 = any content before first ##, Section 1+ = each ## section - rm -f /tmp/section_*.txt - echo "$RELEASE_NOTES" | awk ' - BEGIN { n=0; file="/tmp/section_0.txt" } - /^## / { - if (n > 0) close(file) - n++ - file = "/tmp/section_" n ".txt" - } - { print > file } - END { close(file) } - ' - - # Get first section (usually Features) - FIRST_SECTION="" - for f in /tmp/section_0.txt /tmp/section_1.txt; do - if [ -f "$f" ] && [ -s "$f" ]; then - FIRST_SECTION=$(cat "$f") - FIRST_SECTION_FILE="$f" - break - fi - done - - # Check if header + first section fits in one message - MAIN_CONTENT="${HEADER} + FOOTER=" - ${FIRST_SECTION}" - MAIN_LENGTH=${#MAIN_CONTENT} + 📝 *More in thread...*" + HEADER_LEN=${#HEADER} + FOOTER_LEN=${#FOOTER} + AVAILABLE=$((SAFE_LIMIT - HEADER_LEN - FOOTER_LEN - 10)) - # Count remaining sections - REMAINING_SECTIONS=0 - for f in /tmp/section_*.txt; do - [ -f "$f" ] && [ "$f" != "$FIRST_SECTION_FILE" ] && REMAINING_SECTIONS=$((REMAINING_SECTIONS + 1)) - done + # Check if everything fits in one message (no thread needed) + FULL_MSG="${HEADER} - if [ "$MAIN_LENGTH" -le "$SAFE_LIMIT" ] && [ "$REMAINING_SECTIONS" -eq 0 ]; then - # Everything fits in one message - send_discord "$MAIN_CONTENT" "$DISCORD_CHANNEL_ID" - elif [ "$MAIN_LENGTH" -le "$SAFE_LIMIT" ]; then - # Header + first section fits, put rest in thread - MAIN_MESSAGE="${MAIN_CONTENT} + ${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 - 📝 *More in thread...*" + # 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 - MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") + # 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) - if [ -z "$MESSAGE_ID" ]; then - echo "::error::Failed to send Discord message" - exit 1 - fi + # Build and send main message + MAIN_MESSAGE="${HEADER} - echo "Created message $MESSAGE_ID, creating thread..." - sleep 1 + ${MAIN_CONTENT} + ${FOOTER}" - 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 remaining sections to thread - for section_file in /tmp/section_*.txt; do - [ -f "$section_file" ] || continue - [ "$section_file" = "$FIRST_SECTION_FILE" ] && continue - - SECTION_CONTENT=$(cat "$section_file") - [ -z "$SECTION_CONTENT" ] && continue - - if [ ${#SECTION_CONTENT} -le "$SAFE_LIMIT" ]; then - send_discord "$SECTION_CONTENT" "$THREAD_ID" - else - # Chunk large sections at line boundaries - REMAINING="$SECTION_CONTENT" - while [ -n "$REMAINING" ]; do - CHUNK="${REMAINING:0:$SAFE_LIMIT}" - REMAINING="${REMAINING:$SAFE_LIMIT}" - if [ -n "$REMAINING" ]; then - BEFORE_NL="${CHUNK%$'\n'*}" - if [ "$BEFORE_NL" != "$CHUNK" ] && [ ${#BEFORE_NL} -gt 200 ]; then - REMAINING="${CHUNK:${#BEFORE_NL}}${REMAINING}" - CHUNK="$BEFORE_NL" - fi - fi - send_discord "$CHUNK" "$THREAD_ID" - sleep 1 - done - fi - sleep 1 - done - else - # First section too long - just header in main, everything in thread - MAIN_MESSAGE="${HEADER} + MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") - 📝 *See full release notes in thread below*" + if [ -z "$MESSAGE_ID" ]; then + echo "::error::Failed to send Discord message" + exit 1 + fi - MESSAGE_ID=$(send_discord "$MAIN_MESSAGE" "$DISCORD_CHANNEL_ID") + echo "Created message $MESSAGE_ID, creating thread..." + sleep 1 - if [ -z "$MESSAGE_ID" ]; then - echo "::error::Failed to send Discord message" - exit 1 - fi + # 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 - sleep 1 - THREAD_ID=$(create_thread "$DISCORD_CHANNEL_ID" "$MESSAGE_ID" "Release Notes ${RELEASE_TAG}") - [ -z "$THREAD_ID" ] && THREAD_ID="$DISCORD_CHANNEL_ID" - sleep 1 - - # Post all sections to thread - for section_file in /tmp/section_*.txt; do - [ -f "$section_file" ] || continue - SECTION_CONTENT=$(cat "$section_file") - [ -z "$SECTION_CONTENT" ] && continue - - if [ ${#SECTION_CONTENT} -le "$SAFE_LIMIT" ]; then - send_discord "$SECTION_CONTENT" "$THREAD_ID" - else - REMAINING="$SECTION_CONTENT" - while [ -n "$REMAINING" ]; do - CHUNK="${REMAINING:0:$SAFE_LIMIT}" - REMAINING="${REMAINING:$SAFE_LIMIT}" - if [ -n "$REMAINING" ]; then - BEFORE_NL="${CHUNK%$'\n'*}" - if [ "$BEFORE_NL" != "$CHUNK" ] && [ ${#BEFORE_NL} -gt 200 ]; then - REMAINING="${CHUNK:${#BEFORE_NL}}${REMAINING}" - CHUNK="$BEFORE_NL" - fi - fi - send_discord "$CHUNK" "$THREAD_ID" - sleep 1 - done - fi + # 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/section_*.txt + rm -f /tmp/release_notes_full.txt /tmp/main_content.txt /tmp/thread_content.txt echo "::notice::Discord notification sent successfully" From 4c32660a35abefe48b46de116afa5230f96aafbf Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 19:19:39 -0500 Subject: [PATCH 12/19] fix: add @everyone ping and change Download to Release --- .github/workflows/goose-release-notes.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 66df8cb25543..cc570509cad3 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -226,9 +226,11 @@ jobs: } # Build the announcement header - HEADER="## 🎉 goose ${RELEASE_TAG} is here! + HEADER="@everyone - 📦 **Download:** ${RELEASE_URL}" + ## 🎉 goose ${RELEASE_TAG} is here! + + 📦 **Release:** ${RELEASE_URL}" FOOTER=" From 7822f794be3b5a4fa78f6c86057349048b598826 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 22:44:21 -0500 Subject: [PATCH 13/19] feat: switch to Anthropic with claude-opus-4-5 model --- .github/workflows/goose-release-notes.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index cc570509cad3..3623220c7c62 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -6,14 +6,14 @@ # Trigger: When a GitHub release is published (after release.yml completes) # # Required Secrets: -# - OPENROUTER_API_KEY: API key for OpenRouter +# - ANTHROPIC_API_KEY: API key for Anthropic # # Optional Secrets (for Discord): # - DISCORD_BOT_TOKEN: Discord bot token for posting release announcements # # Optional Variables: -# - GOOSE_PROVIDER: LLM provider (default: openrouter) -# - GOOSE_MODEL: Model name (default: anthropic/claude-sonnet-4) +# - GOOSE_PROVIDER: LLM provider (default: anthropic) +# - GOOSE_MODEL: Model name (default: claude-opus-4-5) # - DISCORD_RELEASE_CHANNEL_ID: Discord channel ID for release announcements name: goose Release Notes Generator @@ -89,9 +89,9 @@ jobs: image: ghcr.io/block/goose:latest options: --user root env: - GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openrouter' }} - GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'anthropic/claude-sonnet-4' }} - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'claude-opus-4-5' }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} HOME: /tmp/goose-home outputs: From 958d9c10cf616a3bbef8501f8a75b37bccd901e8 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 22:45:44 -0500 Subject: [PATCH 14/19] feat: use RELEASE_BOT_ANTHROPIC_KEY secret --- .github/workflows/goose-release-notes.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 3623220c7c62..639a29936fa0 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -6,7 +6,7 @@ # Trigger: When a GitHub release is published (after release.yml completes) # # Required Secrets: -# - ANTHROPIC_API_KEY: API key for Anthropic +# - RELEASE_BOT_ANTHROPIC_KEY: API key for Anthropic # # Optional Secrets (for Discord): # - DISCORD_BOT_TOKEN: Discord bot token for posting release announcements @@ -91,7 +91,7 @@ jobs: env: GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'claude-opus-4-5' }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.RELEASE_BOT_ANTHROPIC_KEY }} HOME: /tmp/goose-home outputs: From 98f342583ce01b4b952886c25c715776c277de3f Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 23:06:03 -0500 Subject: [PATCH 15/19] feat: rename Discord secrets to DISCORD_RELEASE_BOT_TOKEN and DISCORD_RELEASE_BOT_CHANNEL_ID --- .github/workflows/goose-release-notes.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 639a29936fa0..6f316fa0713c 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -9,12 +9,12 @@ # - RELEASE_BOT_ANTHROPIC_KEY: API key for Anthropic # # Optional Secrets (for Discord): -# - DISCORD_BOT_TOKEN: Discord bot token for posting release announcements +# - DISCORD_RELEASE_BOT_TOKEN: Discord bot token for posting release announcements +# - DISCORD_RELEASE_BOT_CHANNEL_ID: Discord channel ID for release announcements # # Optional Variables: # - GOOSE_PROVIDER: LLM provider (default: anthropic) # - GOOSE_MODEL: Model name (default: claude-opus-4-5) -# - DISCORD_RELEASE_CHANNEL_ID: Discord channel ID for release announcements name: goose Release Notes Generator @@ -174,8 +174,8 @@ jobs: steps: - name: Post release announcement env: - DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} - DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_CHANNEL_ID }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_RELEASE_BOT_TOKEN }} + DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_BOT_CHANNEL_ID }} RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} RELEASE_URL: ${{ github.event.release.html_url || format('https://github.com/block/goose/releases/tag/{0}', inputs.tag) }} RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }} From 2e1f22c9dcc541ae1602781768e77a7ce071e2df Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 23:16:41 -0500 Subject: [PATCH 16/19] fix: move @everyone to before 'More in thread' footer --- .github/workflows/goose-release-notes.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 6f316fa0713c..ccb57cb75069 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -226,14 +226,13 @@ jobs: } # Build the announcement header - HEADER="@everyone - - ## 🎉 goose ${RELEASE_TAG} is here! + HEADER="## 🎉 goose ${RELEASE_TAG} is here! 📦 **Release:** ${RELEASE_URL}" FOOTER=" + @everyone 📝 *More in thread...*" HEADER_LEN=${#HEADER} FOOTER_LEN=${#FOOTER} From 720a6a1affe7063290083cca3c135368f2fb48fe Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 23:29:55 -0500 Subject: [PATCH 17/19] fix: use workflow_run trigger instead of release:published GitHub doesn't trigger workflows for events created by GITHUB_TOKEN (to prevent infinite loops). Since release.yml publishes releases with GITHUB_TOKEN, the release:published trigger would never fire. Changed to workflow_run trigger on the Release workflow, which does fire even when the original workflow used GITHUB_TOKEN. This follows the same pattern used by build-notify.yml in this repo. - Added condition to only run when Release workflow succeeds - Extract tag from workflow_run.head_branch - Pass tag through job outputs for downstream jobs - Keep workflow_dispatch for manual testing --- .github/workflows/goose-release-notes.yml | 42 +++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index ccb57cb75069..29e7d4a9c8ca 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -3,7 +3,9 @@ # 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. # -# Trigger: When a GitHub release is published (after release.yml completes) +# Trigger: When the Release workflow completes successfully +# Note: Uses workflow_run instead of release:published because GitHub doesn't trigger +# workflows for events created by GITHUB_TOKEN (to prevent infinite loops). # # Required Secrets: # - RELEASE_BOT_ANTHROPIC_KEY: API key for Anthropic @@ -19,8 +21,12 @@ name: goose Release Notes Generator on: - release: - types: [published] + # Trigger when Release workflow completes (works around GITHUB_TOKEN limitation) + workflow_run: + workflows: + - Release + types: + - completed # Allow manual trigger for testing workflow_dispatch: @@ -75,15 +81,20 @@ permissions: contents: write concurrency: - group: release-notes-${{ github.event.release.tag_name || inputs.tag }} + 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 - # Skip stable tag releases - they're just pointers to versioned releases - if: ${{ (github.event.release.tag_name || inputs.tag) != 'stable' }} + # 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 @@ -97,8 +108,19 @@ jobs: 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 + run: | + # For workflow_run, the tag is in head_branch; for workflow_dispatch, use input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "tag=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT + fi + - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -112,7 +134,7 @@ jobs: - name: Run goose to generate release notes env: - RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} + RELEASE_TAG: ${{ steps.get-tag.outputs.tag }} run: | mkdir -p $HOME/.local/share/goose/sessions mkdir -p $HOME/.config/goose @@ -159,7 +181,7 @@ jobs: - name: Update release body uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 with: - tag: ${{ github.event.release.tag_name || inputs.tag }} + tag: ${{ needs.generate-release-notes.outputs.release_tag }} token: ${{ secrets.GITHUB_TOKEN }} body: ${{ needs.generate-release-notes.outputs.release_notes }} allowUpdates: true @@ -176,8 +198,8 @@ jobs: env: DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_RELEASE_BOT_TOKEN }} DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_BOT_CHANNEL_ID }} - RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} - RELEASE_URL: ${{ github.event.release.html_url || format('https://github.com/block/goose/releases/tag/{0}', inputs.tag) }} + 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 From f388632b680ecba44592ef11ee33449787cf2f29 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 23:31:41 -0500 Subject: [PATCH 18/19] chore: remove verbose comments from header --- .github/workflows/goose-release-notes.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 29e7d4a9c8ca..a5c3234c8c46 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -3,20 +3,8 @@ # 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. # -# Trigger: When the Release workflow completes successfully -# Note: Uses workflow_run instead of release:published because GitHub doesn't trigger -# workflows for events created by GITHUB_TOKEN (to prevent infinite loops). -# -# Required Secrets: -# - RELEASE_BOT_ANTHROPIC_KEY: API key for Anthropic -# -# Optional Secrets (for Discord): -# - DISCORD_RELEASE_BOT_TOKEN: Discord bot token for posting release announcements -# - DISCORD_RELEASE_BOT_CHANNEL_ID: Discord channel ID for release announcements -# -# Optional Variables: -# - GOOSE_PROVIDER: LLM provider (default: anthropic) -# - GOOSE_MODEL: Model name (default: claude-opus-4-5) +# 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 From 5b02152f3d31d9f43bf9b084b137f96f27252c23 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Wed, 25 Feb 2026 23:36:28 -0500 Subject: [PATCH 19/19] fix: prevent shell injection via environment variables Pass GitHub context variables through env instead of direct interpolation to prevent potential shell injection attacks via crafted tag names. --- .github/workflows/goose-release-notes.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index a5c3234c8c46..4efc16daf360 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -101,12 +101,15 @@ jobs: 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: | - # For workflow_run, the tag is in head_branch; for workflow_dispatch, use input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT else - echo "tag=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT + echo "tag=$WORKFLOW_RUN_BRANCH" >> $GITHUB_OUTPUT fi - name: Checkout repository