Skip to content

Commit e5ec7d7

Browse files
authored
feat: improve release note generation (#65)
1 parent aeb3f5c commit e5ec7d7

File tree

2 files changed

+115
-16
lines changed

2 files changed

+115
-16
lines changed

generate-changelog.sh

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22

33
set -euo pipefail
44

5+
echo "Generating changelog for version: $VERSION"
6+
57
# Ensure gh CLI available
68
if ! command -v gh &> /dev/null; then
79
echo "gh CLI is required but not installed." >&2
810
exit 1
911
fi
1012

13+
RELEASE_NOTES_TO_JSON_SCRIPT="$(realpath "$(dirname $0)/release-notes-to-json.sh")"
1114
cd $(dirname "$0")/../../
1215

1316
LATEST_RELEASE_TAG=$(gh release list --json tagName,isLatest --jq '.[] | select(.isLatest)|.tagName')
1417
if [[ -z "$LATEST_RELEASE_TAG" ]]; then # first release?
1518
LATEST_RELEASE_TAG=$(git rev-list --max-parents=0 HEAD) # first commit in the branch.
1619
fi
1720

18-
GIT_LOG_OUTPUT=$(git log "$LATEST_RELEASE_TAG"..HEAD --oneline --pretty=format:"%s" main)
21+
GIT_LOG_OUTPUT=$(git log "$LATEST_RELEASE_TAG"..HEAD --oneline --pretty=format:"%s")
1922
PR_COMMITS=$(echo "$GIT_LOG_OUTPUT" | grep -oE "#[0-9]+" || true | tr -d '#' | sort -u)
2023

2124
CHANGELOG_FILE=./CHANGELOG.md
@@ -43,38 +46,54 @@ done
4346

4447
for PR_NUMBER in $PR_COMMITS; do
4548
PR_JSON=$(gh pr view "$PR_NUMBER" --json number,title,body,url,author)
49+
echo -n "Checking PR $PR_NUMBER"
4650

47-
IS_BOT=$(echo "$PR_JSON" | jq -r '.author.is_bot')
51+
IS_BOT=$(jq -r '.author.is_bot' <<< "$PR_JSON")
4852
if [[ "$IS_BOT" == "true" ]]; then
53+
echo " [skipping bot PR"]
4954
continue
5055
fi
5156

52-
PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')
53-
PR_URL=$(echo "$PR_JSON" | jq -r '.url')
54-
PR_BODY=$(echo "$PR_JSON" | jq -r '.body')
57+
PR_TITLE=$(jq -r '.title' <<< "$PR_JSON")
58+
PR_URL=$(jq -r '.url' <<< "$PR_JSON")
59+
PR_BODY=$(jq -r '.body' <<< "$PR_JSON")
60+
echo " - $PR_TITLE"
5561

5662
# Determine type from conventional commit (assumes title like "type(scope): message" or "type: message")
5763
TYPE=$(echo "$PR_TITLE" | grep -oE '^[a-z]+' || echo "feat")
5864
CLEAN_TITLE=$(echo "$PR_TITLE" | sed -E 's/^[a-z]+(\([^)]+\))?(!)?:[[:space:]]+//')
5965

6066
# Extract release note block, this contains the release notes and the release notes headers.
61-
RELEASE_NOTE_BLOCK=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p')
67+
# The last sed call is required to remove the carriage return characters (Github seems to use \r\n for new lines in PR bodies).
68+
RELEASE_NOTE_BLOCK=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p' | sed 's/\r//g')
6269
# Extract release notes body
63-
RELEASE_NOTE=$(echo "$RELEASE_NOTE_BLOCK" | sed '1d;$d' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
70+
RELEASE_NOTE_JSON=$("$RELEASE_NOTES_TO_JSON_SCRIPT" <<< "$RELEASE_NOTE_BLOCK")
71+
72+
# skip PRs without release notes
73+
if [[ "$RELEASE_NOTE_JSON" == "[]" ]]; then
74+
echo " [ignoring PR without release notes]"
75+
continue
76+
fi
77+
78+
# Format release notes
79+
# Updating NOTE_ENTRY in the loop does not work because it is executed in a subshell, therefore this workaround via echo.
80+
NOTE_ENTRY="$(
81+
jq -rc 'sort_by(.audience, .type) | .[]' <<< "$RELEASE_NOTE_JSON" | while IFS= read -r note; do
82+
NOTE_TYPE=$(jq -r '.type' <<< "$note" | tr '[:lower:]' '[:upper:]')
83+
NOTE_AUDIENCE=$(jq -r '.audience' <<< "$note" | tr '[:lower:]' '[:upper:]')
84+
NOTE_BODY=$(jq -r '.body' <<< "$note")
85+
echo -en "\n - **[$NOTE_AUDIENCE][$NOTE_TYPE]** $NOTE_BODY"
86+
done
87+
)"
6488

6589
# Format entry
6690
ENTRY="- $CLEAN_TITLE [#${PR_NUMBER}](${PR_URL})"
6791

68-
if [[ -z "$RELEASE_NOTE" || "$RELEASE_NOTE" == "NONE" ]]; then
69-
ENTRY+="."
70-
else
71-
# Extract and format the release note headers.
72-
HEADERS=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p'| head -n 1 | sed 's/^```//')
73-
FORMATED_HEADERS=$(echo "$HEADERS" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/\s\+/ /g' | sed 's/\(\S\+\)/[\1]/g')
92+
# Extract and format the release note headers.
93+
HEADERS=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p'| head -n 1 | sed 's/^```//')
94+
FORMATED_HEADERS=$(echo "$HEADERS" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/\s\+/ /g' | sed 's/\(\S\+\)/[\1]/g')
7495

75-
ENTRY="- ${FORMATED_HEADERS} ${CLEAN_TITLE} [#${PR_NUMBER}](${PR_URL}): ${RELEASE_NOTE}"
76-
fi
77-
ENTRY+="\n"
96+
ENTRY="- ${CLEAN_TITLE} [${PR_NUMBER}](${PR_URL})${NOTE_ENTRY}\n"
7897

7998
# Append to appropriate section
8099
if [[ -n "${PR_ENTRIES[$TYPE]+x}" ]]; then

release-notes-to-json.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/bin/bash
2+
3+
# This script reads release notes from STDIN and converts them to JSON format, which is then printed to STDOUT.
4+
# Expected input format:
5+
#
6+
# ```<type> <audience>
7+
# <body> (may span multiple lines)
8+
# ```
9+
# (the above block may repeat multiple times)
10+
#
11+
# Output:
12+
# [
13+
# {
14+
# "type": "<type>",
15+
# "audience": "<audience>",
16+
# "body": "<body>"
17+
# },
18+
# ...
19+
# ]
20+
#
21+
# Additional whitespace is ignored, but each release note block must start with ``` followed by two lowercase words,
22+
# and it must end with ``` followed by any amount of whitespace and then a newline or end of input.
23+
#
24+
# Release notes where the body is empty or "NONE" are ignored and not part of the output JSON array.
25+
26+
set -euo pipefail
27+
28+
type=""
29+
audience=""
30+
body=""
31+
rnj='[]'
32+
33+
start_regex='^[[:blank:]]*```[[:blank:]]*([a-z]+)[[:blank:]]+([a-z]+)[[:blank:]]*$'
34+
end_regex='^[[:blank:]]*```[[:blank:]]*$'
35+
empty_regex='^[[:space:]]*$'
36+
line=""
37+
38+
while IFS= read -r line || [[ -n "$line" ]]; do
39+
# check if we are currently reading a release note body
40+
if [[ -n "$type" ]]; then
41+
# check for end of release note block
42+
if [[ "$line" =~ $end_regex ]]; then
43+
# end of release note block
44+
# add release note to JSON array, unless the body is empty or "NONE"
45+
if [[ ! "$body" =~ $empty_regex ]] && [[ "$body" != "NONE" ]]; then
46+
rnj="$(jq '. + [{"type": $type, "audience": $audience, "body": $body}]' --arg type "$type" --arg audience "$audience" --arg body "$body" <<< "$rnj")"
47+
fi
48+
# reset variables
49+
type=""
50+
audience=""
51+
body=""
52+
else
53+
# more body content
54+
# append line to body
55+
if [[ -n "$body" ]]; then
56+
body="$body\n$line"
57+
else
58+
body="$line"
59+
fi
60+
fi
61+
else
62+
# not currently reading a release note body
63+
# check for start of release note block
64+
if [[ "$line" =~ $start_regex ]]; then
65+
# start of release note block
66+
type="${BASH_REMATCH[1]}"
67+
audience="${BASH_REMATCH[2]}"
68+
body=""
69+
else
70+
# invalid line outside of release note block
71+
if [[ -n "$line" ]]; then
72+
# ignore empty lines, log a warning otherwise
73+
echo "unexpected line in release notes: $line" >&2
74+
fi
75+
fi
76+
fi
77+
done
78+
79+
# output JSON array
80+
echo "$rnj"

0 commit comments

Comments
 (0)