1- #! /bin/bash
1+ #! /usr/ bin/env bash
22#
33# Generates markdown release notes from conventional commits in pan-os-codegen.
44#
55# Usage:
66# generate-release-notes.sh <version> [since-date]
7+ # generate-release-notes.sh <version> [--since-tag <tag>] [--since-date <date>] [--repo-url <url>]
78#
8- # If since-date is provided, only includes commits after that date.
9- # Otherwise includes all commits.
9+ # Options:
10+ # --since-tag Use commits after this tag (preferred over date-based filtering)
11+ # --since-date Only include commits after this date (fallback)
12+ # --repo-url GitHub repository URL for links (auto-detected from git remote)
13+ #
14+ # Sections generated:
15+ # - Breaking Changes (feat(MAJOR) or type! commits)
16+ # - New Resources (feat(specs) commits, listed by resource name)
17+ # - Features (feat commits, excluding specs)
18+ # - Bug Fixes (fix commits)
19+ # - Performance (perf commits)
20+ # - Reverts (revert commits)
1021
1122set -euo pipefail
1223
13- VERSION=" ${1:? Usage: generate-release-notes.sh <version> [since-date]} "
14- SINCE_DATE=" ${2:- } "
24+ SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
25+ # shellcheck source=lib/conventional-commits.sh
26+ source " $SCRIPT_DIR /lib/conventional-commits.sh"
27+
28+ # --- Argument parsing ---
29+
30+ VERSION=" "
31+ SINCE_TAG=" "
32+ SINCE_DATE=" "
33+ REPO_URL=" "
34+
35+ # Support both old positional interface and new named flags
36+ if [ $# -ge 1 ] && [[ " $1 " != --* ]]; then
37+ VERSION=" $1 "
38+ shift
39+ # Check if second positional arg is a date (old interface) or a flag
40+ if [ $# -ge 1 ] && [[ " $1 " != --* ]]; then
41+ SINCE_DATE=" $1 "
42+ shift
43+ fi
44+ fi
45+
46+ while [ $# -gt 0 ]; do
47+ case " $1 " in
48+ --since-tag)
49+ SINCE_TAG=" $2 "
50+ shift 2
51+ ;;
52+ --since-date)
53+ SINCE_DATE=" $2 "
54+ shift 2
55+ ;;
56+ --repo-url)
57+ REPO_URL=" $2 "
58+ shift 2
59+ ;;
60+ * )
61+ if [ -z " $VERSION " ]; then
62+ VERSION=" $1 "
63+ shift
64+ else
65+ echo " Unknown argument: $1 " >&2
66+ exit 1
67+ fi
68+ ;;
69+ esac
70+ done
71+
72+ if [ -z " $VERSION " ]; then
73+ echo " Usage: generate-release-notes.sh <version> [--since-tag <tag>] [--since-date <date>] [--repo-url <url>]" >&2
74+ exit 1
75+ fi
76+
77+ if [ -z " $REPO_URL " ]; then
78+ REPO_URL=$( detect_repo_url)
79+ fi
80+
81+ # --- Build git log range ---
1582
16- LOG_ARGS=" --format=%s"
17- if [ -n " $SINCE_DATE " ]; then
18- LOG_ARGS=" $LOG_ARGS --after=$SINCE_DATE "
83+ LOG_RANGE=" "
84+ if [ -n " $SINCE_TAG " ]; then
85+ # Try codegen release tag format first
86+ CODEGEN_TAG=" release/${SINCE_TAG} "
87+ if git rev-parse " $CODEGEN_TAG " > /dev/null 2>&1 ; then
88+ LOG_RANGE=" ${CODEGEN_TAG} ..HEAD"
89+ elif git rev-parse " $SINCE_TAG " > /dev/null 2>&1 ; then
90+ LOG_RANGE=" ${SINCE_TAG} ..HEAD"
91+ fi
1992fi
2093
21- # Collect commits into arrays by type
94+ if [ -z " $LOG_RANGE " ] && [ -n " $SINCE_DATE " ]; then
95+ LOG_RANGE=" --after=${SINCE_DATE} HEAD"
96+ elif [ -z " $LOG_RANGE " ]; then
97+ LOG_RANGE=" HEAD"
98+ fi
99+
100+ # --- Collect and parse commits ---
101+
102+ # Use simple arrays for each section (bash 3.2 compatible)
103+ BREAKING=()
104+ NEW_RESOURCES=()
22105FEATS=()
23106FIXES=()
24- BREAKING=()
107+ PERFS=()
108+ REVERTS=()
25109
26- while IFS= read -r subject ; do
27- [ -z " $subject " ] && continue
110+ while IFS= read -r line ; do
111+ [ -z " $line " ] && continue
28112
29- # feat(MAJOR) -> breaking
30- if echo " $subject " | grep -qiE ' ^feat\(MAJOR\)' ; then
31- msg=$( echo " $subject " | sed ' s/^feat(MAJOR)[!]*: //' )
32- BREAKING+=(" - $msg " )
33- continue
34- fi
113+ # Split on ||| separator
114+ local_hash=" ${line%% |||* } "
115+ local_subject=" ${line#* |||} "
35116
36- # feat -> feature
37- if echo " $subject " | grep -qE ' ^feat(\(|:)' ; then
38- scope=$( echo " $subject " | sed -n ' s/^feat(\([^)]*\))[!]*:.*/\1/p' )
39- msg=$( echo " $subject " | sed ' s/^feat([^)]*)[!]*: //' | sed ' s/^feat[!]*: //' )
40- if [ -n " $scope " ]; then
41- FEATS+=(" - **$scope **: $msg " )
42- else
43- FEATS+=(" - $msg " )
44- fi
117+ parse_commit " $local_subject " " $local_hash "
118+
119+ # Skip hidden types
120+ if ! is_visible " $CC_TYPE " && ! $CC_BREAKING ; then
45121 continue
46122 fi
47123
48- # fix -> bug fix
49- if echo " $subject " | grep -qE ' ^fix(\(|:)' ; then
50- scope=$( echo " $subject " | sed -n ' s/^fix(\([^)]*\))[!]*:.*/\1/p' )
51- msg=$( echo " $subject " | sed ' s/^fix([^)]*)[!]*: //' | sed ' s/^fix[!]*: //' )
52- if [ -n " $scope " ]; then
53- FIXES+=(" - **$scope **: $msg " )
54- else
55- FIXES+=(" - $msg " )
56- fi
124+ # Format the entry
125+ entry=$( format_entry " $CC_MESSAGE " " $CC_PR " " $CC_HASH_FULL " " $REPO_URL " )
126+
127+ # Breaking changes (from feat(MAJOR) or ! suffix)
128+ if $CC_BREAKING ; then
129+ BREAKING+=(" - $entry " )
57130 continue
58131 fi
59132
60- # Skip chore, docs, ci, test, refactor, style, build — internal commits
61- done < <( git log $LOG_ARGS HEAD 2> /dev/null)
133+ case " $CC_TYPE " in
134+ feat)
135+ if [[ " $CC_SCOPE " == " specs" || " $CC_SCOPE " == " spec" ]]; then
136+ # Extract resource name for New Resources section
137+ resource_name=$( extract_resource_name " $CC_MESSAGE " )
138+ if [ -n " $resource_name " ]; then
139+ NEW_RESOURCES+=(" - \` ${resource_name} \` $( format_entry " " " $CC_PR " " $CC_HASH_FULL " " $REPO_URL " ) " )
140+ else
141+ NEW_RESOURCES+=(" - $entry " )
142+ fi
143+ else
144+ if [ -n " $CC_SCOPE " ]; then
145+ FEATS+=(" - **${CC_SCOPE} :** $entry " )
146+ else
147+ FEATS+=(" - $entry " )
148+ fi
149+ fi
150+ ;;
151+ fix)
152+ if [ -n " $CC_SCOPE " ]; then
153+ FIXES+=(" - **${CC_SCOPE} :** $entry " )
154+ else
155+ FIXES+=(" - $entry " )
156+ fi
157+ ;;
158+ perf)
159+ if [ -n " $CC_SCOPE " ]; then
160+ PERFS+=(" - **${CC_SCOPE} :** $entry " )
161+ else
162+ PERFS+=(" - $entry " )
163+ fi
164+ ;;
165+ revert)
166+ REVERTS+=(" - $entry " )
167+ ;;
168+ esac
169+ done < <( git log $LOG_RANGE --format=" %H|||%s" --no-merges 2> /dev/null)
62170
63- # Also scan commit bodies for BREAKING CHANGE
171+ # Also scan commit bodies for BREAKING CHANGE footer
64172while IFS= read -r body_line; do
65- if echo " $body_line " | grep -q ' BREAKING CHANGE:' ; then
66- msg=$( echo " $body_line " | sed ' s/BREAKING CHANGE: //' )
173+ [ -z " $body_line " ] && continue
174+ if [[ " $body_line " == " BREAKING CHANGE:" * ]]; then
175+ msg=" ${body_line# BREAKING CHANGE: } "
67176 BREAKING+=(" - $msg " )
68177 fi
69- done < <( git log ${SINCE_DATE: +--after=$SINCE_DATE } --format=" %b" HEAD 2> /dev/null)
178+ done < <( git log $LOG_RANGE --format=" %b" --no-merges 2> /dev/null)
179+
180+ # --- Output ---
70181
71- # Output
72182echo " ## What's Changed in ${VERSION} "
73183echo " "
74184
@@ -79,6 +189,13 @@ if [ ${#BREAKING[@]} -gt 0 ]; then
79189 echo " "
80190fi
81191
192+ if [ ${# NEW_RESOURCES[@]} -gt 0 ]; then
193+ echo " ### New Resources"
194+ echo " "
195+ printf ' %s\n' " ${NEW_RESOURCES[@]} "
196+ echo " "
197+ fi
198+
82199if [ ${# FEATS[@]} -gt 0 ]; then
83200 echo " ### Features"
84201 echo " "
@@ -93,7 +210,24 @@ if [ ${#FIXES[@]} -gt 0 ]; then
93210 echo " "
94211fi
95212
96- if [ ${# BREAKING[@]} -eq 0 ] && [ ${# FEATS[@]} -eq 0 ] && [ ${# FIXES[@]} -eq 0 ]; then
213+ if [ ${# PERFS[@]} -gt 0 ]; then
214+ echo " ### Performance Improvements"
215+ echo " "
216+ printf ' %s\n' " ${PERFS[@]} "
217+ echo " "
218+ fi
219+
220+ if [ ${# REVERTS[@]} -gt 0 ]; then
221+ echo " ### Reverts"
222+ echo " "
223+ printf ' %s\n' " ${REVERTS[@]} "
224+ echo " "
225+ fi
226+
227+ # Check if nothing was generated
228+ if [ ${# BREAKING[@]} -eq 0 ] && [ ${# NEW_RESOURCES[@]} -eq 0 ] && \
229+ [ ${# FEATS[@]} -eq 0 ] && [ ${# FIXES[@]} -eq 0 ] && \
230+ [ ${# PERFS[@]} -eq 0 ] && [ ${# REVERTS[@]} -eq 0 ]; then
97231 echo " No notable changes."
98232 echo " "
99233fi
0 commit comments