#!/usr/bin/env bash # Generate release notes using LLM analysis of git commits # Usage: ./scripts/generate-release-notes.sh [previous-tag] set -euo pipefail VERSION=${1:-} PREVIOUS_TAG=${2:-} if [ -z "$VERSION" ]; then echo "Usage: $0 [previous-tag]" echo "Example: $0 4.29.0 v4.28.0" exit 1 fi # Find previous tag if not specified 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, using all commits" PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD) fi fi echo "Generating release notes for v${VERSION}..." echo "Analyzing commits since ${PREVIOUS_TAG}..." # Get commit log COMMIT_LOG=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%h %s" --no-merges) if [ -z "$COMMIT_LOG" ]; then echo "No commits found since ${PREVIOUS_TAG}" exit 1 fi # Count commits COMMIT_COUNT=$(echo "$COMMIT_LOG" | wc -l) echo "Found ${COMMIT_COUNT} commits" # Generate release notes using LLM API # Supports both OpenAI and Anthropic Claude # Set either OPENAI_API_KEY or ANTHROPIC_API_KEY if [ -n "${ANTHROPIC_API_KEY:-}" ]; then LLM_PROVIDER="anthropic" elif [ -n "${OPENAI_API_KEY:-}" ]; then LLM_PROVIDER="openai" else echo "Error: Either OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable must be set" exit 1 fi echo "Using LLM provider: ${LLM_PROVIDER}" # Prepare prompt for LLM read -r -d '' PROMPT <&2 return 1 } response_type=$(echo "$response" | jq -r '.type // empty') if [ "$response_type" = "error" ]; then local message message=$(echo "$response" | jq -r '.error.message // "Unknown error"') echo "Anthropic API error: $message" >&2 return 1 fi content=$(echo "$response" | jq -r '.content[0].text // empty') if [ -z "$content" ] || [ "$content" = "null" ]; then echo "Anthropic API returned empty content: $response" >&2 return 1 fi printf '%s' "$content" } # Helper to call OpenAI (returns notes via stdout) generate_with_openai() { local response content error_msg response=$(curl -s https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${OPENAI_API_KEY}" \ -d @- <&2 return 1 } error_msg=$(echo "$response" | jq -r '.error.message? // empty') if [ -n "$error_msg" ]; then echo "OpenAI API error: $error_msg" >&2 return 1 fi content=$(echo "$response" | jq -r '.choices[0].message.content // empty') if [ -z "$content" ] || [ "$content" = "null" ]; then echo "OpenAI API returned empty content: $response" >&2 return 1 fi printf '%s' "$content" } # Pretty-print a section of notes (expects array name) print_section() { local ref="$1" declare -n arr_ref="$ref" if [ ${#arr_ref[@]} -eq 0 ]; then echo "None" return fi local item for item in "${arr_ref[@]}"; do echo "- ${item}" done } # Deterministic fallback when no LLM providers are available generate_fallback_release_notes() { local subjects=() mapfile -t subjects < <(echo "$COMMIT_LOG" | sed 's/^[0-9a-f]\+ //') local features=() local bugs=() local improvements=() local breakings=() local subject lower for subject in "${subjects[@]}"; do [ -z "$subject" ] && continue lower=$(echo "$subject" | tr '[:upper:]' '[:lower:]') if [[ "$lower" =~ (feat|feature|add|introduc|support|new) ]]; then features+=("$subject") elif [[ "$lower" =~ (fix|bug|issue|patch|regress|correct) ]]; then bugs+=("$subject") elif [[ "$lower" =~ (breaking|deprecat|remove|drop|incompatib) ]]; then breakings+=("$subject") else improvements+=("$subject") fi done if [ ${#improvements[@]} -eq 0 ] && [ ${#features[@]} -eq 0 ] && [ ${#bugs[@]} -eq 0 ]; then improvements=("${subjects[@]}") fi local notes=() for subject in "${subjects[@]}"; do notes+=("$subject") [ ${#notes[@]} -ge 3 ] && break done if [ ${#notes[@]} -eq 0 ]; then notes+=("Routine maintenance and dependency updates.") fi { echo "## What's Changed" echo "" echo "### New Features" print_section features echo "" echo "### Bug Fixes" print_section bugs echo "" echo "### Improvements" print_section improvements echo "" echo "### Breaking Changes" print_section breakings echo "" echo "## Installation" echo "" echo "**Quick Install (systemd / LXC / Proxmox VE):**" cat <<'EOQI' ```bash curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash ``` EOQI echo "" echo "**Docker:**" cat <&2 if ! RELEASE_NOTES=$(generate_with_openai); then echo "OpenAI fallback failed; generating heuristic release notes." >&2 RELEASE_NOTES=$(generate_fallback_release_notes) fi LLM_PROVIDER="openai" else echo "Anthropic generation failed and no OpenAI fallback is available; generating heuristic release notes." >&2 RELEASE_NOTES=$(generate_fallback_release_notes) fi fi else if ! RELEASE_NOTES=$(generate_with_openai); then echo "OpenAI generation failed; generating heuristic release notes." >&2 RELEASE_NOTES=$(generate_fallback_release_notes) fi fi if [ -z "$RELEASE_NOTES" ] || [ "$RELEASE_NOTES" = "null" ]; then echo "Error: Release notes generation returned empty content" >&2 exit 1 fi # Output release notes echo "" echo "Generated release notes:" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "$RELEASE_NOTES" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Optionally save to file if [ "${SAVE_TO_FILE:-}" = "1" ]; then OUTPUT_FILE="release-notes-v${VERSION}.md" echo "$RELEASE_NOTES" > "$OUTPUT_FILE" echo "" echo "Saved to: $OUTPUT_FILE" fi