mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
Added mandatory validation steps to release workflow: - Job 1: validate (hassfest + HACS) - runs before release - Job 2: lint (Ruff check + format) - runs before release - Job 3: sync-manifest - only runs if validation passes - Job 4: release-notes - only runs if all previous jobs pass This ensures no release is created if code doesn't pass validation. Fixed generate-release-notes script to suppress colored output in CI: - Added log_info() wrapper that sends output to stderr in CI - Keeps release notes clean (only markdown, no ANSI codes) - Local execution still shows colored output for better UX Impact: Release workflow now fails fast if validation fails. Release notes are clean without shell output artifacts.
367 lines
11 KiB
Bash
Executable file
367 lines
11 KiB
Bash
Executable file
#!/bin/sh
|
||
|
||
# script/generate-release-notes: Generate release notes from conventional commits
|
||
#
|
||
# This script generates release notes by parsing conventional commits between git tags.
|
||
# It supports multiple backends for generation:
|
||
#
|
||
# 1. GitHub Copilot (VS Code Copilot CLI) - Intelligent, context-aware
|
||
# 2. git-cliff - Fast, template-based Rust tool
|
||
# 3. Manual parsing - Simple grep/awk fallback
|
||
#
|
||
# Usage:
|
||
# ./scripts/generate-release-notes [FROM_TAG] [TO_TAG]
|
||
# ./scripts/generate-release-notes v1.0.0 v1.1.0
|
||
# ./scripts/generate-release-notes v1.0.0 HEAD
|
||
# ./scripts/generate-release-notes # Uses latest tag to HEAD
|
||
#
|
||
# Environment variables:
|
||
# RELEASE_NOTES_BACKEND - Force specific backend: copilot, git-cliff, manual
|
||
# USE_AI - Set to "false" to skip AI backends (for CI/CD)
|
||
|
||
set -e
|
||
|
||
cd "$(dirname "$0")/.."
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
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 "$@" >&2
|
||
}
|
||
else
|
||
# Local execution, show colored output
|
||
log_info() {
|
||
echo "$@"
|
||
}
|
||
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 "${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
|
||
COMMITS=$(git log --pretty=format:"%h | %s%n%b%n---" "${FROM_TAG}..${TO_TAG}")
|
||
|
||
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="Generate release notes in GitHub-flavored Markdown from these conventional commits.
|
||
|
||
**Commits between ${FROM_TAG} and ${TO_TAG}:**
|
||
|
||
${COMMITS}
|
||
|
||
**Format Requirements:**
|
||
1. Parse conventional commit format: type(scope): description
|
||
2. Group by type with emoji headings:
|
||
- 🎉 New Features (feat)
|
||
- 🐛 Bug Fixes (fix)
|
||
- 📚 Documentation (docs)
|
||
- <20> Dependencies (chore(deps))
|
||
- <20>🔧 Maintenance & Refactoring (refactor, chore)
|
||
- 🧪 Testing (test)
|
||
3. **Smart grouping**: If multiple commits address the same logical change or feature:
|
||
- Combine them into one release note entry
|
||
- Link all related commits: ([hash1](url1), [hash2](url2))
|
||
- Use a descriptive summary that captures the overall change
|
||
4. Extract 'Impact:' sections from commit bodies as user-friendly descriptions
|
||
5. Format each item as: - **scope**: Description ([hash](https://github.com/${GITHUB_REPO}/commit/hash))
|
||
6. Keep it concise, focus on user-visible changes
|
||
7. **Exclude from release notes**:
|
||
- Manifest.json version bumps: chore(release): bump version
|
||
- Development environment: feat/fix/chore(devcontainer), feat/fix/chore(vscode), feat/fix/chore(scripts), feat/fix/chore(dev-env)
|
||
- CI/CD infrastructure: feat/fix/chore/ci(ci), feat/fix/chore/ci(workflow), feat/fix/chore/ci(actions)
|
||
8. **Include in release notes**:
|
||
- Dependency updates: chore(deps) - these ARE relevant for users
|
||
- Any user-facing changes regardless of scope
|
||
9. Output ONLY the markdown, no explanations
|
||
|
||
**Examples of smart grouping:**
|
||
- Multiple commits fixing the same bug → One entry with all commits
|
||
- Commits adding a feature incrementally → One entry describing the complete feature
|
||
- Related refactoring commits → One entry summarizing the improvement
|
||
|
||
Generate the release notes now:"
|
||
|
||
# Save prompt to temp file for copilot
|
||
TEMP_PROMPT=$(mktemp)
|
||
echo "$PROMPT" > "$TEMP_PROMPT"
|
||
|
||
# Call copilot CLI (it will handle authentication interactively)
|
||
copilot < "$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}"
|
||
|
||
# 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}"
|
||
|
||
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
|
||
|
||
# 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"
|
||
}
|
||
|
||
# Execute based on backend
|
||
case "$BACKEND" in
|
||
copilot)
|
||
if ! command -v copilot >/dev/null 2>&1; then
|
||
echo "${RED}Error: GitHub Copilot CLI not found${NC}"
|
||
echo "Install: npm install -g @github/copilot"
|
||
echo "See: https://github.com/github/copilot-cli"
|
||
exit 1
|
||
fi
|
||
generate_with_copilot
|
||
;;
|
||
git-cliff)
|
||
if ! command -v git-cliff >/dev/null 2>&1; then
|
||
echo "${RED}Error: git-cliff not found${NC}"
|
||
echo "Install: https://git-cliff.org/docs/installation"
|
||
exit 1
|
||
fi
|
||
generate_with_gitcliff
|
||
;;
|
||
manual)
|
||
generate_with_manual
|
||
;;
|
||
*)
|
||
echo "${RED}Error: Unknown backend: ${BACKEND}${NC}"
|
||
echo "Valid backends: copilot, git-cliff, manual"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
log_info "${GREEN}==> Release notes generated successfully!${NC}"
|