Skip to content
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 Feb 25, 2026
dc220ef
feat: add Discord bot integration for release announcements
blackgirlbytes Feb 25, 2026
3f5daff
fix: suppress Discord embeds for cleaner release announcements
blackgirlbytes Feb 25, 2026
acd5f7c
fix: use secrets for Discord channel ID instead of variables
blackgirlbytes Feb 25, 2026
ec67bad
fix: check Discord config inside script instead of job-level if
blackgirlbytes Feb 25, 2026
d0898bc
debug: add Discord API error logging
blackgirlbytes Feb 25, 2026
dd57a0e
feat: improve Discord message format with release notes preview
blackgirlbytes Feb 25, 2026
d1cde06
fix: correct YAML indentation in multiline strings
blackgirlbytes Feb 25, 2026
5bd6abd
fix: simplify Discord posting - header only in main msg, full notes i…
blackgirlbytes Feb 25, 2026
c8ee0f4
fix: show first section (Features) in main msg, rest in thread withou…
blackgirlbytes Feb 25, 2026
ebe0df1
fix: fit as many complete bullet points in main msg, continue rest in…
blackgirlbytes Feb 25, 2026
4c32660
fix: add @everyone ping and change Download to Release
blackgirlbytes Feb 26, 2026
7822f79
feat: switch to Anthropic with claude-opus-4-5 model
blackgirlbytes Feb 26, 2026
958d9c1
feat: use RELEASE_BOT_ANTHROPIC_KEY secret
blackgirlbytes Feb 26, 2026
98f3425
feat: rename Discord secrets to DISCORD_RELEASE_BOT_TOKEN and DISCORD…
blackgirlbytes Feb 26, 2026
2e1f22c
fix: move @everyone to before 'More in thread' footer
blackgirlbytes Feb 26, 2026
720a6a1
fix: use workflow_run trigger instead of release:published
blackgirlbytes Feb 26, 2026
f388632
chore: remove verbose comments from header
blackgirlbytes Feb 26, 2026
5b02152
fix: prevent shell injection via environment variables
blackgirlbytes Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 350 additions & 0 deletions .github/workflows/goose-release-notes.yml
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
Comment on lines +259 to +261
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail when single-message Discord post is rejected

In the short-message branch, the workflow calls send_discord and 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_discord emits a warning and returns an empty ID, but this path still exits 0, so release announcements can be silently dropped even though the job appears green.

Useful? React with 👍 / 👎.

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"
Loading