Skip to content

Commit e935f44

Browse files
committed
Fix and harden PR changelog check script
Signed-off-by: Parth J Chaudhary <parthchaudhary.jc@yahoo.com>
1 parent 84e29f8 commit e935f44

File tree

2 files changed

+124
-79
lines changed

2 files changed

+124
-79
lines changed

.github/scripts/pr-check-changelog.sh

Lines changed: 123 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,90 @@
11
#!/bin/bash
2-
set -euo pipefail
2+
set -uo pipefail
33

44
# ==============================================================================
55
# Executes When:
66
# - Run by GitHub Actions workflow: .github/workflows/pr-check-changelog.yml
7-
# - Triggers: workflow_dispatch (manual) and pull_request (opened, edited, synch).
7+
# - Triggers: pull_request events (opened, edited, synchronized) or manual runs.
88
#
99
# Goal:
10-
# It acts as a gatekeeper for Pull Requests, blocking any merge unless the user
11-
# has added a new entry to CHANGELOG.md and correctly placed it under the
12-
# [Unreleased] section with a proper category subtitle.
10+
# This script enforces CHANGELOG discipline in Pull Requests.
11+
# It blocks merging unless:
12+
# 1) A new changelog entry is added.
13+
# 2) The entry is placed under [Unreleased].
14+
# 3) The entry is under a valid subsection (e.g., ### Added, ### Fixed).
15+
# 4) No previous changelog entries are deleted.
1316
#
1417
# ------------------------------------------------------------------------------
15-
# Flow: Basic Idea
16-
# 1. Grabs the official blueprints (upstream/main) to compare against current work.
17-
# 2. Checks if anything new was written. If not, fails immediately.
18-
# 3. Walks through the file line-by-line to ensure new notes are strictly filed
19-
# under [Unreleased] and organized under a category (e.g., "Added", "Fixed").
20-
# 4. If notes are missing, misplaced, or dangling, it fails the build.
21-
# If filed correctly, it approves the build.
18+
# Flow: High-Level Overview
19+
# 1. Fetch upstream main branch to compare against PR changes.
20+
# 2. Compute diff for CHANGELOG.md.
21+
# 3. Detect added and deleted bullet entries.
22+
# 4. Walk through CHANGELOG.md to verify correct placement.
23+
# 5. Fail CI if entries are missing, misplaced, or removed.
2224
#
2325
# ------------------------------------------------------------------------------
2426
# Flow: Detailed Technical Steps
2527
#
26-
# 1️⃣ Network Setup & Fetch
27-
# - Action: Sets up a remote connection to GitHub and runs 'git fetch upstream main'.
28-
# - Why: Needs the "Source of Truth" to compare the Pull Request against.
28+
# 1️⃣ Upstream Setup & Fetch
29+
# - Ensures 'upstream' remote exists (points to GITHUB_REPOSITORY).
30+
# - Runs: git fetch upstream main
31+
# - Purpose: Compare PR against canonical main branch.
2932
#
30-
# 2️⃣ Diff Analysis & Visualization
31-
# - Action: Runs 'git diff upstream/main -- CHANGELOG.md'.
32-
# - UX/Display: Prints raw diff with colors (Green=Additions, Red=Deletions)
33-
# strictly for human readability in logs; logic does not rely on colors.
34-
# - Logic: Extracts two lists:
35-
# * added_bullets: Every line starting with '+' (new text).
36-
# * deleted_bullets: Every line starting with '-' (removed text).
37-
# - Immediate Fail Check: If 'added_bullets' is empty, sets failed=1 and exits.
38-
# (You cannot merge code without a changelog entry).
33+
# 2️⃣ Diff Extraction & Logging
34+
# - Runs: git diff upstream/main -- CHANGELOG.md
35+
# - Displays color-coded diff:
36+
# Green = Added lines
37+
# Red = Removed lines
38+
# - Extracts:
39+
# * added_bullets -> new bullet lines (+)
40+
# * deleted_bullets -> removed bullet lines (-)
3941
#
40-
# 3️⃣ Context Tracking
41-
# As the script reads the file line-by-line, it tracks:
42-
# - current_release: Main version header (e.g., [Unreleased] or [1.0.0]).
43-
# - current_subtitle: Sub-category (e.g., ### Added, ### Fixed).
44-
# - in_unreleased: Flag (0 or 1).
45-
# * 1 (True) -> Currently inside [Unreleased] (Safe Zone).
46-
# * 0 (False) -> Reading an old version (Danger Zone).
42+
# 3️⃣ Mandatory Entry Check
43+
# - If no new bullet entries are found:
44+
# → FAIL (You must update the changelog in every PR).
4745
#
48-
# 4️⃣ Sorting
49-
# Flag is ON (1) AND Subtitle is Set -> correctly_placed -> PASS ✅
50-
# Flag is ON (1) BUT Subtitle is Empty -> orphan_entries -> FAIL ❌ (It's dangling, not under a category)
51-
# Flag is OFF (0) -> wrong_release_entries -> FAIL ❌ (edited old history)
46+
# 4️⃣ Context Tracking While Parsing File
47+
# Tracks current parsing state:
48+
# - current_release -> version header (e.g., [Unreleased], [1.2.0])
49+
# - current_subtitle -> section header (### Added, ### Fixed, etc.)
50+
# - in_unreleased -> boolean flag (1 if inside [Unreleased])
5251
#
53-
# 5️⃣ Final Result
54-
# Aggregates failures from Step 4. If any FAIL buckets are not empty, exit 1.
52+
# 5️⃣ Classification Rules
53+
# Condition → Result
54+
# -------------------------------------------------------------
55+
# In [Unreleased] AND under subtitle → correctly_placed (PASS)
56+
# In [Unreleased] BUT no subtitle → orphan_entries (FAIL)
57+
# Under released version section → wrong_release_entries (FAIL)
58+
#
59+
# 6️⃣ Deletion Detection
60+
# - If any existing changelog bullet lines are removed:
61+
# → FAIL (History must never be rewritten).
62+
#
63+
# 7️⃣ Exit Behavior
64+
# - If any failure condition is detected:
65+
# exit 1 (CI blocks merge)
66+
# - Otherwise:
67+
# exit 0 (PR passes changelog gate)
5568
#
5669
# ------------------------------------------------------------------------------
5770
# Parameters:
58-
# None. (The script accepts no command-line arguments).
71+
# None (script does not accept CLI arguments).
5972
#
60-
# Environment Variables (Required):
61-
# - GITHUB_REPOSITORY: Used to fetch the upstream 'main' branch for comparison.
73+
# Required Environment Variables:
74+
# - GITHUB_REPOSITORY : Used to configure upstream remote URL.
6275
#
6376
# Dependencies:
64-
# - git (must be able to fetch upstream)
65-
# - grep, sed (standard Linux utilities)
66-
# - CHANGELOG.md (file must exist in the root directory)
77+
# - git
78+
# - grep, sed (POSIX utilities)
79+
# - CHANGELOG.md must exist in repository root.
6780
#
6881
# Permissions:
69-
# - 'contents: read' (to access the file structure).
70-
# - Network access (to run 'git fetch upstream').
82+
# - contents: read
83+
# - network access (for git fetch)
7184
#
7285
# Returns:
73-
# 0 (Success) - Changes are valid and correctly placed.
74-
# 1 (Failure) - Missing entries, wrong placement (e.g. under released version),
75-
# orphan entries (no subtitle), or accidental deletions.
86+
# 0 → Changelog entries valid and correctly placed.
87+
# 1 → Missing entries, wrong placement, orphan entries, or deleted history.
7688
# ==============================================================================
7789

7890
CHANGELOG="CHANGELOG.md"
@@ -86,13 +98,13 @@ RESET="\033[0m"
8698
failed=0
8799

88100
# Fetch upstream
89-
git remote add upstream https://github.com/${GITHUB_REPOSITORY}.git
101+
git remote get-url upstream &>/dev/null || git remote add upstream https://github.com/${GITHUB_REPOSITORY}.git
90102
git fetch upstream main >/dev/null 2>&1
91103

92-
# Get raw diff
93-
raw_diff=$(git diff upstream/main -- "$CHANGELOG")
104+
# Get raw diff (git diff may legitimately return non-zero)
105+
raw_diff=$(git diff upstream/main -- "$CHANGELOG" || true)
94106

95-
# 1️⃣ Show raw diff with colors
107+
# Show raw diff with colors
96108
echo "=== Raw git diff of $CHANGELOG against upstream/main ==="
97109
while IFS= read -r line; do
98110
if [[ $line =~ ^\+ && ! $line =~ ^\+\+\+ ]]; then
@@ -105,37 +117,68 @@ while IFS= read -r line; do
105117
done <<< "$raw_diff"
106118
echo "================================="
107119

108-
# 2️⃣ Extract added bullet lines
109-
added_bullets=()
120+
# Extract added bullet lines
121+
declare -A added_bullets=()
122+
file_line=0
123+
in_hunk=0
124+
110125
while IFS= read -r line; do
111-
[[ -n "$line" ]] && added_bullets+=("$line")
112-
done < <(echo "$raw_diff" | sed -n 's/^+//p' | grep -E '^[[:space:]]*[-*]' | sed '/^[[:space:]]*$/d')
126+
if [[ $line =~ ^\@\@ ]]; then
127+
# Extract starting line number of the new file
128+
file_line=$(echo "$line" | sed -E 's/.*\+([0-9]+).*/\1/')
129+
in_hunk=1
130+
continue
131+
fi
132+
133+
# Ignore lines until we are inside a hunk
134+
if [[ $in_hunk -eq 0 ]]; then
135+
continue
136+
fi
137+
138+
if [[ $line =~ ^\+ && ! $line =~ ^\+\+\+ ]]; then
139+
content="${line#+}"
140+
if [[ $content =~ ^[[:space:]]*[-*] ]]; then
141+
added_bullets[$file_line]="$content"
142+
fi
143+
((file_line++))
144+
elif [[ ! $line =~ ^\- ]]; then
145+
((file_line++))
146+
fi
147+
done <<< "$raw_diff"
113148

114-
# 2️⃣a Extract deleted bullet lines
149+
# Extract deleted bullet lines
115150
deleted_bullets=()
116151
while IFS= read -r line; do
117152
[[ -n "$line" ]] && deleted_bullets+=("$line")
118-
done < <(echo "$raw_diff" | grep '^\-' | grep -vE '^(--- |\+\+\+ |@@ )' | sed 's/^-//')
119-
120-
# 2️⃣b Warn if no added entries
153+
done < <(
154+
echo "$raw_diff" \
155+
| grep '^\-' \
156+
| grep -vE '^(--- |\+\+\+ |@@ )' \
157+
| sed 's/^-//' \
158+
|| true
159+
)
160+
161+
# Warn if no added entries
121162
if [[ ${#added_bullets[@]} -eq 0 ]]; then
122163
echo -e "${RED}❌ No new changelog entries detected in this PR.${RESET}"
123-
echo -e "${YELLOW}⚠️ Please add an entry in [UNRELEASED] under the appropriate subheading.${RESET}"
164+
echo -e "${YELLOW}⚠️ Please add an entry in [Unreleased] under the appropriate subheading.${RESET}"
124165
failed=1
125166
fi
126167

127-
# 3️⃣ Initialize results
168+
# Initialize results
128169
correctly_placed=""
129170
orphan_entries=""
130171
wrong_release_entries=""
131172

132-
# 4️⃣ Walk through changelog to classify entries
173+
# Walk through changelog to classify entries
174+
line_no=0
133175
current_release=""
134176
current_subtitle=""
135177
in_unreleased=0
136178

137179
while IFS= read -r line; do
138-
# Track release sections
180+
((line_no++))
181+
139182
if [[ $line =~ ^##\ \[Unreleased\] ]]; then
140183
current_release="Unreleased"
141184
in_unreleased=1
@@ -151,21 +194,22 @@ while IFS= read -r line; do
151194
continue
152195
fi
153196

154-
# Check each added bullet
155-
for added in "${added_bullets[@]}"; do
156-
if [[ "$line" == "$added" ]]; then
157-
if [[ "$in_unreleased" -eq 1 && -n "$current_subtitle" ]]; then
158-
correctly_placed+="$added (placed under $current_subtitle)"$'\n'
159-
elif [[ "$in_unreleased" -eq 1 && -z "$current_subtitle" ]]; then
160-
orphan_entries+="$added (NOT under a subtitle)"$'\n'
161-
elif [[ "$in_unreleased" -eq 0 ]]; then
162-
wrong_release_entries+="$added (added under released version $current_release)"$'\n'
163-
fi
197+
if [[ -n "${added_bullets[$line_no]:-}" ]]; then
198+
added="${added_bullets[$line_no]}"
199+
200+
if [[ "$in_unreleased" -eq 0 ]]; then
201+
wrong_release_entries+="$added (added under released version $current_release)"$'\n'
202+
failed=1
203+
elif [[ -z "$current_subtitle" ]]; then
204+
orphan_entries+="$added (NOT under a subtitle)"$'\n'
205+
failed=1
206+
else
207+
correctly_placed+="$added (placed under $current_subtitle)"$'\n'
164208
fi
165-
done
209+
fi
166210
done < "$CHANGELOG"
167211

168-
# 5️⃣ Display results
212+
# Display results
169213
if [[ -n "$orphan_entries" ]]; then
170214
echo -e "${RED}❌ Some CHANGELOG entries are not under a subtitle in [Unreleased]:${RESET}"
171215
echo "$orphan_entries"
@@ -183,16 +227,17 @@ if [[ -n "$correctly_placed" ]]; then
183227
echo "$correctly_placed"
184228
fi
185229

186-
# 6️⃣ Display deleted entries
230+
# Display deleted entries
187231
if [[ ${#deleted_bullets[@]} -gt 0 ]]; then
188232
echo -e "${RED}❌ Changelog entries removed in this PR:${RESET}"
189233
for deleted in "${deleted_bullets[@]}"; do
190234
echo -e " - ${RED}$deleted${RESET}"
191235
done
192236
echo -e "${YELLOW}⚠️ Please add these entries back under the appropriate sections${RESET}"
237+
failed=1
193238
fi
194239

195-
# 7️⃣ Exit with failure if any bad entries exist
240+
# Exit
196241
if [[ $failed -eq 1 ]]; then
197242
echo -e "${RED}❌ Changelog check failed.${RESET}"
198243
exit 1

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
136136
- Added comprehensive docstring to `compress_with_cryptography` function (#1626)
137137

138138
### Changed
139-
- Hardened PR changelog check script by enabling strict Bash mode. (#1492)
140139
- Added missing type hints to sign method in Transaction class (#1630)
141140
- Refactored `examples/consensus/topic_create_transaction.py` to use `Client.from_env()` (#1611)
142141
- Updated GitHub Actions setup-node action to v6.2.0.
@@ -227,6 +226,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
227226
- chore: update merge conflict bot message with web editor tips (#1592)
228227

229228
### Fixed
229+
- Correct PR changelog validation logic and harden script with strict Bash mode to prevent false positives and missed entries (#1492)
230230
- Fix the next-issue recommendation bot to post the correct issue recommendation comment. (#1593)
231231
- Ensured that the GFI assignment bot skips posting `/assign` reminders for repository collaborators to avoid unnecessary notifications.(#1568).
232232
- Reduced notification spam by skipping the entire advanced qualification job for non-advanced issues and irrelevant events (#1517)

0 commit comments

Comments
 (0)