mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-04-09 09:03:40 +00:00
cliff.toml has trim=true which strips git-cliff's trailing newline. When written to GITHUB_OUTPUT via heredoc, the closing delimiter was appended to the last content line instead of its own line, causing "Matching delimiter not found" error. Added printf '\n' in the workflow and echo "" in generate-notes to guarantee a newline before the heredoc closing delimiter. Impact: Release workflow no longer fails when generating release notes.
676 lines
24 KiB
Bash
Executable file
676 lines
24 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# script/generate-notes: Generate release notes from conventional commits
|
|
#
|
|
# Parses conventional commits between git tags and generates formatted release
|
|
# notes. Supports multiple backends: GitHub Copilot CLI (AI), git-cliff
|
|
# (template), or manual grep/awk parsing. Can auto-update existing GitHub releases.
|
|
#
|
|
# Usage:
|
|
# ./scripts/release/generate-notes [FROM_TAG] [TO_TAG]
|
|
#
|
|
# Examples:
|
|
# ./scripts/release/generate-notes # Latest tag to HEAD
|
|
# ./scripts/release/generate-notes v1.0.0 v1.1.0
|
|
# ./scripts/release/generate-notes v1.0.0 HEAD
|
|
|
|
set -e
|
|
|
|
cd "$(dirname "$0")/../.."
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Detect if running in CI (suppress colored output to stdout)
|
|
if [[ -n $CI || -n $GITHUB_ACTIONS ]]; then
|
|
# In CI, send info messages to stderr to keep release notes clean
|
|
log_info() {
|
|
echo -e "$@" >&2
|
|
}
|
|
else
|
|
# Local execution, show colored output
|
|
log_info() {
|
|
echo -e "$@"
|
|
}
|
|
fi
|
|
|
|
# Configuration
|
|
BACKEND="${RELEASE_NOTES_BACKEND:-auto}"
|
|
USE_AI="${USE_AI:-true}"
|
|
GITHUB_REPO="${GITHUB_REPOSITORY:-jpawlowski/hass.tibber_prices}"
|
|
|
|
# Parse arguments
|
|
FROM_TAG="${1:-$(git describe --tags --abbrev=0 2>/dev/null || echo "")}"
|
|
TO_TAG="${2:-HEAD}"
|
|
|
|
if [[ -z $FROM_TAG ]]; then
|
|
echo -e "${RED}Error: No tags found in repository${NC}" >&2
|
|
echo "Usage: $0 [FROM_TAG] [TO_TAG]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_info "${BLUE}==> Generating release notes: ${FROM_TAG}..${TO_TAG}${NC}"
|
|
log_info ""
|
|
|
|
# Detect available backends
|
|
detect_backend() {
|
|
if [[ $BACKEND != auto ]]; then
|
|
echo "$BACKEND"
|
|
return
|
|
fi
|
|
|
|
# Skip AI in CI/CD or if disabled
|
|
if [[ $USE_AI == false || -n $CI || -n $GITHUB_ACTIONS ]]; then
|
|
if command -v git-cliff >/dev/null 2>&1; then
|
|
echo "git-cliff"
|
|
return
|
|
fi
|
|
echo "manual"
|
|
return
|
|
fi
|
|
|
|
# Check for GitHub Copilot CLI (AI-powered, best quality)
|
|
if command -v copilot >/dev/null 2>&1; then
|
|
echo "copilot"
|
|
return
|
|
fi
|
|
|
|
# Check for git-cliff (fast and reliable)
|
|
if command -v git-cliff >/dev/null 2>&1; then
|
|
echo "git-cliff"
|
|
return
|
|
fi
|
|
|
|
# Fallback to manual parsing
|
|
echo "manual"
|
|
}
|
|
|
|
BACKEND=$(detect_backend)
|
|
log_info "${GREEN}Using backend: ${BACKEND}${NC}"
|
|
log_info ""
|
|
|
|
# Backend: GitHub Copilot CLI (AI-powered)
|
|
generate_with_copilot() {
|
|
log_info "${BLUE}==> Generating with GitHub Copilot CLI (AI-powered)${NC}"
|
|
log_info "${YELLOW}Note: This will use one premium request from your monthly quota${NC}"
|
|
log_info ""
|
|
|
|
# Get commit log for the range with file statistics
|
|
# This helps the AI understand which commits touched which files
|
|
COMMITS=$(git log --pretty=format:"%h | %s%n%b%n" --stat --compact-summary "${FROM_TAG}..${TO_TAG}")
|
|
|
|
# Get code diff for user-facing files to give the AI real context about what changed.
|
|
# Limited to ~8KB to keep the prompt manageable.
|
|
DIFF_CONTEXT=$(git diff --unified=2 --diff-filter=AM \
|
|
-- "custom_components/tibber_prices/sensor/" \
|
|
-- "custom_components/tibber_prices/binary_sensor/" \
|
|
-- "custom_components/tibber_prices/config_flow_handlers/" \
|
|
-- "custom_components/tibber_prices/services/" \
|
|
-- "custom_components/tibber_prices/translations/" \
|
|
-- "custom_components/tibber_prices/number/" \
|
|
-- "custom_components/tibber_prices/switch/" \
|
|
"${FROM_TAG}..${TO_TAG}" 2>/dev/null | head -c 8000 || true)
|
|
|
|
if [[ -z $COMMITS ]]; then
|
|
log_info "${YELLOW}No commits found between ${FROM_TAG} and ${TO_TAG}${NC}"
|
|
exit 0
|
|
fi
|
|
|
|
# Create prompt for Copilot
|
|
PROMPT="You are writing a GitHub release announcement for a Home Assistant integration called **Tibber Prices**.
|
|
|
|
## YOUR AUDIENCE
|
|
Home Assistant users — people who automate their home and build dashboards. They are NOT developers.
|
|
They do not understand technical terms like: coordinator, entity, async, refactor, TypedDict, module,
|
|
class, iterator, cache, pool, BaseCalculator, interval_pool, or any Python/software engineering terminology.
|
|
|
|
## WHAT THIS INTEGRATION DOES
|
|
It fetches electricity prices from Tibber and provides sensors to Home Assistant, so users can:
|
|
- See the current electricity price on their dashboard
|
|
- Automate appliances to run during cheapest hours (dishwasher, washing machine, heat pump, EV charging)
|
|
- Get alerts when prices are unusually high or low
|
|
- Find the best time window in the day for flexible loads
|
|
|
|
## YOUR GOAL
|
|
Translate technical changes into real user benefits. Write as a product manager telling
|
|
users what they can now DO, what was BROKEN and is now FIXED, or why the integration
|
|
is now more RELIABLE.
|
|
|
|
## WRITING RULES — FOLLOW STRICTLY
|
|
|
|
**Rule 1 — No jargon, ever.**
|
|
NEVER use: coordinator, entity, async, class, module, package, cache invalidation, refactor, pool,
|
|
iterator, TypedDict, BaseCalculator, interval_pool, config entry, data class, state machine.
|
|
Always rephrase as a user experience.
|
|
|
|
**Rule 2 — Second person, present tense.**
|
|
Write: \"You can now see...\", \"Your automations will...\", \"The setup wizard now...\"
|
|
|
|
**Rule 3 — Bug fixes: describe the SYMPTOM, not the technical cause.**
|
|
❌ \"Fix timezone-aware datetime comparison at resolution boundary\"
|
|
✅ \"Fixed: Price data was missing or showed wrong values when switching to/from daylight saving time\"
|
|
❌ \"Restore missing while-loop increment preventing indefinite hang during period calculation\"
|
|
✅ \"Fixed: The integration could occasionally freeze when calculating the cheapest time windows\"
|
|
|
|
**Rule 4 — New features: lead with what the user can DO.**
|
|
❌ \"Add volatility calculator with BaseCalculator integration\"
|
|
✅ \"New sensors show how stable today's prices are — useful for deciding whether to run flexible appliances now or wait\"
|
|
|
|
**Rule 5 — Refactoring without user impact: omit completely.**
|
|
Only mention internal changes if they improve actual reliability or stability that users
|
|
would notice. If so, group them under \"⚡ More Reliable\" as a brief single line.
|
|
NEVER write a refactoring bullet per commit.
|
|
|
|
**Rule 6 — Combine related commits into one story.**
|
|
Do NOT list every commit as a separate bullet. Group related commits into a single
|
|
paragraph or bullet that tells the complete story. Use multiple commit links for the group.
|
|
|
|
**Rule 7 — Use Impact: sections.**
|
|
Commit bodies may contain an \"Impact:\" line — this is the user-facing description.
|
|
Prioritize it over the commit subject line.
|
|
|
|
**Rule 8 — Imagine explaining to your neighbor.**
|
|
Before writing each bullet, ask: \"Would my neighbor who uses Home Assistant to automate their
|
|
home understand this?\" If not, rephrase.
|
|
|
|
## OUTPUT FORMAT
|
|
|
|
Start with: # [Short, exciting title — max 6 words, NO version number]
|
|
|
|
Then sections using ### (H3 headings):
|
|
|
|
- ### 🎉 What's New
|
|
(New sensors, new config options, new automations possible — use this for feat commits with real user benefit)
|
|
|
|
- ### 🐛 Fixed
|
|
(Bugs users experienced — describe what was wrong, what is better now)
|
|
|
|
- ### ⚡ More Reliable
|
|
(Performance improvements OR refactoring that improves stability — only when users would notice)
|
|
|
|
- ### 📦 Updated Dependencies
|
|
(Dependency bumps — keep brief, one line per group)
|
|
|
|
Skip any section that has no content.
|
|
|
|
After the last section, ALWAYS end with this exact footer, no modifications:
|
|
|
|
---
|
|
|
|
If this release saved you some money on your electricity bill, a coffee would be much appreciated! ☕
|
|
|
|
[](https://www.buymeacoffee.com/jpawlowski)
|
|
|
|
## TITLE SELECTION
|
|
- Find the most user-impactful change: new sensors > bug fixes affecting data quality > reliability improvements > UI improvements > translations
|
|
- Ignore dev/CI/docs commits for the title
|
|
- Make it sound exciting: \"Smarter Cheap-Period Detection\", \"Reliable DST Price Data\", \"New Price Stability Sensors\"
|
|
- Example: if DST price data was wrong and now fixed → title = \"Reliable Prices Around Clock Changes\"
|
|
|
|
## WHAT TO INCLUDE vs SKIP
|
|
✅ Include: Changes to what sensors show, bugs fixing wrong data, config UI improvements,
|
|
automation reliability, dependency updates
|
|
❌ Skip: Script/CI/workflow changes, version bumps, pure code reorganization,
|
|
documentation commits, test changes
|
|
|
|
## FILE PATH GUIDE (for assessing importance)
|
|
- sensor/ = Price sensors users see → HIGH
|
|
- binary_sensor/ = On/off sensors for automations → HIGH
|
|
- config_flow_handlers/ or config_flow/ = Setup wizard users interact with → HIGH
|
|
- services/ = Actions users trigger → HIGH
|
|
- translations/ = Labels users see in the UI → MEDIUM
|
|
- number/ or switch/ = Controls users interact with → HIGH
|
|
- coordinator/ = Data fetching/processing (rarely user-visible unless fixing data bugs) → LOW-MEDIUM
|
|
- scripts/, .github/, docs/ = Developer-only → SKIP
|
|
|
|
---
|
|
|
|
**Commits to analyze (${FROM_TAG} → ${TO_TAG}):**
|
|
|
|
${COMMITS}
|
|
|
|
---
|
|
|
|
**Code diff for user-facing files (use this to understand WHAT actually changed — but always translate to user language):**
|
|
|
|
${DIFF_CONTEXT}
|
|
|
|
---
|
|
|
|
Output ONLY the release notes. Start directly with the # title.
|
|
End after the Buy Me A Coffee button. No meta-commentary, no explanations."
|
|
|
|
# Save prompt to temp file for copilot
|
|
TEMP_PROMPT=$(mktemp)
|
|
echo "$PROMPT" > "$TEMP_PROMPT"
|
|
|
|
# Use Claude Sonnet for better user-focused, plain-language release notes.
|
|
# Haiku tends to echo technical commit language; Sonnet better translates to user benefits.
|
|
# Can override with: COPILOT_MODEL=claude-haiku-4.5 ./scripts/release/generate-notes
|
|
COPILOT_MODEL="${COPILOT_MODEL:-claude-sonnet-4-6}"
|
|
|
|
# Call copilot CLI (it will handle authentication interactively)
|
|
copilot --model "$COPILOT_MODEL" < "$TEMP_PROMPT" || {
|
|
echo ""
|
|
log_info "${YELLOW}Warning: GitHub Copilot CLI failed or was not authenticated${NC}"
|
|
log_info "${YELLOW}Falling back to git-cliff${NC}"
|
|
rm -f "$TEMP_PROMPT"
|
|
if command -v git-cliff >/dev/null 2>&1; then
|
|
generate_with_gitcliff
|
|
else
|
|
generate_with_manual
|
|
fi
|
|
return
|
|
}
|
|
|
|
rm -f "$TEMP_PROMPT"
|
|
}
|
|
|
|
# Backend: git-cliff (template-based)
|
|
generate_with_gitcliff() {
|
|
log_info "${BLUE}==> Generating with git-cliff${NC}"
|
|
|
|
# Analyze commits to generate smart title
|
|
FROM_TAG_SHORT="${FROM_TAG}"
|
|
TO_TAG_SHORT="${TO_TAG}"
|
|
COMMITS_LOG=$(git log --pretty=format:"%s" "${FROM_TAG}..${TO_TAG}" 2>/dev/null || echo "")
|
|
|
|
# Count user-facing changes (exclude dev/ci/docs)
|
|
FEAT_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat\((sensors|binary_sensor|config_flow|services|coordinator|api)\):" || true)
|
|
FIX_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix\((sensors|binary_sensor|config_flow|services|coordinator|api|translations)\):" || true)
|
|
PERF_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf\(" || true)
|
|
|
|
# Generate intelligent title based on changes
|
|
if [ $PERF_COUNT -gt 0 ]; then
|
|
TITLE="# Performance & Reliability Improvements"
|
|
elif [ $FEAT_COUNT -gt 2 ]; then
|
|
TITLE="# New Features & Enhancements"
|
|
elif [ $FEAT_COUNT -gt 0 ] && [ $FIX_COUNT -gt 0 ]; then
|
|
TITLE="# New Features & Bug Fixes"
|
|
elif [ $FEAT_COUNT -gt 0 ]; then
|
|
TITLE="# New Features"
|
|
elif [ $FIX_COUNT -gt 2 ]; then
|
|
TITLE="# Bug Fixes & Improvements"
|
|
elif [ $FIX_COUNT -gt 0 ]; then
|
|
TITLE="# Bug Fixes"
|
|
else
|
|
TITLE="# Release Updates"
|
|
fi
|
|
|
|
echo "$TITLE"
|
|
echo ""
|
|
|
|
# Create temporary cliff.toml if not exists
|
|
if [ ! -f "cliff.toml" ]; then
|
|
cat > /tmp/cliff.toml <<'EOF'
|
|
[changelog]
|
|
header = ""
|
|
body = """
|
|
{% for group, commits in commits | group_by(attribute="group") %}
|
|
### {{ group | striptags | trim | upper_first }}
|
|
{% for commit in commits %}
|
|
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}\
|
|
{% if commit.breaking %} [**BREAKING**]{% endif %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
"""
|
|
trim = true
|
|
|
|
[git]
|
|
conventional_commits = true
|
|
filter_unconventional = false
|
|
split_commits = false
|
|
commit_parsers = [
|
|
{ message = "^feat", group = "🎉 New Features" },
|
|
{ message = "^fix", group = "🐛 Bug Fixes" },
|
|
{ message = "^doc", group = "📚 Documentation" },
|
|
{ message = "^perf", group = "⚡ Performance" },
|
|
{ message = "^refactor", group = "🔧 Maintenance & Refactoring" },
|
|
{ message = "^style", group = "🎨 Styling" },
|
|
{ message = "^test", group = "🧪 Testing" },
|
|
{ message = "^chore", group = "🔧 Maintenance & Refactoring" },
|
|
{ message = "^ci", group = "🔄 CI/CD" },
|
|
{ message = "^build", group = "📦 Build" },
|
|
]
|
|
EOF
|
|
CLIFF_CONFIG="/tmp/cliff.toml"
|
|
else
|
|
CLIFF_CONFIG="cliff.toml"
|
|
fi
|
|
|
|
git-cliff --config "$CLIFF_CONFIG" "${FROM_TAG}..${TO_TAG}"
|
|
echo "" # Ensure output ends with newline (cliff.toml trim=true removes trailing newline)
|
|
|
|
if [ "$CLIFF_CONFIG" = "/tmp/cliff.toml" ]; then
|
|
rm -f /tmp/cliff.toml
|
|
fi
|
|
}
|
|
|
|
# Backend: Manual parsing (fallback)
|
|
generate_with_manual() {
|
|
log_info "${BLUE}==> Generating with manual parsing${NC}"
|
|
echo ""
|
|
|
|
# Check if we have commits
|
|
if ! git log --oneline "${FROM_TAG}..${TO_TAG}" >/dev/null 2>&1; then
|
|
log_info "${YELLOW}No commits found between ${FROM_TAG} and ${TO_TAG}${NC}"
|
|
exit 0
|
|
fi
|
|
|
|
# Analyze commits to generate smart title
|
|
COMMITS_LOG=$(git log --pretty=format:"%s" "${FROM_TAG}..${TO_TAG}" 2>/dev/null || echo "")
|
|
|
|
# Count user-facing changes (exclude dev/ci/docs)
|
|
FEAT_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat\((sensors|binary_sensor|config_flow|services|coordinator|api)\):" || true)
|
|
FIX_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix\((sensors|binary_sensor|config_flow|services|coordinator|api|translations)\):" || true)
|
|
PERF_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf\(" || true)
|
|
|
|
# Generate intelligent title based on changes
|
|
if [ $PERF_COUNT -gt 0 ]; then
|
|
echo "# Performance & Reliability Improvements"
|
|
elif [ $FEAT_COUNT -gt 2 ]; then
|
|
echo "# New Features & Enhancements"
|
|
elif [ $FEAT_COUNT -gt 0 ] && [ $FIX_COUNT -gt 0 ]; then
|
|
echo "# New Features & Bug Fixes"
|
|
elif [ $FEAT_COUNT -gt 0 ]; then
|
|
echo "# New Features"
|
|
elif [ $FIX_COUNT -gt 2 ]; then
|
|
echo "# Bug Fixes & Improvements"
|
|
elif [ $FIX_COUNT -gt 0 ]; then
|
|
echo "# Bug Fixes"
|
|
else
|
|
echo "# Release Updates"
|
|
fi
|
|
echo ""
|
|
|
|
# Create temporary files for each category
|
|
TMPDIR=$(mktemp -d)
|
|
FEAT_FILE="${TMPDIR}/feat"
|
|
FIX_FILE="${TMPDIR}/fix"
|
|
DOCS_FILE="${TMPDIR}/docs"
|
|
REFACTOR_FILE="${TMPDIR}/refactor"
|
|
TEST_FILE="${TMPDIR}/test"
|
|
OTHER_FILE="${TMPDIR}/other"
|
|
|
|
touch "$FEAT_FILE" "$FIX_FILE" "$DOCS_FILE" "$REFACTOR_FILE" "$TEST_FILE" "$OTHER_FILE"
|
|
|
|
# Process commits
|
|
git log --pretty=format:"%h %s" "${FROM_TAG}..${TO_TAG}" | while read -r hash subject; do
|
|
# Simple type extraction (before colon)
|
|
TYPE=""
|
|
if echo "$subject" | grep -q '^feat'; then
|
|
TYPE="feat"
|
|
elif echo "$subject" | grep -q '^fix'; then
|
|
TYPE="fix"
|
|
elif echo "$subject" | grep -q '^docs'; then
|
|
TYPE="docs"
|
|
elif echo "$subject" | grep -q '^refactor\|^chore'; then
|
|
TYPE="refactor"
|
|
elif echo "$subject" | grep -q '^test'; then
|
|
TYPE="test"
|
|
else
|
|
TYPE="other"
|
|
fi
|
|
|
|
# Create markdown line
|
|
LINE="- ${subject} ([${hash}](https://github.com/jpawlowski/hass.tibber_prices/commit/${hash}))"
|
|
|
|
# Append to appropriate file
|
|
case "$TYPE" in
|
|
feat)
|
|
echo "$LINE" >> "$FEAT_FILE"
|
|
;;
|
|
fix)
|
|
echo "$LINE" >> "$FIX_FILE"
|
|
;;
|
|
docs)
|
|
echo "$LINE" >> "$DOCS_FILE"
|
|
;;
|
|
refactor)
|
|
echo "$LINE" >> "$REFACTOR_FILE"
|
|
;;
|
|
test)
|
|
echo "$LINE" >> "$TEST_FILE"
|
|
;;
|
|
*)
|
|
echo "$LINE" >> "$OTHER_FILE"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Output grouped by category
|
|
if [ -s "$FEAT_FILE" ]; then
|
|
echo "### 🎉 New Features"
|
|
echo ""
|
|
cat "$FEAT_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
if [ -s "$FIX_FILE" ]; then
|
|
echo "### 🐛 Bug Fixes"
|
|
echo ""
|
|
cat "$FIX_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
if [ -s "$DOCS_FILE" ]; then
|
|
echo "### 📚 Documentation"
|
|
echo ""
|
|
cat "$DOCS_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
if [ -s "$REFACTOR_FILE" ]; then
|
|
echo "### 🔧 Maintenance & Refactoring"
|
|
echo ""
|
|
cat "$REFACTOR_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
if [ -s "$TEST_FILE" ]; then
|
|
echo "### 🧪 Testing"
|
|
echo ""
|
|
cat "$TEST_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
if [ -s "$OTHER_FILE" ]; then
|
|
echo "### 📝 Other Changes"
|
|
echo ""
|
|
cat "$OTHER_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
# Cleanup
|
|
rm -rf "$TMPDIR"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Check if auto-update is possible (before generation)
|
|
# ============================================================================
|
|
|
|
AUTO_UPDATE_AVAILABLE=false
|
|
CURRENT_TITLE=""
|
|
CURRENT_URL=""
|
|
|
|
if [ -z "$CI" ] && [ -z "$GITHUB_ACTIONS" ] && command -v gh >/dev/null 2>&1 && [ "$TO_TAG" != "HEAD" ]; then
|
|
# Check if TO_TAG is a valid tag and release exists
|
|
if git rev-parse "$TO_TAG" >/dev/null 2>&1 && gh release view "$TO_TAG" >/dev/null 2>&1; then
|
|
AUTO_UPDATE_AVAILABLE=true
|
|
CURRENT_TITLE=$(gh release view "$TO_TAG" --json name --jq '.name' 2>/dev/null || echo "$TO_TAG")
|
|
CURRENT_URL=$(gh release view "$TO_TAG" --json url --jq '.url' 2>/dev/null || echo "")
|
|
fi
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Generate release notes (only once)
|
|
# ============================================================================
|
|
|
|
# Validate backend availability
|
|
case "$BACKEND" in
|
|
copilot)
|
|
if ! command -v copilot >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: GitHub Copilot CLI not found${NC}" >&2
|
|
echo "Install: npm install -g @github/copilot" >&2
|
|
echo "See: https://github.com/github/copilot-cli" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
git-cliff)
|
|
if ! command -v git-cliff >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: git-cliff not found${NC}" >&2
|
|
echo "Install: https://git-cliff.org/docs/installation" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Generate to temp file if auto-update is possible, else stdout
|
|
if [ "$AUTO_UPDATE_AVAILABLE" = "true" ]; then
|
|
TEMP_NOTES=$(mktemp)
|
|
case "$BACKEND" in
|
|
copilot)
|
|
generate_with_copilot > "$TEMP_NOTES"
|
|
;;
|
|
git-cliff)
|
|
generate_with_gitcliff > "$TEMP_NOTES"
|
|
;;
|
|
manual)
|
|
generate_with_manual > "$TEMP_NOTES"
|
|
;;
|
|
esac
|
|
else
|
|
# No auto-update, just output to stdout
|
|
case "$BACKEND" in
|
|
copilot)
|
|
generate_with_copilot
|
|
;;
|
|
git-cliff)
|
|
generate_with_gitcliff
|
|
;;
|
|
manual)
|
|
generate_with_manual
|
|
;;
|
|
esac
|
|
|
|
echo "" >&2
|
|
log_info "${GREEN}==> Release notes generated successfully!${NC}"
|
|
exit 0
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Auto-update existing GitHub release
|
|
# ============================================================================
|
|
|
|
# Extract title and body from generated notes
|
|
# Title is the first line starting with '# ' (H1)
|
|
# Body is everything after the first blank line following the title
|
|
EXTRACTED_TITLE=""
|
|
NOTES_BODY=""
|
|
|
|
# Find the first line that starts with '# ' (skip any metadata before it)
|
|
TITLE_LINE_NUM=$(grep -n '^# ' "$TEMP_NOTES" | head -n 1 | cut -d: -f1)
|
|
|
|
if [ -n "$TITLE_LINE_NUM" ]; then
|
|
# Extract title (remove '# ' prefix from that specific line)
|
|
FIRST_TITLE_LINE=$(sed -n "${TITLE_LINE_NUM}p" "$TEMP_NOTES")
|
|
EXTRACTED_TITLE=$(echo "$FIRST_TITLE_LINE" | sed 's/^# //')
|
|
# Body starts from the line after the title
|
|
BODY_START_LINE=$((TITLE_LINE_NUM + 1))
|
|
NOTES_BODY=$(tail -n +$BODY_START_LINE "$TEMP_NOTES" | sed '1{/^$/d;}')
|
|
else
|
|
# No H1 title found, use entire content as body
|
|
NOTES_BODY=$(cat "$TEMP_NOTES")
|
|
fi
|
|
|
|
# Generate final title for GitHub release (without version tag - HACS adds it automatically)
|
|
if [ -n "$EXTRACTED_TITLE" ]; then
|
|
# Use the extracted title from release notes (no version prefix)
|
|
RELEASE_TITLE="$EXTRACTED_TITLE"
|
|
else
|
|
# Fallback: Keep current title if it looks meaningful
|
|
# (more than just the tag itself)
|
|
RELEASE_TITLE="$CURRENT_TITLE"
|
|
if [ "$RELEASE_TITLE" = "$TO_TAG" ]; then
|
|
# Current title is just the tag, generate from commit analysis
|
|
# This is the git-cliff style fallback (simple but functional)
|
|
FEAT_COUNT=$(echo "$NOTES_BODY" | grep -c "^### 🎉 New Features" || true)
|
|
FIX_COUNT=$(echo "$NOTES_BODY" | grep -c "^### 🐛 Bug Fixes" || true)
|
|
DOCS_COUNT=$(echo "$NOTES_BODY" | grep -c "^### 📚 Documentation" || true)
|
|
|
|
if [ "$FEAT_COUNT" -gt 0 ] && [ "$FIX_COUNT" -gt 0 ]; then
|
|
RELEASE_TITLE="New Features & Bug Fixes"
|
|
elif [ "$FEAT_COUNT" -gt 0 ]; then
|
|
RELEASE_TITLE="New Features"
|
|
elif [ "$FIX_COUNT" -gt 0 ]; then
|
|
RELEASE_TITLE="Bug Fixes"
|
|
elif [ "$DOCS_COUNT" -gt 0 ]; then
|
|
RELEASE_TITLE="Documentation Updates"
|
|
else
|
|
RELEASE_TITLE="Release Updates"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Save body (without H1 title) to temp file for GitHub
|
|
TEMP_BODY=$(mktemp)
|
|
echo "$NOTES_BODY" > "$TEMP_BODY"
|
|
|
|
# Show the generated notes (with title for preview)
|
|
if [ -n "$EXTRACTED_TITLE" ]; then
|
|
echo "# $EXTRACTED_TITLE"
|
|
echo ""
|
|
fi
|
|
cat "$TEMP_BODY"
|
|
|
|
echo "" >&2
|
|
log_info "${GREEN}==> Release notes generated successfully!${NC}"
|
|
echo "" >&2
|
|
log_info "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
|
log_info "A GitHub release exists for ${CYAN}$TO_TAG${NC}"
|
|
log_info "Current title: ${CYAN}$CURRENT_TITLE${NC}"
|
|
log_info "New title: ${CYAN}$RELEASE_TITLE${NC}"
|
|
if [ -n "$CURRENT_URL" ]; then
|
|
log_info "URL: ${CYAN}$CURRENT_URL${NC}"
|
|
fi
|
|
log_info "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo "" >&2
|
|
|
|
printf "${YELLOW}Do you want to update the release notes on GitHub? [y/N]:${NC} " >&2
|
|
read -r UPDATE_RELEASE
|
|
|
|
if [ "$UPDATE_RELEASE" = "y" ] || [ "$UPDATE_RELEASE" = "Y" ]; then
|
|
log_info "Updating release $TO_TAG on GitHub..."
|
|
log_info "Title: ${CYAN}$RELEASE_TITLE${NC}"
|
|
|
|
# Update release with both title and body
|
|
if gh release edit "$TO_TAG" --title "$RELEASE_TITLE" --notes-file "$TEMP_BODY" 2>&1 >&2; then
|
|
echo "" >&2
|
|
log_info "${GREEN}✓ Release notes updated successfully!${NC}"
|
|
if [ -n "$CURRENT_URL" ]; then
|
|
log_info "View at: ${CYAN}$CURRENT_URL${NC}"
|
|
fi
|
|
else
|
|
echo "" >&2
|
|
log_info "${RED}✗ Failed to update release${NC}"
|
|
log_info "You can manually update with:"
|
|
echo -e " ${CYAN}gh release edit $TO_TAG --notes-file -${NC} < notes.md" >&2
|
|
fi
|
|
else
|
|
log_info "Skipped release update"
|
|
log_info "You can update manually later with:"
|
|
echo -e " ${CYAN}./scripts/release/generate-notes $FROM_TAG $TO_TAG | gh release edit $TO_TAG --notes-file -${NC}" >&2
|
|
fi
|
|
|
|
rm -f "$TEMP_NOTES" "$TEMP_BODY"
|
|
exit 0
|
|
fi
|
|
|
|
# If no auto-update, just show success message
|
|
echo ""
|
|
log_info "${GREEN}==> Release notes generated successfully!${NC}"
|