#!/usr/bin/env bash # Generate release notes with an agent that explores the actual repo history. # # Engine: headless Claude Code (`claude -p`, uses your Claude subscription — # no API key), with Codex CLI (`codex exec`, OpenAI subscription) as fallback. # The agent runs read-only git/gh commands itself instead of being fed # pre-chewed diff fragments, so nothing user-visible is missed by grep luck. # # Usage: ./scripts/generate-release-notes.sh [previous-tag] # # Contract: the release notes markdown is written to STDOUT (trigger-release.sh # captures it); all progress/diagnostics go to STDERR. SAVE_TO_FILE=1 also # writes release-notes-v.md. # # Env overrides: # RELEASE_NOTES_ENGINE=claude|codex force an engine (default: claude, codex fallback) # RELEASE_NOTES_MODEL= model for the claude engine (default: sonnet) set -euo pipefail VERSION=${1:-} PREVIOUS_TAG=${2:-} if [ -z "$VERSION" ]; then echo "Usage: $0 [previous-tag]" >&2 echo "Example: $0 6.1.0 v6.0.6" >&2 exit 1 fi cd "$(git rev-parse --show-toplevel)" if [ -z "$PREVIOUS_TAG" ]; then PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -z "$PREVIOUS_TAG" ]; then echo "No previous tag found, cannot generate diff-based release notes" >&2 exit 1 fi fi if ! git rev-parse -q --verify "${PREVIOUS_TAG}^{commit}" >/dev/null; then echo "Previous tag '${PREVIOUS_TAG}' does not exist" >&2 exit 1 fi echo "Generating release notes for v${VERSION} (changes since ${PREVIOUS_TAG})..." >&2 read -r -d '' PROMPT </dev/null || return 1 echo "Engine: claude (model: ${RELEASE_NOTES_MODEL:-sonnet})" >&2 scrub_env claude -p "$PROMPT" \ --model "${RELEASE_NOTES_MODEL:-sonnet}" \ --allowedTools \ "Bash(git log:*)" "Bash(git diff:*)" "Bash(git show:*)" \ "Bash(git describe:*)" "Bash(git tag:*)" "Bash(git rev-parse:*)" \ "Bash(gh issue view:*)" "Bash(gh pr view:*)" \ "Read" "Grep" "Glob" \ /dev/null || return 1 echo "Engine: codex" >&2 local out out=$(mktemp) # Session log goes to stderr; only the agent's final message is kept. if ! scrub_env codex exec --sandbox read-only -o "$out" "$PROMPT" >&2 &2 exit 1 } ;; *) if ! RELEASE_NOTES=$(generate_with_claude); then echo "Claude generation failed, trying Codex fallback..." >&2 RELEASE_NOTES=$(generate_with_codex) || { echo "Both engines failed (need 'claude' or 'codex' CLI, logged in)" >&2 exit 1 } fi ;; esac RELEASE_NOTES=$(printf '%s\n' "$RELEASE_NOTES" | clean_notes) if [ -z "$RELEASE_NOTES" ]; then echo "Error: release notes generation returned no '## v' section" >&2 exit 1 fi # Notes to stdout only — callers capture this. printf '%s\n' "$RELEASE_NOTES" if [ "${SAVE_TO_FILE:-}" = "1" ]; then OUTPUT_FILE="release-notes-v${VERSION}.md" printf '%s\n' "$RELEASE_NOTES" > "$OUTPUT_FILE" echo "Saved to ${OUTPUT_FILE}" >&2 fi