From 24f71e3e73e63324d4ac99ff4a74de114288f286 Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Sun, 31 May 2026 17:36:48 +0800 Subject: [PATCH 1/3] fix(ci): skip coverage download when unit-tests fail The pr-comment-summary and coverage-gate jobs tried to download the coverage-report artifact unconditionally. When unit-tests failed (or were cancelled), the artifact was never uploaded, causing 'Artifact not found' errors. - Add 'if: needs.unit-tests.result == success' to coverage-gate - Add 'if: needs.unit-tests.result == success' to coverage download step - Summary generation already handles missing coverage gracefully --- .github/workflows/pr-gatekeeper.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-gatekeeper.yml b/.github/workflows/pr-gatekeeper.yml index fc11475..eda18ad 100644 --- a/.github/workflows/pr-gatekeeper.yml +++ b/.github/workflows/pr-gatekeeper.yml @@ -89,6 +89,7 @@ jobs: name: coverage-gate runs-on: ubuntu-latest needs: [unit-tests] + if: ${{ needs.unit-tests.result == 'success' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -227,6 +228,7 @@ jobs: steps: - name: Download coverage uses: actions/download-artifact@v4 + if: ${{ needs.unit-tests.result == 'success' }} with: name: coverage-report path: coverage/ From b40584a9bb999c1d234e73748a489c8bcd9a5d3f Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Sun, 31 May 2026 17:44:49 +0800 Subject: [PATCH 2/3] fix(ci): automate release notes from commit bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use § delimiter to capture full commit body (not just subject line) - Extract and display bullet points from commit descriptions - Strip git metadata (Change-Id, Co-authored-by, etc.) - Fix sed regex for prefix stripping (use [^:]* instead of nested parens) - Fix OTHER_CHANGEStidy typo - Add Other Changes section for test/docs/ci/chore commits Before: release notes showed static 'No improvements in this release' After: release notes show actual commit details with bullet points --- .github/workflows/production-release.yml | 115 ++++++++++++++++++----- 1 file changed, 91 insertions(+), 24 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 98d5eaf..2a2ea71 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -256,8 +256,8 @@ jobs: PREV_DISPLAY="(initial)" fi - # Collect all data - ALL_COMMITS=$(git log ${COMMIT_RANGE} --pretty=format:"%h|%s|%an|%ae" --no-merges 2>/dev/null) + # Collect all data — use § as commit separator, | as field separator + ALL_COMMITS=$(git log ${COMMIT_RANGE} --pretty=format:"%h§%s§%b§%an§%ae" --no-merges 2>/dev/null) COMMIT_COUNT=$(git log ${COMMIT_RANGE} --oneline --no-merges 2>/dev/null | wc -l) FILES_CHANGED=$(git diff --stat ${COMMIT_RANGE} 2>/dev/null | tail -1) ADDITIONS=$(echo "$FILES_CHANGED" | grep -oP '\d+(?= insertion)' || echo "0") @@ -307,31 +307,96 @@ jobs: BREAKING="" OTHER_CHANGES="" - while IFS='|' read -r hash subject author; do - [ -z "$hash" ] && continue - if echo "$subject" | grep -qiE "^feat"; then - CLEAN=$(echo "$subject" | sed -E 's/^feat(\([^)]*\))?!?:[ ]*//') - FEATURES="${FEATURES}${CLEAN} (#${hash} -- @${author})\n" - elif echo "$subject" | grep -qiE "^fix"; then - CLEAN=$(echo "$subject" | sed -E 's/^fix(\([^)]*\))?!?:[ ]*//') - BUG_FIXES="${BUG_FIXES}Fixed ${CLEAN}. (#${hash})\n" - elif echo "$subject" | grep -qiE "^refactor"; then - CLEAN=$(echo "$subject" | sed -E 's/^refactor(\([^)]*\))?!?:[ ]*//') - IMPROVEMENTS="${IMPROVEMENTS}Refactored ${CLEAN}. (#${hash})\n" - elif echo "$subject" | grep -qiE "^perf"; then - CLEAN=$(echo "$subject" | sed -E 's/^perf(\([^)]*\))?!?:[ ]*//') - IMPROVEMENTS="${IMPROVEMENTS}Optimized ${CLEAN}. (#${hash})\n" - elif echo "$subject" | grep -qiE "^BREAKING|^feat!"; then - CLEAN=$(echo "$subject" | sed -E 's/^(BREAKING CHANGE|feat!)!?:[ ]*//') - BREAKING="${BREAKING}[Change] -- ${CLEAN}\n" + while IFS= read -r line; do + [ -z "$line" ] && continue + + # Parse: hash§subject§body§author§email + HASH=$(echo "$line" | cut -d'§' -f1) + SUBJECT=$(echo "$line" | cut -d'§' -f2) + BODY=$(echo "$line" | cut -d'§' -f3- | sed 's/§.*//') + AUTHOR=$(echo "$line" | cut -d'§' -f4) + + [ -z "$HASH" ] && continue + + # Extract prefix type and clean subject + PREFIX_TYPE="" + CLEAN="" + + if echo "$SUBJECT" | grep -qiE "^feat"; then + PREFIX_TYPE="feat" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^feat[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^fix"; then + PREFIX_TYPE="fix" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^fix[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^refactor"; then + PREFIX_TYPE="refactor" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^refactor[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^perf"; then + PREFIX_TYPE="perf" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^perf[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^test"; then + PREFIX_TYPE="test" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^test[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^docs"; then + PREFIX_TYPE="docs" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^docs[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^ci"; then + PREFIX_TYPE="ci" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^ci[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^chore"; then + PREFIX_TYPE="chore" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^chore[^:]*:[ ]*//') + elif echo "$SUBJECT" | grep -qiE "^BREAKING|^feat!"; then + PREFIX_TYPE="breaking" + CLEAN=$(echo "$SUBJECT" | sed -E 's/^(BREAKING CHANGE|feat!)!?:[ ]*//') else - CLEAN=$(echo "$subject" | sed -E 's/^[a-z]+(\([^)]*\))?!?:[ ]*//') - OTHER_CHANGES="${OTHER_CHANGEStidy}- ${CLEAN} (#${hash})\n" + PREFIX_TYPE="other" + CLEAN="$SUBJECT" fi + + # Fallback: if sed didn't strip the prefix, use the full subject + if [ -z "$CLEAN" ]; then + CLEAN="$SUBJECT" + fi + + # Format body: extract meaningful lines (skip empty, skip commit msg separators) + FORMATTED_BODY="" + if [ -n "$BODY" ]; then + FORMATTED_BODY=$(echo "$BODY" | sed '/^[[:space:]]*$/d' | sed '/^Change-Id:/d' | sed '/^Co-authored-by:/d' | sed '/^Reviewed-by:/d' | sed '/^Signed-off-by:/d' | sed 's/^[[:space:]]*//') + fi + + # Build entry: title + optional body bullets + ENTRY="- ${CLEAN} (#${HASH})" + if [ -n "$FORMATTED_BODY" ]; then + ENTRY="${ENTRY}\n" + while IFS= read -r bodyline; do + [ -z "$bodyline" ] && continue + ENTRY="${ENTRY} ${bodyline}\n" + done <<< "$FORMATTED_BODY" + fi + + # Categorize into sections + case "$PREFIX_TYPE" in + feat) + FEATURES="${FEATURES}${ENTRY}\n" + ;; + fix) + BUG_FIXES="${BUG_FIXES}${ENTRY}\n" + ;; + refactor|perf) + IMPROVEMENTS="${IMPROVEMENTS}${ENTRY}\n" + ;; + breaking) + BREAKING="${BREAKING}${ENTRY}\n" + ;; + *) + OTHER_CHANGES="${OTHER_CHANGES}${ENTRY}\n" + ;; + esac done <<< "$ALL_COMMITS" # Build summary from first meaningful commit - SUMMARY=$(echo "$ALL_COMMITS" | head -1 | cut -d'|' -f2) + SUMMARY=$(echo "$ALL_COMMITS" | head -1 | cut -d'§' -f2) if [ -z "$SUMMARY" ]; then SUMMARY="Production deployment with latest fixes and improvements." fi @@ -374,9 +439,11 @@ jobs: $([ -n "$BUG_FIXES" ] && echo -e "$BUG_FIXES" || echo "_No bug fixes in this release._") - ### Performance + --- + + ## Other Changes - _Performance benchmarks to be added in future releases._ + $([ -n "$OTHER_CHANGES" ] && echo -e "$OTHER_CHANGES" || echo "_No other changes in this release._") --- From 081600822478544792696fad453ed2280d0ed7c2 Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Sun, 31 May 2026 17:58:17 +0800 Subject: [PATCH 3/3] fix(ci): reliable release notes parsing with awk-based categorization - Replace fragile bash while-loop parsing with single awk script - Correctly extracts hash, subject, and multi-line body from git log - Strips conventional commit prefixes (feat, fix, refactor, perf, etc.) - Indents body bullet points under each commit entry - Strips git metadata (Change-Id, Co-authored-by, etc.) - Handles nested parentheses in scopes like fix(security): - Fixes OTHER_CHANGEStidy typo from original code --- .github/workflows/production-release.yml | 161 ++++++++++------------- 1 file changed, 70 insertions(+), 91 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 2a2ea71..4b9e5b1 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -256,8 +256,9 @@ jobs: PREV_DISPLAY="(initial)" fi - # Collect all data — use § as commit separator, | as field separator - ALL_COMMITS=$(git log ${COMMIT_RANGE} --pretty=format:"%h§%s§%b§%an§%ae" --no-merges 2>/dev/null) + # Collect all data — use unique separator between commits + # Each commit block: hash|subject|body (until next separator) + ALL_COMMITS=$(git log ${COMMIT_RANGE} --pretty=format:"COMMIT_SEP%n%h%n%s%n%b" --no-merges 2>/dev/null) COMMIT_COUNT=$(git log ${COMMIT_RANGE} --oneline --no-merges 2>/dev/null | wc -l) FILES_CHANGED=$(git diff --stat ${COMMIT_RANGE} 2>/dev/null | tail -1) ADDITIONS=$(echo "$FILES_CHANGED" | grep -oP '\d+(?= insertion)' || echo "0") @@ -300,103 +301,81 @@ jobs: CONTRIBUTORS="github-actions[bot]" fi - # Categorize commits + # Categorize commits using awk for reliable multi-line parsing + echo "$ALL_COMMITS" | awk ' + /^COMMIT_SEP$/ { + if (n > 0 && hash != "") { + # Categorize previous commit + categorize() + } + hash = ""; subject = ""; body = "" + getline hash + getline subject + n++ + next + } + { body = body (body ? "\n" : "") $0 } + END { + if (n > 0 && hash != "") categorize() + } + function categorize() { + prefix = "other" + clean = subject + if (subject ~ /^feat/) { prefix = "feat"; sub(/^feat[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^fix/) { prefix = "fix"; sub(/^fix[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^refactor/) { prefix = "refactor"; sub(/^refactor[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^perf/) { prefix = "perf"; sub(/^perf[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^test/) { prefix = "test"; sub(/^test[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^docs/) { prefix = "docs"; sub(/^docs[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^ci/) { prefix = "ci"; sub(/^ci[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^chore/) { prefix = "chore"; sub(/^chore[^:]*:[ ]*/, "", clean) } + else if (subject ~ /^BREAKING/ || subject ~ /^feat!/) { prefix = "breaking"; sub(/^(BREAKING CHANGE|feat!)!?:[ ]*/, "", clean) } + + if (clean == "") clean = subject + + # Format body: strip empty lines and git metadata + formatted = "" + if (body != "") { + split(body, blines, "\n") + for (i in blines) { + line = blines[i] + gsub(/^[[:space:]]+/, "", line) + if (line == "") continue + if (line ~ /^Change-Id:/) continue + if (line ~ /^Co-authored-by:/) continue + if (line ~ /^Reviewed-by:/) continue + if (line ~ /^Signed-off-by:/) continue + formatted = formatted (formatted ? "\n" : "") " " line + } + } + + entry = "- " clean " (#" hash ")" + if (formatted != "") entry = entry "\n" formatted + + print prefix "|" entry + } + ' > /tmp/commit-categorized.txt + + # Parse categorized output into variables FEATURES="" IMPROVEMENTS="" BUG_FIXES="" BREAKING="" OTHER_CHANGES="" - while IFS= read -r line; do - [ -z "$line" ] && continue - - # Parse: hash§subject§body§author§email - HASH=$(echo "$line" | cut -d'§' -f1) - SUBJECT=$(echo "$line" | cut -d'§' -f2) - BODY=$(echo "$line" | cut -d'§' -f3- | sed 's/§.*//') - AUTHOR=$(echo "$line" | cut -d'§' -f4) - - [ -z "$HASH" ] && continue - - # Extract prefix type and clean subject - PREFIX_TYPE="" - CLEAN="" - - if echo "$SUBJECT" | grep -qiE "^feat"; then - PREFIX_TYPE="feat" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^feat[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^fix"; then - PREFIX_TYPE="fix" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^fix[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^refactor"; then - PREFIX_TYPE="refactor" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^refactor[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^perf"; then - PREFIX_TYPE="perf" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^perf[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^test"; then - PREFIX_TYPE="test" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^test[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^docs"; then - PREFIX_TYPE="docs" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^docs[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^ci"; then - PREFIX_TYPE="ci" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^ci[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^chore"; then - PREFIX_TYPE="chore" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^chore[^:]*:[ ]*//') - elif echo "$SUBJECT" | grep -qiE "^BREAKING|^feat!"; then - PREFIX_TYPE="breaking" - CLEAN=$(echo "$SUBJECT" | sed -E 's/^(BREAKING CHANGE|feat!)!?:[ ]*//') - else - PREFIX_TYPE="other" - CLEAN="$SUBJECT" - fi - - # Fallback: if sed didn't strip the prefix, use the full subject - if [ -z "$CLEAN" ]; then - CLEAN="$SUBJECT" - fi - - # Format body: extract meaningful lines (skip empty, skip commit msg separators) - FORMATTED_BODY="" - if [ -n "$BODY" ]; then - FORMATTED_BODY=$(echo "$BODY" | sed '/^[[:space:]]*$/d' | sed '/^Change-Id:/d' | sed '/^Co-authored-by:/d' | sed '/^Reviewed-by:/d' | sed '/^Signed-off-by:/d' | sed 's/^[[:space:]]*//') - fi - - # Build entry: title + optional body bullets - ENTRY="- ${CLEAN} (#${HASH})" - if [ -n "$FORMATTED_BODY" ]; then - ENTRY="${ENTRY}\n" - while IFS= read -r bodyline; do - [ -z "$bodyline" ] && continue - ENTRY="${ENTRY} ${bodyline}\n" - done <<< "$FORMATTED_BODY" - fi - - # Categorize into sections - case "$PREFIX_TYPE" in - feat) - FEATURES="${FEATURES}${ENTRY}\n" - ;; - fix) - BUG_FIXES="${BUG_FIXES}${ENTRY}\n" - ;; - refactor|perf) - IMPROVEMENTS="${IMPROVEMENTS}${ENTRY}\n" - ;; - breaking) - BREAKING="${BREAKING}${ENTRY}\n" - ;; - *) - OTHER_CHANGES="${OTHER_CHANGES}${ENTRY}\n" - ;; + while IFS='|' read -r ptype entry; do + [ -z "$ptype" ] && continue + case "$ptype" in + feat) FEATURES="${FEATURES}${entry}"$'\n' ;; + fix) BUG_FIXES="${BUG_FIXES}${entry}"$'\n' ;; + refactor|perf) IMPROVEMENTS="${IMPROVEMENTS}${entry}"$'\n' ;; + breaking) BREAKING="${BREAKING}${entry}"$'\n' ;; + *) OTHER_CHANGES="${OTHER_CHANGES}${entry}"$'\n' ;; esac - done <<< "$ALL_COMMITS" + done < /tmp/commit-categorized.txt - # Build summary from first meaningful commit - SUMMARY=$(echo "$ALL_COMMITS" | head -1 | cut -d'§' -f2) + # Build summary from first commit's subject line + SUMMARY=$(echo "$ALL_COMMITS" | tr '\0' '\n' | sed -n '2p') if [ -z "$SUMMARY" ]; then SUMMARY="Production deployment with latest fixes and improvements." fi