11#! /bin/bash
2-
2+ set -euo pipefail
33# ==============================================================================
44# Executes When:
55# - Run by GitHub Actions workflow: .github/workflows/pr-check-changelog.yml
6161#
6262# Dependencies:
6363# - git (must be able to fetch upstream)
64+ # - jq (required for PR context parsing)
6465# - grep, sed (standard Linux utilities)
6566# - CHANGELOG.md (file must exist in the root directory)
6667#
7677
7778CHANGELOG=" CHANGELOG.md"
7879
80+ # Ensure jq is available (required for PR context + comments)
81+ command -v jq > /dev/null || {
82+ echo " ❌ jq is required but not installed"
83+ exit 1
84+ }
85+
86+ # Validate required environment variables
87+ : " ${GITHUB_EVENT_PATH:? GITHUB_EVENT_PATH is not set} "
88+ : " ${GITHUB_REPOSITORY:? GITHUB_REPOSITORY is not set} "
89+
90+ # PR Number
91+ PR_NUMBER=$( jq -r ' .pull_request.number // empty' " $GITHUB_EVENT_PATH " )
92+ if [[ -n " $PR_NUMBER " ]] && ! [[ " $PR_NUMBER " =~ ^[0-9]+$ ]]; then
93+ echo " ❌ Invalid PR_NUMBER: $PR_NUMBER "
94+ exit 1
95+ fi
96+
97+ # GITHUB_TOKEN is only required for PR commenting
98+ if [[ -n " $PR_NUMBER " ]]; then
99+ : " ${GITHUB_TOKEN:? GITHUB_TOKEN is required for PR commenting but is not set} "
100+ fi
101+
102+ # Marker
103+ MISSING_MARKER=" <!-- changelog-missing-bot -->"
104+ WRONG_SECTION_MARKER=" <!-- changelog-wrong-section-bot -->"
105+
79106# ANSI color codes
80107RED=" \033[31m"
81108GREEN=" \033[32m"
@@ -88,6 +115,57 @@ failed=0
88115git remote add upstream https://github.com/${GITHUB_REPOSITORY} .git
89116git fetch upstream main > /dev/null 2>&1
90117
118+ # PR Comment
119+ DRY_RUN=" ${DRY_RUN:- false} "
120+ post_pr_comment () {
121+ local message=" $1 "
122+
123+ # Only comment if this is a PR (not workflow_dispatch)
124+ if [[ -z " $PR_NUMBER " ]]; then
125+ echo " ℹ️ No PR_NUMBER set — skipping comment."
126+ return
127+ fi
128+
129+ if [[ " $DRY_RUN " == " true" ]]; then
130+ echo " 🔍 [DRY RUN] Would post PR comment on #${PR_NUMBER} "
131+ return
132+ fi
133+
134+ local http_code
135+ http_code=$( curl -sS -o /dev/null -w " %{http_code}" -X POST \
136+ -H " Authorization: Bearer $GITHUB_TOKEN " \
137+ -H " Accept: application/vnd.github+json" \
138+ " https://api.github.com/repos/${GITHUB_REPOSITORY} /issues/${PR_NUMBER} /comments" \
139+ -d " $( jq -n --arg body " $message " ' {body: $body}' ) " )
140+
141+ if [[ " $http_code " -lt 200 || " $http_code " -ge 300 ]]; then
142+ echo " ⚠️ Failed to post PR comment (HTTP $http_code )"
143+ fi
144+ }
145+
146+ comment_already_exists () {
147+ local marker=" $1 "
148+
149+ if [[ -z " $PR_NUMBER " ]]; then
150+ return 1
151+ fi
152+
153+ local comments
154+ comments=$( curl -s \
155+ -H " Authorization: Bearer $GITHUB_TOKEN " \
156+ -H " Accept: application/vnd.github+json" \
157+ " https://api.github.com/repos/${GITHUB_REPOSITORY} /issues/${PR_NUMBER} /comments?per_page=100" )
158+
159+ if ! echo " $comments " | jq -e ' type == "array"' > /dev/null 2>&1 ; then
160+ echo " ⚠️ Failed to fetch PR comments — assuming marker absent"
161+ return 1
162+ fi
163+
164+ echo " $comments " | jq -e --arg marker " $marker " '
165+ .[] | select(.body | contains($marker))
166+ ' > /dev/null
167+ }
168+
91169# Get raw diff
92170raw_diff=$( git diff upstream/main -- " $CHANGELOG " )
93171
@@ -119,7 +197,21 @@ done < <(echo "$raw_diff" | grep '^\-' | grep -vE '^(--- |\+\+\+ |@@ )' | sed 's
119197# 2️⃣b Warn if no added entries
120198if [[ ${# added_bullets[@]} -eq 0 ]]; then
121199 echo -e " ${RED} ❌ No new changelog entries detected in this PR.${RESET} "
122- echo -e " ${YELLOW} ⚠️ Please add an entry in [UNRELEASED] under the appropriate subheading.${RESET} "
200+ echo -e " ${YELLOW} ⚠️ Please add an entry in [Unreleased] under the appropriate subheading.${RESET} "
201+
202+ if ! comment_already_exists " $MISSING_MARKER " ; then
203+ post_pr_comment " $MISSING_MARKER
204+ 👋 **Changelog reminder**
205+
206+ This PR appears to resolve an issue, but no entry was found in **CHANGELOG.md**.
207+
208+ 📌 In this repository, all resolved issues — including docs, CI, and workflow changes — are expected to include a changelog entry under **[Unreleased]**, grouped by category (e.g. *Added*, *Fixed*).
209+
210+ If this PR should not require a changelog entry, feel free to clarify in the discussion. Thanks! 🙌"
211+ else
212+ echo " ⚠️ Changelog bot comment already exists, skipping"
213+ fi
214+
123215 failed=1
124216fi
125217
@@ -133,15 +225,17 @@ current_release=""
133225current_subtitle=" "
134226in_unreleased=0
135227
228+ shopt -s extglob
229+
136230while IFS= read -r line; do
137231 # Track release sections
138232 if [[ $line =~ ^# #\ \[Unreleased\] ]]; then
139233 current_release= " Unreleased"
140234 in_unreleased= 1
141235 current_subtitle= " "
142236 continue
143- elif [[ $line =~ ^# # \ \[.* \] ]]; then
144- current_release= " $line "
237+ elif [[ $line =~ ^#{1,2} \ \[ ([0-9]+ \. [0-9]+ \. [0-9]+) \] ]]; then
238+ current_release= " ${BASH_REMATCH[1]} "
145239 in_unreleased= 0
146240 current_subtitle= " "
147241 continue
@@ -151,14 +245,18 @@ while IFS= read -r line; do
151245 fi
152246
153247 # Check each added bullet
248+
249+ normalized_line= " ${line%% +([[:space:]])} "
154250 for added in " ${added_bullets[@]} " ; do
155- if [[ " $line " == " $added " ]]; then
251+ normalized_added=" ${added%% +([[:space:]])} "
252+
253+ if [[ " $normalized_line " == " $normalized_added " ]]; then
156254 if [[ " $in_unreleased " -eq 1 && -n " $current_subtitle " ]]; then
157255 correctly_placed+=" $added (placed under $current_subtitle )" $' \n '
158256 elif [[ " $in_unreleased " -eq 1 && -z " $current_subtitle " ]]; then
159257 orphan_entries+=" $added (NOT under a subtitle)" $' \n '
160258 elif [[ " $in_unreleased " -eq 0 ]]; then
161- wrong_release_entries+=" $added (added under released version $current_release )" $' \n '
259+ wrong_release_entries+=" $added (added under released version [ $current_release ], expected under [Unreleased] )" $' \n '
162260 fi
163261 fi
164262 done
174272if [[ -n " $wrong_release_entries " ]]; then
175273 echo -e " ${RED} ❌ Some changelog entries were added under a released version (should be in [Unreleased]):${RESET} "
176274 echo " $wrong_release_entries "
275+
276+ if ! comment_already_exists " $WRONG_SECTION_MARKER " ; then
277+ post_pr_comment " $WRONG_SECTION_MARKER
278+ ⚠️ **Changelog placement issue**
279+
280+ Thanks for adding a changelog entry! 🙌
281+ However, one or more entries in this PR were added under a **released version**.
282+
283+ 📌 New changelog entries should always go under **[Unreleased]**, grouped beneath an appropriate category (e.g. *Added*, *Fixed*).
284+
285+ Please move the following entries to **[Unreleased]**:
286+
287+ \`\`\`
288+ $wrong_release_entries
289+ \`\`\`
290+
291+
292+ Let us know if you’re unsure where it should live — happy to help!"
293+
294+ else
295+ echo " ⚠️ Changelog bot comment already exists, skipping"
296+ fi
297+
177298 failed=1
178299fi
179300
0 commit comments