feat: add goose-powered release notes generator workflow (#7503)
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user