mirror of
https://github.com/block/goose.git
synced 2026-07-10 00:20:17 +00:00
352 lines
12 KiB
YAML
352 lines
12 KiB
YAML
# 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
|
|
branches-ignore:
|
|
- "release/**"
|
|
|
|
# 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/aaif-goose/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: release branch preflight builds are ignored by the trigger.
|
|
# 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/aaif-goose/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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.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/aaif-goose/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"
|