mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
chore(scripts): improve release tooling with trailer filtering and Impact rendering
cliff.toml: - Extract Impact footer value from commit footers and use as release note text when present, falling back to scope+message format otherwise - Fix whitespace in body template (remove extra indentation) scripts/release/generate-notes: - Add RELEASE_NOTES_TRAILER_SKIP_FILTER to exclude commits marked with Release-Notes: skip, User-Impact: none, or Released-Bug: no trailers - Add RELEASE_NOTES_COMPACT_DIFF and RELEASE_NOTES_DIFF_MAX_BYTES to limit AI diff context size for faster, more focused prompts - Add RELEASE_NOTES_CLIFF_FILTER_PATHS to restrict cliff to user-facing paths only when generating AI-assisted notes - Add RELEASE_NOTES_CLIFF_SINGLE_RELEASE to pass --latest to cliff - Define USER_FACING_PATHS list for scoped AI diff context Release-Notes: skip User-Impact: none
This commit is contained in:
parent
6e990564b9
commit
32b080d178
2 changed files with 439 additions and 147 deletions
57
cliff.toml
57
cliff.toml
|
|
@ -5,13 +5,19 @@
|
|||
# Template for the changelog body
|
||||
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 %} \
|
||||
([{{ commit.id | truncate(length=7, end="") }}](https://github.com/jpawlowski/hass.tibber_prices/commit/{{ commit.id }}))
|
||||
{% endfor %}
|
||||
{% for group, commits in commits | group_by(attribute="group") -%}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits -%}
|
||||
{% set impact_text = "" -%}
|
||||
{% set footers = commit.footers | default(value=[]) -%}
|
||||
{% for footer in footers -%}
|
||||
{% if footer.token == "Impact" -%}
|
||||
{% set impact_text = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {% if impact_text %}{{ impact_text | trim | upper_first }}{% else %}{% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}{% endif %}{% if commit.breaking %} [**BREAKING**]{% endif %} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/jpawlowski/hass.tibber_prices/commit/{{ commit.id }}))
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
|
|
@ -25,7 +31,8 @@ trim = true
|
|||
[git]
|
||||
# Parse conventional commits
|
||||
conventional_commits = true
|
||||
# Include all commits (even non-conventional)
|
||||
# Keep unconventional commits in parsing pipeline; parser rules decide what to skip.
|
||||
# This avoids noisy parse-error warnings on older commit history.
|
||||
filter_unconventional = false
|
||||
split_commits = false
|
||||
|
||||
|
|
@ -33,22 +40,28 @@ split_commits = false
|
|||
commit_parsers = [
|
||||
# Skip manifest.json version bumps (release housekeeping)
|
||||
{ message = "^chore\\(release\\): bump version", skip = true },
|
||||
# Skip explicit revert commits; final net state should drive release notes
|
||||
{ message = "^revert", skip = true },
|
||||
# Skip development environment changes (not user-relevant)
|
||||
{ message = "^(feat|fix|chore|refactor)\\((devcontainer|vscode|scripts|dev-env|environment)\\):", skip = true },
|
||||
# Skip CI/CD infrastructure changes (not user-relevant)
|
||||
{ message = "^(feat|fix|chore|ci)\\((ci|workflow|actions|github-actions)\\):", skip = true },
|
||||
# Keep dependency updates - these ARE relevant for users
|
||||
{ message = "^chore\\(deps\\):", group = "📦 Dependencies" },
|
||||
# Regular commit types
|
||||
{ message = "^feat", group = "🎉 New Features" },
|
||||
{ message = "^fix", group = "🐛 Bug Fixes" },
|
||||
{ message = "^docs?", 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 = "^build", group = "📦 Build" },
|
||||
# Skip non-user-facing fix scopes
|
||||
{ message = "^fix\\((docs|lint|types|tests?|ci|workflow|scripts|devcontainer|vscode|build|release)\\):", skip = true },
|
||||
# User-facing categories aligned with AI output style
|
||||
{ message = "^feat", group = "🎉 What's New" },
|
||||
{ message = "^fix", group = "🐛 Fixed" },
|
||||
{ message = "^perf", group = "⚡ More Reliable" },
|
||||
{ message = "^chore\\(deps\\):", group = "📦 Updated Dependencies" },
|
||||
# Skip mostly developer-facing categories
|
||||
{ message = "^docs?", skip = true },
|
||||
{ message = "^refactor", skip = true },
|
||||
{ message = "^style", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^build", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
# Final fallback to avoid ungrouped commits
|
||||
{ message = ".*", skip = true },
|
||||
]
|
||||
|
||||
# Protect breaking changes
|
||||
|
|
@ -56,5 +69,5 @@ commit_preprocessors = [
|
|||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/jpawlowski/hass.tibber_prices/issues/${2}))" },
|
||||
]
|
||||
|
||||
# Filter out commits
|
||||
filter_commits = false
|
||||
# Apply commit parser filtering rules
|
||||
filter_commits = true
|
||||
|
|
|
|||
|
|
@ -43,6 +43,156 @@ fi
|
|||
BACKEND="${RELEASE_NOTES_BACKEND:-auto}"
|
||||
USE_AI="${USE_AI:-true}"
|
||||
GITHUB_REPO="${GITHUB_REPOSITORY:-jpawlowski/hass.tibber_prices}"
|
||||
RELEASE_NOTES_COMPACT_DIFF="${RELEASE_NOTES_COMPACT_DIFF:-true}"
|
||||
RELEASE_NOTES_DIFF_MAX_BYTES="${RELEASE_NOTES_DIFF_MAX_BYTES:-8000}"
|
||||
RELEASE_NOTES_CLIFF_FILTER_PATHS="${RELEASE_NOTES_CLIFF_FILTER_PATHS:-true}"
|
||||
RELEASE_NOTES_CLIFF_SINGLE_RELEASE="${RELEASE_NOTES_CLIFF_SINGLE_RELEASE:-true}"
|
||||
RELEASE_NOTES_TRAILER_SKIP_FILTER="${RELEASE_NOTES_TRAILER_SKIP_FILTER:-true}"
|
||||
|
||||
# Commits explicitly marked to be excluded from release notes.
|
||||
RELEASE_NOTES_SKIP_COMMITS=()
|
||||
|
||||
# User-facing paths for AI diff context
|
||||
USER_FACING_PATHS=(
|
||||
"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/"
|
||||
)
|
||||
|
||||
# Parse common truthy values from environment variables
|
||||
is_truthy() {
|
||||
case "${1,,}" in
|
||||
1 | true | yes | on)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Determine if commit body explicitly marks commit as non-user-facing for release notes.
|
||||
# Supported trailers (case-insensitive):
|
||||
# - Release-Notes: skip
|
||||
# - User-Impact: none
|
||||
# - Released-Bug: no
|
||||
commit_marked_for_release_notes_skip() {
|
||||
local commit_hash="$1"
|
||||
local body
|
||||
|
||||
body=$(git show -s --format=%B "$commit_hash" 2>/dev/null || true)
|
||||
|
||||
if printf "%s\n" "$body" | grep -Eiq '^[[:space:]]*release[ -]notes:[[:space:]]*skip([[:space:]]|$)'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if printf "%s\n" "$body" | grep -Eiq '^[[:space:]]*user-impact:[[:space:]]*none([[:space:]]|$)'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if printf "%s\n" "$body" | grep -Eiq '^[[:space:]]*released-bug:[[:space:]]*no([[:space:]]|$)'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
collect_release_notes_skip_commits() {
|
||||
RELEASE_NOTES_SKIP_COMMITS=()
|
||||
|
||||
if ! is_truthy "$RELEASE_NOTES_TRAILER_SKIP_FILTER"; then
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r commit_hash; do
|
||||
if commit_marked_for_release_notes_skip "$commit_hash"; then
|
||||
RELEASE_NOTES_SKIP_COMMITS+=("$commit_hash")
|
||||
fi
|
||||
done < <(git rev-list --reverse "${FROM_TAG}..${TO_TAG}")
|
||||
}
|
||||
|
||||
is_release_notes_skipped_commit() {
|
||||
local target_hash="$1"
|
||||
local skip_hash
|
||||
|
||||
for skip_hash in "${RELEASE_NOTES_SKIP_COMMITS[@]}"; do
|
||||
if [[ "$skip_hash" == "$target_hash" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
build_copilot_commits_context() {
|
||||
local commit_hash=""
|
||||
local output=""
|
||||
local subject=""
|
||||
local body=""
|
||||
local short_hash=""
|
||||
local stat=""
|
||||
|
||||
while IFS= read -r commit_hash; do
|
||||
if is_release_notes_skipped_commit "$commit_hash"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
short_hash=$(git rev-parse --short "$commit_hash")
|
||||
subject=$(git show -s --format=%s "$commit_hash")
|
||||
body=$(git show -s --format=%b "$commit_hash")
|
||||
stat=$(git show --stat --compact-summary --format='' "$commit_hash" 2>/dev/null || true)
|
||||
|
||||
output+="${short_hash} | ${subject}"$'\n'
|
||||
if [[ -n "$body" ]]; then
|
||||
output+="${body}"$'\n'
|
||||
fi
|
||||
if [[ -n "$stat" ]]; then
|
||||
output+="${stat}"$'\n'
|
||||
fi
|
||||
output+=$'\n'
|
||||
done < <(git rev-list --reverse "${FROM_TAG}..${TO_TAG}")
|
||||
|
||||
printf "%s" "$output"
|
||||
}
|
||||
|
||||
# Build AI diff context with compact formatting by default to save tokens.
|
||||
# Set RELEASE_NOTES_COMPACT_DIFF=false to use the legacy full patch context format.
|
||||
build_diff_context() {
|
||||
local diff_context=""
|
||||
|
||||
if is_truthy "$RELEASE_NOTES_COMPACT_DIFF"; then
|
||||
if ! diff_context=$(
|
||||
set -o pipefail
|
||||
git diff --unified=0 --no-color --minimal --patience --diff-filter=AM \
|
||||
"${FROM_TAG}..${TO_TAG}" -- "${USER_FACING_PATHS[@]}" 2>/dev/null \
|
||||
| awk '
|
||||
/^diff --git / { print; next }
|
||||
/^--- / || /^\+\+\+ / || /^@@ / { print; next }
|
||||
/^[+-]/ {
|
||||
sub(/[ \t]+$/, "", $0)
|
||||
print
|
||||
next
|
||||
}
|
||||
' \
|
||||
| head -c "$RELEASE_NOTES_DIFF_MAX_BYTES"
|
||||
); then
|
||||
log_info "${YELLOW}Warning: compact diff generation failed, falling back to legacy diff context${NC}"
|
||||
diff_context=$(git diff --unified=2 --diff-filter=AM \
|
||||
"${FROM_TAG}..${TO_TAG}" -- "${USER_FACING_PATHS[@]}" 2>/dev/null \
|
||||
| head -c "$RELEASE_NOTES_DIFF_MAX_BYTES" || true)
|
||||
fi
|
||||
else
|
||||
diff_context=$(git diff --unified=2 --diff-filter=AM \
|
||||
"${FROM_TAG}..${TO_TAG}" -- "${USER_FACING_PATHS[@]}" 2>/dev/null \
|
||||
| head -c "$RELEASE_NOTES_DIFF_MAX_BYTES" || true)
|
||||
fi
|
||||
|
||||
echo "$diff_context"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
FROM_TAG="${1:-$(git describe --tags --abbrev=0 2>/dev/null || echo "")}"
|
||||
|
|
@ -54,6 +204,11 @@ if [[ -z $FROM_TAG ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
collect_release_notes_skip_commits
|
||||
if [[ ${#RELEASE_NOTES_SKIP_COMMITS[@]} -gt 0 ]]; then
|
||||
log_info "${YELLOW}Skipping ${#RELEASE_NOTES_SKIP_COMMITS[@]} commit(s) marked as non-user-facing via trailers${NC}"
|
||||
fi
|
||||
|
||||
log_info "${BLUE}==> Generating release notes: ${FROM_TAG}..${TO_TAG}${NC}"
|
||||
log_info ""
|
||||
|
||||
|
|
@ -100,21 +255,36 @@ generate_with_copilot() {
|
|||
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 filtered commit log for the range with file statistics (oldest -> newest).
|
||||
# Commits explicitly marked as non-user-facing are excluded.
|
||||
COMMITS=$(build_copilot_commits_context)
|
||||
|
||||
# Final repository-level changed file list (net state from FROM_TAG to TO_TAG).
|
||||
# This is useful when commit history contains back-and-forth changes.
|
||||
FINAL_FILE_CHANGES=$(git diff --name-status "${FROM_TAG}..${TO_TAG}" 2>/dev/null || true)
|
||||
|
||||
# Revert commits are useful chronology hints (earlier changes may be superseded).
|
||||
REVERT_COMMITS=$(git log --reverse --pretty=format:"%h | %s" "${FROM_TAG}..${TO_TAG}" \
|
||||
| grep -Ei '\|[[:space:]]*revert\b' || true)
|
||||
|
||||
if [[ -z $FINAL_FILE_CHANGES ]]; then
|
||||
FINAL_FILE_CHANGES="(none)"
|
||||
fi
|
||||
|
||||
if [[ -z $REVERT_COMMITS ]]; then
|
||||
REVERT_COMMITS="(none)"
|
||||
fi
|
||||
|
||||
if [[ ${#RELEASE_NOTES_SKIP_COMMITS[@]} -gt 0 ]]; then
|
||||
SKIPPED_COMMITS_CONTEXT=$(printf "%s\n" "${RELEASE_NOTES_SKIP_COMMITS[@]}" | xargs -I{} git show -s --format='%h | %s' {} 2>/dev/null || true)
|
||||
else
|
||||
SKIPPED_COMMITS_CONTEXT="(none)"
|
||||
fi
|
||||
|
||||
# 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)
|
||||
# Compact mode is enabled by default to reduce prompt tokens without reducing semantic signal.
|
||||
# Toggle: RELEASE_NOTES_COMPACT_DIFF=false
|
||||
DIFF_CONTEXT=$(build_diff_context)
|
||||
|
||||
if [[ -z $COMMITS ]]; then
|
||||
log_info "${YELLOW}No commits found between ${FROM_TAG} and ${TO_TAG}${NC}"
|
||||
|
|
@ -141,6 +311,13 @@ Translate technical changes into real user benefits. Write as a product manager
|
|||
users what they can now DO, what was BROKEN and is now FIXED, or why the integration
|
||||
is now more RELIABLE.
|
||||
|
||||
## SOURCE OF TRUTH (IMPORTANT)
|
||||
- Primary source: final net repository state from ${FROM_TAG} to ${TO_TAG}
|
||||
- Use commit text as context, not as absolute truth
|
||||
- If commit text conflicts with final file list or final diff, trust final state
|
||||
- Revert commits indicate earlier changes may be superseded
|
||||
- Commits listed as "explicitly skipped" are non-user-facing by author intent and must not be mentioned
|
||||
|
||||
## WRITING RULES — FOLLOW STRICTLY
|
||||
|
||||
**Rule 1 — No jargon, ever.**
|
||||
|
|
@ -236,6 +413,24 @@ ${COMMITS}
|
|||
|
||||
---
|
||||
|
||||
**Final changed files (net state, source of truth):**
|
||||
|
||||
${FINAL_FILE_CHANGES}
|
||||
|
||||
---
|
||||
|
||||
**Revert commits in range (chronology hints):**
|
||||
|
||||
${REVERT_COMMITS}
|
||||
|
||||
---
|
||||
|
||||
**Commits explicitly skipped by trailer (do not mention):**
|
||||
|
||||
${SKIPPED_COMMITS_CONTEXT}
|
||||
|
||||
---
|
||||
|
||||
**Code diff for user-facing files (use this to understand WHAT actually changed — but always translate to user language):**
|
||||
|
||||
${DIFF_CONTEXT}
|
||||
|
|
@ -275,29 +470,28 @@ End after the Buy Me A Coffee button. No meta-commentary, no explanations."
|
|||
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}"
|
||||
# Analyze commits to generate a title aligned with current user-facing groups
|
||||
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)
|
||||
WHATS_NEW_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat(\(|:)" || true)
|
||||
FIXED_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix(\(|:)" || true)
|
||||
RELIABLE_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf(\(|:)" || true)
|
||||
DEPS_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^chore\(deps\):" || 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"
|
||||
if [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$FIXED_COUNT" -gt 0 ]; then
|
||||
TITLE="# What's New & Fixed"
|
||||
elif [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
TITLE="# What's New & More Reliable"
|
||||
elif [ "$WHATS_NEW_COUNT" -gt 0 ]; then
|
||||
TITLE="# What's New"
|
||||
elif [ "$FIXED_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
TITLE="# Fixed & More Reliable"
|
||||
elif [ "$FIXED_COUNT" -gt 0 ]; then
|
||||
TITLE="# Fixed"
|
||||
elif [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
TITLE="# More Reliable"
|
||||
elif [ "$DEPS_COUNT" -gt 0 ]; then
|
||||
TITLE="# Dependency Updates"
|
||||
else
|
||||
TITLE="# Release Updates"
|
||||
fi
|
||||
|
|
@ -326,24 +520,114 @@ 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" },
|
||||
{ message = "^chore\\(release\\): bump version", skip = true },
|
||||
{ message = "^revert", skip = true },
|
||||
{ message = "^(feat|fix|chore|refactor)\\((devcontainer|vscode|scripts|dev-env|environment)\\):", skip = true },
|
||||
{ message = "^(feat|fix|chore|ci)\\((ci|workflow|actions|github-actions)\\):", skip = true },
|
||||
{ message = "^fix\\((docs|lint|types|tests?|ci|workflow|scripts|devcontainer|vscode|build|release)\\):", skip = true },
|
||||
{ message = "^feat", group = "🎉 What's New" },
|
||||
{ message = "^fix", group = "🐛 Fixed" },
|
||||
{ message = "^perf", group = "⚡ More Reliable" },
|
||||
{ message = "^chore\\(deps\\):", group = "📦 Updated Dependencies" },
|
||||
{ message = "^docs?", skip = true },
|
||||
{ message = "^refactor", skip = true },
|
||||
{ message = "^style", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^build", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = ".*", skip = true },
|
||||
]
|
||||
filter_commits = true
|
||||
EOF
|
||||
CLIFF_CONFIG="/tmp/cliff.toml"
|
||||
else
|
||||
CLIFF_CONFIG="cliff.toml"
|
||||
fi
|
||||
|
||||
git-cliff --config "$CLIFF_CONFIG" "${FROM_TAG}..${TO_TAG}"
|
||||
CLIFF_CMD=(git-cliff --config "$CLIFF_CONFIG")
|
||||
|
||||
# Restrict changelog to user-facing integration files by default.
|
||||
# Toggle with: RELEASE_NOTES_CLIFF_FILTER_PATHS=false
|
||||
if is_truthy "$RELEASE_NOTES_CLIFF_FILTER_PATHS"; then
|
||||
CLIFF_CMD+=(--include-path "custom_components/tibber_prices/**")
|
||||
fi
|
||||
|
||||
# Exclude commits explicitly marked as non-user-facing.
|
||||
if [[ ${#RELEASE_NOTES_SKIP_COMMITS[@]} -gt 0 ]]; then
|
||||
for skip_hash in "${RELEASE_NOTES_SKIP_COMMITS[@]}"; do
|
||||
CLIFF_CMD+=(--skip-commit "$skip_hash")
|
||||
done
|
||||
fi
|
||||
|
||||
CLIFF_CMD+=("${FROM_TAG}..${TO_TAG}")
|
||||
|
||||
CLIFF_OUTPUT=$("${CLIFF_CMD[@]}")
|
||||
|
||||
# In unusual ranges, git-cliff can emit multiple release blocks.
|
||||
# Keep only the first block by default to match this script's single-release intent.
|
||||
# Toggle with: RELEASE_NOTES_CLIFF_SINGLE_RELEASE=false
|
||||
if is_truthy "$RELEASE_NOTES_CLIFF_SINGLE_RELEASE"; then
|
||||
BMAC_COUNT=$(printf "%s" "$CLIFF_OUTPUT" | grep -c "buymeacoffee.com/jpawlowski" || true)
|
||||
if [[ $BMAC_COUNT -gt 1 ]]; then
|
||||
log_info "${YELLOW}Notice: Multiple release blocks detected; keeping the first block${NC}"
|
||||
CLIFF_OUTPUT=$(printf "%s" "$CLIFF_OUTPUT" | awk '
|
||||
/https:\/\/www\.buymeacoffee\.com\/jpawlowski/ {
|
||||
line = $0
|
||||
marker = "https://www.buymeacoffee.com/jpawlowski)"
|
||||
marker_pos = index(line, marker)
|
||||
if (marker_pos > 0) {
|
||||
line = substr(line, 1, marker_pos + length(marker) - 1)
|
||||
}
|
||||
print line
|
||||
exit
|
||||
}
|
||||
{ print }
|
||||
')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalize spacing from template edge-cases:
|
||||
# - remove blank lines between consecutive bullet points
|
||||
# - collapse multiple blank lines to a single blank line
|
||||
CLIFF_OUTPUT=$(printf "%s" "$CLIFF_OUTPUT" | awk '
|
||||
{
|
||||
lines[NR] = $0
|
||||
}
|
||||
END {
|
||||
blank_streak = 0
|
||||
for (i = 1; i <= NR; i++) {
|
||||
line = lines[i]
|
||||
next_line = (i < NR) ? lines[i + 1] : ""
|
||||
|
||||
if (line ~ /^[[:space:]]*$/) {
|
||||
prev_line = (out_count > 0) ? out[out_count] : ""
|
||||
prev_is_bullet = prev_line ~ /^- /
|
||||
next_is_bullet = next_line ~ /^- /
|
||||
|
||||
# Skip blank separators between bullet items
|
||||
if (prev_is_bullet && next_is_bullet) {
|
||||
continue
|
||||
}
|
||||
|
||||
blank_streak++
|
||||
if (blank_streak > 1) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
blank_streak = 0
|
||||
}
|
||||
|
||||
out_count++
|
||||
out[out_count] = line
|
||||
}
|
||||
|
||||
for (i = 1; i <= out_count; i++) {
|
||||
print out[i]
|
||||
}
|
||||
}
|
||||
')
|
||||
|
||||
printf "%s\n" "$CLIFF_OUTPUT"
|
||||
echo "" # Ensure output ends with newline (cliff.toml trim=true removes trailing newline)
|
||||
|
||||
if [ "$CLIFF_CONFIG" = "/tmp/cliff.toml" ]; then
|
||||
|
|
@ -362,27 +646,28 @@ generate_with_manual() {
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# Analyze commits to generate smart title
|
||||
# Analyze commits to generate title aligned with user-facing groups
|
||||
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)
|
||||
WHATS_NEW_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat(\(|:)" || true)
|
||||
FIXED_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix(\(|:)" || true)
|
||||
RELIABLE_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf(\(|:)" || true)
|
||||
DEPS_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^chore\(deps\):" || 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"
|
||||
if [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$FIXED_COUNT" -gt 0 ]; then
|
||||
echo "# What's New & Fixed"
|
||||
elif [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
echo "# What's New & More Reliable"
|
||||
elif [ "$WHATS_NEW_COUNT" -gt 0 ]; then
|
||||
echo "# What's New"
|
||||
elif [ "$FIXED_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
echo "# Fixed & More Reliable"
|
||||
elif [ "$FIXED_COUNT" -gt 0 ]; then
|
||||
echo "# Fixed"
|
||||
elif [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||
echo "# More Reliable"
|
||||
elif [ "$DEPS_COUNT" -gt 0 ]; then
|
||||
echo "# Dependency Updates"
|
||||
else
|
||||
echo "# Release Updates"
|
||||
fi
|
||||
|
|
@ -390,99 +675,98 @@ generate_with_manual() {
|
|||
|
||||
# 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"
|
||||
WHATS_NEW_FILE="${TMPDIR}/whats_new"
|
||||
FIXED_FILE="${TMPDIR}/fixed"
|
||||
RELIABLE_FILE="${TMPDIR}/reliable"
|
||||
DEPS_FILE="${TMPDIR}/deps"
|
||||
|
||||
touch "$FEAT_FILE" "$FIX_FILE" "$DOCS_FILE" "$REFACTOR_FILE" "$TEST_FILE" "$OTHER_FILE"
|
||||
touch "$WHATS_NEW_FILE" "$FIXED_FILE" "$RELIABLE_FILE" "$DEPS_FILE"
|
||||
|
||||
# Process commits
|
||||
git log --pretty=format:"%h %s" "${FROM_TAG}..${TO_TAG}" | while read -r hash subject; do
|
||||
git log --pretty=format:"%H %s" "${FROM_TAG}..${TO_TAG}" | while read -r hash subject; do
|
||||
if is_release_notes_skipped_commit "$hash"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
short_hash=$(git rev-parse --short "$hash")
|
||||
# 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"
|
||||
# Skip non-user-facing scope changes to align with git-cliff behavior
|
||||
if echo "$subject" | grep -qE '^(feat|fix|chore|refactor)\([^)]*(devcontainer|vscode|scripts|dev-env|environment)[^)]*\):'; then
|
||||
TYPE="skip"
|
||||
elif echo "$subject" | grep -qE '^(feat|fix|chore|ci)\([^)]*(ci|workflow|actions|github-actions)[^)]*\):'; then
|
||||
TYPE="skip"
|
||||
elif echo "$subject" | grep -qE '^revert'; then
|
||||
TYPE="skip"
|
||||
else
|
||||
TYPE="other"
|
||||
if echo "$subject" | grep -qE '^feat(\(|:)'; then
|
||||
TYPE="whats_new"
|
||||
elif echo "$subject" | grep -qE '^fix(\(|:)'; then
|
||||
# Skip non-user-facing fix scopes to align with cliff filtering
|
||||
if echo "$subject" | grep -qE '^fix\((docs|lint|types|tests?|ci|workflow|scripts|devcontainer|vscode|build|release)\):'; then
|
||||
TYPE="skip"
|
||||
else
|
||||
TYPE="fixed"
|
||||
fi
|
||||
elif echo "$subject" | grep -qE '^perf(\(|:)'; then
|
||||
TYPE="reliable"
|
||||
elif echo "$subject" | grep -qE '^chore\(deps\):'; then
|
||||
TYPE="deps"
|
||||
else
|
||||
TYPE="skip"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create markdown line
|
||||
LINE="- ${subject} ([${hash}](https://github.com/jpawlowski/hass.tibber_prices/commit/${hash}))"
|
||||
LINE="- ${subject} ([${short_hash}](https://github.com/jpawlowski/hass.tibber_prices/commit/${hash}))"
|
||||
|
||||
# Append to appropriate file
|
||||
case "$TYPE" in
|
||||
feat)
|
||||
echo "$LINE" >> "$FEAT_FILE"
|
||||
whats_new)
|
||||
echo "$LINE" >> "$WHATS_NEW_FILE"
|
||||
;;
|
||||
fix)
|
||||
echo "$LINE" >> "$FIX_FILE"
|
||||
fixed)
|
||||
echo "$LINE" >> "$FIXED_FILE"
|
||||
;;
|
||||
docs)
|
||||
echo "$LINE" >> "$DOCS_FILE"
|
||||
reliable)
|
||||
echo "$LINE" >> "$RELIABLE_FILE"
|
||||
;;
|
||||
refactor)
|
||||
echo "$LINE" >> "$REFACTOR_FILE"
|
||||
deps)
|
||||
echo "$LINE" >> "$DEPS_FILE"
|
||||
;;
|
||||
test)
|
||||
echo "$LINE" >> "$TEST_FILE"
|
||||
skip)
|
||||
;;
|
||||
*)
|
||||
echo "$LINE" >> "$OTHER_FILE"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Output grouped by category
|
||||
if [ -s "$FEAT_FILE" ]; then
|
||||
echo "### 🎉 New Features"
|
||||
if [ -s "$WHATS_NEW_FILE" ]; then
|
||||
echo "### 🎉 What's New"
|
||||
echo ""
|
||||
cat "$FEAT_FILE"
|
||||
cat "$WHATS_NEW_FILE"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -s "$FIX_FILE" ]; then
|
||||
echo "### 🐛 Bug Fixes"
|
||||
if [ -s "$FIXED_FILE" ]; then
|
||||
echo "### 🐛 Fixed"
|
||||
echo ""
|
||||
cat "$FIX_FILE"
|
||||
cat "$FIXED_FILE"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -s "$DOCS_FILE" ]; then
|
||||
echo "### 📚 Documentation"
|
||||
if [ -s "$RELIABLE_FILE" ]; then
|
||||
echo "### ⚡ More Reliable"
|
||||
echo ""
|
||||
cat "$DOCS_FILE"
|
||||
cat "$RELIABLE_FILE"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -s "$REFACTOR_FILE" ]; then
|
||||
echo "### 🔧 Maintenance & Refactoring"
|
||||
if [ -s "$DEPS_FILE" ]; then
|
||||
echo "### 📦 Updated Dependencies"
|
||||
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"
|
||||
cat "$DEPS_FILE"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
|
|
@ -668,9 +952,4 @@ else
|
|||
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}"
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue