Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions generate-changelog.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

set -euo pipefail

echo "Generating changelog for version: $VERSION"

# Ensure gh CLI available
if ! command -v gh &> /dev/null; then
echo "gh CLI is required but not installed." >&2
exit 1
fi

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

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

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

CHANGELOG_FILE=./CHANGELOG.md
Expand Down Expand Up @@ -43,38 +46,54 @@ done

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

IS_BOT=$(echo "$PR_JSON" | jq -r '.author.is_bot')
IS_BOT=$(jq -r '.author.is_bot' <<< "$PR_JSON")
if [[ "$IS_BOT" == "true" ]]; then
echo " [skipping bot PR"]
continue
fi

PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')
PR_URL=$(echo "$PR_JSON" | jq -r '.url')
PR_BODY=$(echo "$PR_JSON" | jq -r '.body')
PR_TITLE=$(jq -r '.title' <<< "$PR_JSON")
PR_URL=$(jq -r '.url' <<< "$PR_JSON")
PR_BODY=$(jq -r '.body' <<< "$PR_JSON")
echo " - $PR_TITLE"

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

# Extract release note block, this contains the release notes and the release notes headers.
RELEASE_NOTE_BLOCK=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p')
# The last sed call is required to remove the carriage return characters (Github seems to use \r\n for new lines in PR bodies).
RELEASE_NOTE_BLOCK=$(echo "$PR_BODY" | sed -n '/\*\*Release note\*\*:/,$p' | sed -n '/^```.*$/,/^```$/p' | sed 's/\r//g')
# Extract release notes body
RELEASE_NOTE=$(echo "$RELEASE_NOTE_BLOCK" | sed '1d;$d' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
RELEASE_NOTE_JSON=$("$RELEASE_NOTES_TO_JSON_SCRIPT" <<< "$RELEASE_NOTE_BLOCK")

# skip PRs without release notes
if [[ "$RELEASE_NOTE_JSON" == "[]" ]]; then
echo " [ignoring PR without release notes]"
continue
fi

# Format release notes
# Updating NOTE_ENTRY in the loop does not work because it is executed in a subshell, therefore this workaround via echo.
NOTE_ENTRY="$(
jq -rc 'sort_by(.audience, .type) | .[]' <<< "$RELEASE_NOTE_JSON" | while IFS= read -r note; do
NOTE_TYPE=$(jq -r '.type' <<< "$note" | tr '[:lower:]' '[:upper:]')
NOTE_AUDIENCE=$(jq -r '.audience' <<< "$note" | tr '[:lower:]' '[:upper:]')
NOTE_BODY=$(jq -r '.body' <<< "$note")
echo -en "\n - **[$NOTE_AUDIENCE][$NOTE_TYPE]** $NOTE_BODY"
done
)"

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

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

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

# Append to appropriate section
if [[ -n "${PR_ENTRIES[$TYPE]+x}" ]]; then
Expand Down
80 changes: 80 additions & 0 deletions release-notes-to-json.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash

# This script reads release notes from STDIN and converts them to JSON format, which is then printed to STDOUT.
# Expected input format:
#
# ```<type> <audience>
# <body> (may span multiple lines)
# ```
# (the above block may repeat multiple times)
#
# Output:
# [
# {
# "type": "<type>",
# "audience": "<audience>",
# "body": "<body>"
# },
# ...
# ]
#
# Additional whitespace is ignored, but each release note block must start with ``` followed by two lowercase words,
# and it must end with ``` followed by any amount of whitespace and then a newline or end of input.
#
# Release notes where the body is empty or "NONE" are ignored and not part of the output JSON array.

set -euo pipefail

type=""
audience=""
body=""
rnj='[]'

start_regex='^[[:blank:]]*```[[:blank:]]*([a-z]+)[[:blank:]]+([a-z]+)[[:blank:]]*$'
end_regex='^[[:blank:]]*```[[:blank:]]*$'
empty_regex='^[[:space:]]*$'
line=""

while IFS= read -r line || [[ -n "$line" ]]; do
# check if we are currently reading a release note body
if [[ -n "$type" ]]; then
# check for end of release note block
if [[ "$line" =~ $end_regex ]]; then
# end of release note block
# add release note to JSON array, unless the body is empty or "NONE"
if [[ ! "$body" =~ $empty_regex ]] && [[ "$body" != "NONE" ]]; then
rnj="$(jq '. + [{"type": $type, "audience": $audience, "body": $body}]' --arg type "$type" --arg audience "$audience" --arg body "$body" <<< "$rnj")"
fi
# reset variables
type=""
audience=""
body=""
else
# more body content
# append line to body
if [[ -n "$body" ]]; then
body="$body\n$line"
else
body="$line"
fi
fi
else
# not currently reading a release note body
# check for start of release note block
if [[ "$line" =~ $start_regex ]]; then
# start of release note block
type="${BASH_REMATCH[1]}"
audience="${BASH_REMATCH[2]}"
body=""
else
# invalid line outside of release note block
if [[ -n "$line" ]]; then
# ignore empty lines, log a warning otherwise
echo "unexpected line in release notes: $line" >&2
fi
fi
fi
done

# output JSON array
echo "$rnj"