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
53
cliff.toml
53
cliff.toml
|
|
@ -5,13 +5,19 @@
|
||||||
# Template for the changelog body
|
# Template for the changelog body
|
||||||
header = ""
|
header = ""
|
||||||
body = """
|
body = """
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
{% for group, commits in commits | group_by(attribute="group") -%}
|
||||||
### {{ group | striptags | trim | upper_first }}
|
### {{ group | striptags | trim | upper_first }}
|
||||||
{% for commit in commits %}
|
{% for commit in commits -%}
|
||||||
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}\
|
{% set impact_text = "" -%}
|
||||||
{% if commit.breaking %} [**BREAKING**]{% endif %} \
|
{% set footers = commit.footers | default(value=[]) -%}
|
||||||
([{{ commit.id | truncate(length=7, end="") }}](https://github.com/jpawlowski/hass.tibber_prices/commit/{{ commit.id }}))
|
{% 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 %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -25,7 +31,8 @@ trim = true
|
||||||
[git]
|
[git]
|
||||||
# Parse conventional commits
|
# Parse conventional commits
|
||||||
conventional_commits = true
|
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
|
filter_unconventional = false
|
||||||
split_commits = false
|
split_commits = false
|
||||||
|
|
||||||
|
|
@ -33,22 +40,28 @@ split_commits = false
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
# Skip manifest.json version bumps (release housekeeping)
|
# Skip manifest.json version bumps (release housekeeping)
|
||||||
{ message = "^chore\\(release\\): bump version", skip = true },
|
{ 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)
|
# Skip development environment changes (not user-relevant)
|
||||||
{ message = "^(feat|fix|chore|refactor)\\((devcontainer|vscode|scripts|dev-env|environment)\\):", skip = true },
|
{ message = "^(feat|fix|chore|refactor)\\((devcontainer|vscode|scripts|dev-env|environment)\\):", skip = true },
|
||||||
# Skip CI/CD infrastructure changes (not user-relevant)
|
# Skip CI/CD infrastructure changes (not user-relevant)
|
||||||
{ message = "^(feat|fix|chore|ci)\\((ci|workflow|actions|github-actions)\\):", skip = true },
|
{ message = "^(feat|fix|chore|ci)\\((ci|workflow|actions|github-actions)\\):", skip = true },
|
||||||
# Keep dependency updates - these ARE relevant for users
|
# Skip non-user-facing fix scopes
|
||||||
{ message = "^chore\\(deps\\):", group = "📦 Dependencies" },
|
{ message = "^fix\\((docs|lint|types|tests?|ci|workflow|scripts|devcontainer|vscode|build|release)\\):", skip = true },
|
||||||
# Regular commit types
|
# User-facing categories aligned with AI output style
|
||||||
{ message = "^feat", group = "🎉 New Features" },
|
{ message = "^feat", group = "🎉 What's New" },
|
||||||
{ message = "^fix", group = "🐛 Bug Fixes" },
|
{ message = "^fix", group = "🐛 Fixed" },
|
||||||
{ message = "^docs?", group = "📚 Documentation" },
|
{ message = "^perf", group = "⚡ More Reliable" },
|
||||||
{ message = "^perf", group = "⚡ Performance" },
|
{ message = "^chore\\(deps\\):", group = "📦 Updated Dependencies" },
|
||||||
{ message = "^refactor", group = "🔧 Maintenance & Refactoring" },
|
# Skip mostly developer-facing categories
|
||||||
{ message = "^style", group = "🎨 Styling" },
|
{ message = "^docs?", skip = true },
|
||||||
{ message = "^test", group = "🧪 Testing" },
|
{ message = "^refactor", skip = true },
|
||||||
{ message = "^chore", group = "🔧 Maintenance & Refactoring" },
|
{ message = "^style", skip = true },
|
||||||
{ message = "^build", group = "📦 Build" },
|
{ message = "^test", skip = true },
|
||||||
|
{ message = "^build", skip = true },
|
||||||
|
{ message = "^chore", skip = true },
|
||||||
|
# Final fallback to avoid ungrouped commits
|
||||||
|
{ message = ".*", skip = true },
|
||||||
]
|
]
|
||||||
|
|
||||||
# Protect breaking changes
|
# 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}))" },
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/jpawlowski/hass.tibber_prices/issues/${2}))" },
|
||||||
]
|
]
|
||||||
|
|
||||||
# Filter out commits
|
# Apply commit parser filtering rules
|
||||||
filter_commits = false
|
filter_commits = true
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,156 @@ fi
|
||||||
BACKEND="${RELEASE_NOTES_BACKEND:-auto}"
|
BACKEND="${RELEASE_NOTES_BACKEND:-auto}"
|
||||||
USE_AI="${USE_AI:-true}"
|
USE_AI="${USE_AI:-true}"
|
||||||
GITHUB_REPO="${GITHUB_REPOSITORY:-jpawlowski/hass.tibber_prices}"
|
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
|
# Parse arguments
|
||||||
FROM_TAG="${1:-$(git describe --tags --abbrev=0 2>/dev/null || echo "")}"
|
FROM_TAG="${1:-$(git describe --tags --abbrev=0 2>/dev/null || echo "")}"
|
||||||
|
|
@ -54,6 +204,11 @@ if [[ -z $FROM_TAG ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 "${BLUE}==> Generating release notes: ${FROM_TAG}..${TO_TAG}${NC}"
|
||||||
log_info ""
|
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 "${YELLOW}Note: This will use one premium request from your monthly quota${NC}"
|
||||||
log_info ""
|
log_info ""
|
||||||
|
|
||||||
# Get commit log for the range with file statistics
|
# Get filtered commit log for the range with file statistics (oldest -> newest).
|
||||||
# This helps the AI understand which commits touched which files
|
# Commits explicitly marked as non-user-facing are excluded.
|
||||||
COMMITS=$(git log --pretty=format:"%h | %s%n%b%n" --stat --compact-summary "${FROM_TAG}..${TO_TAG}")
|
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.
|
# Get code diff for user-facing files to give the AI real context about what changed.
|
||||||
# Limited to ~8KB to keep the prompt manageable.
|
# Compact mode is enabled by default to reduce prompt tokens without reducing semantic signal.
|
||||||
DIFF_CONTEXT=$(git diff --unified=2 --diff-filter=AM \
|
# Toggle: RELEASE_NOTES_COMPACT_DIFF=false
|
||||||
-- "custom_components/tibber_prices/sensor/" \
|
DIFF_CONTEXT=$(build_diff_context)
|
||||||
-- "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
|
if [[ -z $COMMITS ]]; then
|
||||||
log_info "${YELLOW}No commits found between ${FROM_TAG} and ${TO_TAG}${NC}"
|
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
|
users what they can now DO, what was BROKEN and is now FIXED, or why the integration
|
||||||
is now more RELIABLE.
|
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
|
## WRITING RULES — FOLLOW STRICTLY
|
||||||
|
|
||||||
**Rule 1 — No jargon, ever.**
|
**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):**
|
**Code diff for user-facing files (use this to understand WHAT actually changed — but always translate to user language):**
|
||||||
|
|
||||||
${DIFF_CONTEXT}
|
${DIFF_CONTEXT}
|
||||||
|
|
@ -275,29 +470,28 @@ End after the Buy Me A Coffee button. No meta-commentary, no explanations."
|
||||||
generate_with_gitcliff() {
|
generate_with_gitcliff() {
|
||||||
log_info "${BLUE}==> Generating with git-cliff${NC}"
|
log_info "${BLUE}==> Generating with git-cliff${NC}"
|
||||||
|
|
||||||
# Analyze commits to generate smart title
|
# Analyze commits to generate a title aligned with current user-facing groups
|
||||||
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 "")
|
COMMITS_LOG=$(git log --pretty=format:"%s" "${FROM_TAG}..${TO_TAG}" 2>/dev/null || echo "")
|
||||||
|
|
||||||
# Count user-facing changes (exclude dev/ci/docs)
|
WHATS_NEW_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat(\(|:)" || true)
|
||||||
FEAT_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat\((sensors|binary_sensor|config_flow|services|coordinator|api)\):" || true)
|
FIXED_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix(\(|:)" || true)
|
||||||
FIX_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix\((sensors|binary_sensor|config_flow|services|coordinator|api|translations)\):" || true)
|
RELIABLE_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf(\(|:)" || true)
|
||||||
PERF_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 [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$FIXED_COUNT" -gt 0 ]; then
|
||||||
if [ $PERF_COUNT -gt 0 ]; then
|
TITLE="# What's New & Fixed"
|
||||||
TITLE="# Performance & Reliability Improvements"
|
elif [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 2 ]; then
|
TITLE="# What's New & More Reliable"
|
||||||
TITLE="# New Features & Enhancements"
|
elif [ "$WHATS_NEW_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 0 ] && [ $FIX_COUNT -gt 0 ]; then
|
TITLE="# What's New"
|
||||||
TITLE="# New Features & Bug Fixes"
|
elif [ "$FIXED_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 0 ]; then
|
TITLE="# Fixed & More Reliable"
|
||||||
TITLE="# New Features"
|
elif [ "$FIXED_COUNT" -gt 0 ]; then
|
||||||
elif [ $FIX_COUNT -gt 2 ]; then
|
TITLE="# Fixed"
|
||||||
TITLE="# Bug Fixes & Improvements"
|
elif [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FIX_COUNT -gt 0 ]; then
|
TITLE="# More Reliable"
|
||||||
TITLE="# Bug Fixes"
|
elif [ "$DEPS_COUNT" -gt 0 ]; then
|
||||||
|
TITLE="# Dependency Updates"
|
||||||
else
|
else
|
||||||
TITLE="# Release Updates"
|
TITLE="# Release Updates"
|
||||||
fi
|
fi
|
||||||
|
|
@ -326,24 +520,114 @@ conventional_commits = true
|
||||||
filter_unconventional = false
|
filter_unconventional = false
|
||||||
split_commits = false
|
split_commits = false
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
{ message = "^feat", group = "🎉 New Features" },
|
{ message = "^chore\\(release\\): bump version", skip = true },
|
||||||
{ message = "^fix", group = "🐛 Bug Fixes" },
|
{ message = "^revert", skip = true },
|
||||||
{ message = "^doc", group = "📚 Documentation" },
|
{ message = "^(feat|fix|chore|refactor)\\((devcontainer|vscode|scripts|dev-env|environment)\\):", skip = true },
|
||||||
{ message = "^perf", group = "⚡ Performance" },
|
{ message = "^(feat|fix|chore|ci)\\((ci|workflow|actions|github-actions)\\):", skip = true },
|
||||||
{ message = "^refactor", group = "🔧 Maintenance & Refactoring" },
|
{ message = "^fix\\((docs|lint|types|tests?|ci|workflow|scripts|devcontainer|vscode|build|release)\\):", skip = true },
|
||||||
{ message = "^style", group = "🎨 Styling" },
|
{ message = "^feat", group = "🎉 What's New" },
|
||||||
{ message = "^test", group = "🧪 Testing" },
|
{ message = "^fix", group = "🐛 Fixed" },
|
||||||
{ message = "^chore", group = "🔧 Maintenance & Refactoring" },
|
{ message = "^perf", group = "⚡ More Reliable" },
|
||||||
{ message = "^ci", group = "🔄 CI/CD" },
|
{ message = "^chore\\(deps\\):", group = "📦 Updated Dependencies" },
|
||||||
{ message = "^build", group = "📦 Build" },
|
{ 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
|
EOF
|
||||||
CLIFF_CONFIG="/tmp/cliff.toml"
|
CLIFF_CONFIG="/tmp/cliff.toml"
|
||||||
else
|
else
|
||||||
CLIFF_CONFIG="cliff.toml"
|
CLIFF_CONFIG="cliff.toml"
|
||||||
fi
|
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)
|
echo "" # Ensure output ends with newline (cliff.toml trim=true removes trailing newline)
|
||||||
|
|
||||||
if [ "$CLIFF_CONFIG" = "/tmp/cliff.toml" ]; then
|
if [ "$CLIFF_CONFIG" = "/tmp/cliff.toml" ]; then
|
||||||
|
|
@ -362,27 +646,28 @@ generate_with_manual() {
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
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 "")
|
COMMITS_LOG=$(git log --pretty=format:"%s" "${FROM_TAG}..${TO_TAG}" 2>/dev/null || echo "")
|
||||||
|
|
||||||
# Count user-facing changes (exclude dev/ci/docs)
|
WHATS_NEW_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat(\(|:)" || true)
|
||||||
FEAT_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^feat\((sensors|binary_sensor|config_flow|services|coordinator|api)\):" || true)
|
FIXED_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix(\(|:)" || true)
|
||||||
FIX_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^fix\((sensors|binary_sensor|config_flow|services|coordinator|api|translations)\):" || true)
|
RELIABLE_COUNT=$(echo "$COMMITS_LOG" | grep -cE "^perf(\(|:)" || true)
|
||||||
PERF_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 [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$FIXED_COUNT" -gt 0 ]; then
|
||||||
if [ $PERF_COUNT -gt 0 ]; then
|
echo "# What's New & Fixed"
|
||||||
echo "# Performance & Reliability Improvements"
|
elif [ "$WHATS_NEW_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 2 ]; then
|
echo "# What's New & More Reliable"
|
||||||
echo "# New Features & Enhancements"
|
elif [ "$WHATS_NEW_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 0 ] && [ $FIX_COUNT -gt 0 ]; then
|
echo "# What's New"
|
||||||
echo "# New Features & Bug Fixes"
|
elif [ "$FIXED_COUNT" -gt 0 ] && [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FEAT_COUNT -gt 0 ]; then
|
echo "# Fixed & More Reliable"
|
||||||
echo "# New Features"
|
elif [ "$FIXED_COUNT" -gt 0 ]; then
|
||||||
elif [ $FIX_COUNT -gt 2 ]; then
|
echo "# Fixed"
|
||||||
echo "# Bug Fixes & Improvements"
|
elif [ "$RELIABLE_COUNT" -gt 0 ]; then
|
||||||
elif [ $FIX_COUNT -gt 0 ]; then
|
echo "# More Reliable"
|
||||||
echo "# Bug Fixes"
|
elif [ "$DEPS_COUNT" -gt 0 ]; then
|
||||||
|
echo "# Dependency Updates"
|
||||||
else
|
else
|
||||||
echo "# Release Updates"
|
echo "# Release Updates"
|
||||||
fi
|
fi
|
||||||
|
|
@ -390,99 +675,98 @@ generate_with_manual() {
|
||||||
|
|
||||||
# Create temporary files for each category
|
# Create temporary files for each category
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
FEAT_FILE="${TMPDIR}/feat"
|
WHATS_NEW_FILE="${TMPDIR}/whats_new"
|
||||||
FIX_FILE="${TMPDIR}/fix"
|
FIXED_FILE="${TMPDIR}/fixed"
|
||||||
DOCS_FILE="${TMPDIR}/docs"
|
RELIABLE_FILE="${TMPDIR}/reliable"
|
||||||
REFACTOR_FILE="${TMPDIR}/refactor"
|
DEPS_FILE="${TMPDIR}/deps"
|
||||||
TEST_FILE="${TMPDIR}/test"
|
|
||||||
OTHER_FILE="${TMPDIR}/other"
|
|
||||||
|
|
||||||
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
|
# 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)
|
# Simple type extraction (before colon)
|
||||||
TYPE=""
|
TYPE=""
|
||||||
if echo "$subject" | grep -q '^feat'; then
|
# Skip non-user-facing scope changes to align with git-cliff behavior
|
||||||
TYPE="feat"
|
if echo "$subject" | grep -qE '^(feat|fix|chore|refactor)\([^)]*(devcontainer|vscode|scripts|dev-env|environment)[^)]*\):'; then
|
||||||
elif echo "$subject" | grep -q '^fix'; then
|
TYPE="skip"
|
||||||
TYPE="fix"
|
elif echo "$subject" | grep -qE '^(feat|fix|chore|ci)\([^)]*(ci|workflow|actions|github-actions)[^)]*\):'; then
|
||||||
elif echo "$subject" | grep -q '^docs'; then
|
TYPE="skip"
|
||||||
TYPE="docs"
|
elif echo "$subject" | grep -qE '^revert'; then
|
||||||
elif echo "$subject" | grep -q '^refactor\|^chore'; then
|
TYPE="skip"
|
||||||
TYPE="refactor"
|
|
||||||
elif echo "$subject" | grep -q '^test'; then
|
|
||||||
TYPE="test"
|
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# Create markdown line
|
# 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
|
# Append to appropriate file
|
||||||
case "$TYPE" in
|
case "$TYPE" in
|
||||||
feat)
|
whats_new)
|
||||||
echo "$LINE" >> "$FEAT_FILE"
|
echo "$LINE" >> "$WHATS_NEW_FILE"
|
||||||
;;
|
;;
|
||||||
fix)
|
fixed)
|
||||||
echo "$LINE" >> "$FIX_FILE"
|
echo "$LINE" >> "$FIXED_FILE"
|
||||||
;;
|
;;
|
||||||
docs)
|
reliable)
|
||||||
echo "$LINE" >> "$DOCS_FILE"
|
echo "$LINE" >> "$RELIABLE_FILE"
|
||||||
;;
|
;;
|
||||||
refactor)
|
deps)
|
||||||
echo "$LINE" >> "$REFACTOR_FILE"
|
echo "$LINE" >> "$DEPS_FILE"
|
||||||
;;
|
;;
|
||||||
test)
|
skip)
|
||||||
echo "$LINE" >> "$TEST_FILE"
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "$LINE" >> "$OTHER_FILE"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Output grouped by category
|
# Output grouped by category
|
||||||
if [ -s "$FEAT_FILE" ]; then
|
if [ -s "$WHATS_NEW_FILE" ]; then
|
||||||
echo "### 🎉 New Features"
|
echo "### 🎉 What's New"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$FEAT_FILE"
|
cat "$WHATS_NEW_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s "$FIX_FILE" ]; then
|
if [ -s "$FIXED_FILE" ]; then
|
||||||
echo "### 🐛 Bug Fixes"
|
echo "### 🐛 Fixed"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$FIX_FILE"
|
cat "$FIXED_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s "$DOCS_FILE" ]; then
|
if [ -s "$RELIABLE_FILE" ]; then
|
||||||
echo "### 📚 Documentation"
|
echo "### ⚡ More Reliable"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$DOCS_FILE"
|
cat "$RELIABLE_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s "$REFACTOR_FILE" ]; then
|
if [ -s "$DEPS_FILE" ]; then
|
||||||
echo "### 🔧 Maintenance & Refactoring"
|
echo "### 📦 Updated Dependencies"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$REFACTOR_FILE"
|
cat "$DEPS_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 ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -669,8 +953,3 @@ fi
|
||||||
|
|
||||||
rm -f "$TEMP_NOTES" "$TEMP_BODY"
|
rm -f "$TEMP_NOTES" "$TEMP_BODY"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
|
||||||
|
|
||||||
# If no auto-update, just show success message
|
|
||||||
echo ""
|
|
||||||
log_info "${GREEN}==> Release notes generated successfully!${NC}"
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue