Files
Pulse/scripts/generate-release-notes.sh
2025-11-13 09:10:43 +00:00

409 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Generate release notes using LLM analysis of git commits
# Usage: ./scripts/generate-release-notes.sh <version> [previous-tag]
set -euo pipefail
VERSION=${1:-}
PREVIOUS_TAG=${2:-}
if [ -z "$VERSION" ]; then
echo "Usage: $0 <version> [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 "No LLM API keys detected falling back to deterministic release notes."
LLM_PROVIDER="fallback"
fi
echo "Using LLM provider: ${LLM_PROVIDER}"
# Prepare prompt for LLM
read -r -d '' PROMPT <<EOF || true
You are generating release notes for Pulse v${VERSION}.
Pulse is a monitoring system for Proxmox VE and PBS with Docker container monitoring capabilities.
Analyze the following ${COMMIT_COUNT} git commits and generate professional release notes:
${COMMIT_LOG}
Generate release notes following this EXACT template format:
## What's Changed
### New Features
[List new features as bullet points, each starting with "**Feature name**:" followed by description. Do NOT include commit hashes or references.]
### Bug Fixes
[List bug fixes as bullet points, each starting with "**Component/area**:" followed by description. Include issue references like (#123) ONLY if the commit message explicitly mentions an issue number. Do NOT include commit hashes.]
### Improvements
[List improvements/enhancements as bullet points, each starting with "**Component/area**:" followed by description. Do NOT include commit hashes or references.]
### Breaking Changes
[List any breaking changes, or write "None" if there are none]
## Installation
**Quick Install (systemd / LXC / Proxmox VE):**
\`\`\`bash
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash
\`\`\`
**Docker:**
\`\`\`bash
docker pull rcourtman/pulse:v${VERSION}
docker stop pulse && docker rm pulse
docker run -d --name pulse \\
--restart unless-stopped \\
-p 7655:7655 -p 7656:7656 \\
-v /opt/pulse/data:/data \\
rcourtman/pulse:v${VERSION}
\`\`\`
**Manual Binary (amd64 example):**
\`\`\`bash
curl -LO https://github.com/rcourtman/Pulse/releases/download/v${VERSION}/pulse-v${VERSION}-linux-amd64.tar.gz
sudo systemctl stop pulse
sudo tar -xzf pulse-v${VERSION}-linux-amd64.tar.gz -C /usr/local/bin pulse
sudo systemctl start pulse
\`\`\`
**Helm:**
\`\`\`bash
helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart \\
--version ${VERSION} \\
--namespace pulse \\
--create-namespace
\`\`\`
## Downloads
- Universal tarball (auto-detects architecture): \`pulse-v${VERSION}.tar.gz\`
- Architecture-specific: \`amd64\`, \`arm64\`, \`armv7\`, \`armv6\`, \`386\`
- Host agent packages: macOS (amd64/arm64), Windows (amd64/arm64/386), Linux (amd64/arm64/armv7/armv6/386)
- Sensor proxy: Linux (amd64/arm64/armv7/armv6/386)
- Helm chart: \`pulse-${VERSION}.tgz\`
- SHA256 checksums: \`checksums.txt\`
## Notes
[Add 2-4 bullet points highlighting the most important changes, configuration notes, or upgrade considerations. Keep this section concise and actionable.]
Guidelines:
- Match the exact format and style of the template above
- Use bold for feature/component names followed by colon
- Do NOT include commit hashes - they clutter the release notes
- Only include issue references like (#123) if explicitly mentioned in commit messages
- Focus ONLY on user-visible changes - exclude all development/infrastructure changes
- EXCLUDE: CI/CD changes, release workflow improvements, build process changes, development tooling, testing infrastructure
- EXCLUDE: Anything about "release notes generation", "automated release", "validation scripts", "GitHub workflows"
- INCLUDE ONLY: Features users interact with, bug fixes users experience, performance improvements users notice
- Use clear, non-technical language
- Group related changes together logically
- If there are no breaking changes, write "None" in that section
- Keep the Notes section practical and actionable for end users
EOF
# Helper to call Anthropic (returns notes via stdout)
generate_with_anthropic() {
local response response_type content
response=$(curl -s https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-d @- <<JSON
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 2000,
"system": "You are a technical writer creating release notes. Be concise, clear, and focus on user-visible changes. Use proper markdown formatting.",
"messages": [
{
"role": "user",
"content": $(echo "$PROMPT" | jq -Rs .)
}
]
}
JSON
) || {
echo "Anthropic API request failed" >&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 @- <<JSON
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a technical writer creating release notes. Be concise, clear, and focus on user-visible changes. Use proper markdown formatting."
},
{
"role": "user",
"content": $(echo "$PROMPT" | jq -Rs .)
}
],
"temperature": 0.7,
"max_tokens": 2000
}
JSON
) || {
echo "OpenAI API request failed" >&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 raw_subjects=()
mapfile -t raw_subjects < <(echo "$COMMIT_LOG" | sed 's/^[0-9a-f]\+ //')
local subjects=()
local subject lower
for subject in "${raw_subjects[@]}"; do
lower=$(echo "$subject" | tr '[:upper:]' '[:lower:]')
if [[ "$lower" =~ (release[[:space:]-]?notes|release[[:space:]-]?workflow|workflow|github|ci|lint|docs|documentation|helm|auto-update|mock|dry[[:space:]-]?run|test|integration|build|validation|telemetry|release[[:space:]-]?assets|fallback|prepare) ]]; then
continue
fi
subjects+=("$subject")
done
if [ ${#subjects[@]} -eq 0 ]; then
subjects=("${raw_subjects[@]}")
fi
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 <<'EOQDock'
```bash
docker pull rcourtman/pulse:v${VERSION}
docker stop pulse && docker rm pulse
docker run -d --name pulse \
--restart unless-stopped \
-p 7655:7655 -p 7656:7656 \
-v /opt/pulse/data:/data \
rcourtman/pulse:v${VERSION}
```
EOQDock
echo ""
echo "**Manual Binary (amd64 example):**"
cat <<'EOQMan'
```bash
curl -LO https://github.com/rcourtman/Pulse/releases/download/v${VERSION}/pulse-v${VERSION}-linux-amd64.tar.gz
sudo systemctl stop pulse
sudo tar -xzf pulse-v${VERSION}-linux-amd64.tar.gz -C /usr/local/bin pulse
sudo systemctl start pulse
```
EOQMan
echo ""
echo "**Helm:**"
cat <<'EOQHelm'
```bash
helm upgrade --install pulse oci://ghcr.io/rcourtman/pulse-chart \
--version ${VERSION} \
--namespace pulse \
--create-namespace
```
EOQHelm
echo ""
echo "## Downloads"
echo "- Universal tarball (auto-detects architecture): \`pulse-v${VERSION}.tar.gz\`"
echo "- Architecture-specific: \`amd64\`, \`arm64\`, \`armv7\`, \`armv6\`, \`386\`"
echo "- Host agent packages: macOS, Windows, Linux"
echo "- Sensor proxy binaries: Linux (amd64/arm64/armv7/armv6/386)"
echo "- Helm chart: \`pulse-${VERSION}.tgz\`"
echo "- SHA256 checksums: \`checksums.txt\`"
echo ""
echo "## Notes"
local item
for item in "${notes[@]}"; do
echo "- ${item}"
done
}
}
# Call LLM API based on provider with graceful fallback
RELEASE_NOTES=""
if [ "$LLM_PROVIDER" = "anthropic" ]; then
if ! RELEASE_NOTES=$(generate_with_anthropic); then
if [ -n "${OPENAI_API_KEY:-}" ]; then
echo "Anthropic generation failed, falling back to OpenAI..." >&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