1- # .github/workflows/pr-quality-check .yml
1+ # .github/workflows/pr-checklist .yml
22name : PR Quality check
33
4- # Add a constant anchor we can always find
5- env :
6- PR_QUALITY_ANCHOR : " <!-- pr-quality-anchor -->"
7-
84on :
9- pull_request :
5+ pull_request_target :
106 types : [opened, edited, synchronize, labeled, unlabeled, reopened]
117
128permissions :
139 contents : read
1410 pull-requests : write
1511 issues : write
1612
13+ env :
14+ # Hidden marker to always find/update the same comment
15+ PR_CHECKLIST_ANCHOR : " <!-- pr-checklist-anchor -->"
16+ # Title bypass label for release-notes length rule
17+ TITLE_BYPASS_LABEL : " pr:ignore-for-release"
18+ # Title word limits (for release notes)
19+ MIN_TITLE_WORDS : " 5"
20+ MAX_TITLE_WORDS : " 12"
21+
1722jobs :
18- pr-quality :
23+ pr-checklist :
1924 runs-on : ubuntu-latest
2025
2126 steps :
22- - name : Validate PR quality
27+ - name : Validate PR
2328 id : validate
2429 env :
2530 TITLE : ${{ github.event.pull_request.title }}
2631 BODY : ${{ github.event.pull_request.body }}
2732 LABELS_CSV : ${{ join(github.event.pull_request.labels.*.name, ',') }}
33+ MIN_WORDS : ${{ env.MIN_TITLE_WORDS }}
34+ MAX_WORDS : ${{ env.MAX_TITLE_WORDS }}
35+ BYPASS : ${{ env.TITLE_BYPASS_LABEL }}
2836 run : |
2937 set -euo pipefail
3038
3139 failures=""
3240
33- # ---- Settings ----
34- MIN_WORDS=5
35- MAX_WORDS=12
36- TITLE_BYPASS_LABEL="pr:ignore-for-release"
37-
3841 has_label () {
3942 case ",${LABELS_CSV}," in
4043 *,"$1",*) return 0 ;;
@@ -51,30 +54,34 @@ jobs:
5154 return 1
5255 }
5356
54- # --- 1) Title check
55- if ! has_label "$TITLE_BYPASS_LABEL "; then
56- title_words=$(echo "$TITLE" | tr -s '[:space:]' ' ' | sed -e 's/^ *//' -e 's/ *$//' | wc -w | xargs)
57- if [ -z "$title_words" ]; then title_words=0; fi
57+ # ---- 1) Title length (unless bypass label present)
58+ if ! has_label "$BYPASS "; then
59+ title_words=$(echo "$TITLE" | tr -s '[:space:]' ' ' | sed -e 's/^ *//' -e 's/ *$//' | wc -w | xargs || true )
60+ if [ -z "${ title_words:-} " ]; then title_words=0; fi
5861 if [ "$title_words" -lt "$MIN_WORDS" ] || [ "$title_words" -gt "$MAX_WORDS" ]; then
59- failures="${failures}\n- **Title** should be ${MIN_WORDS}–${MAX_WORDS} words for release notes . Current: ${title_words} word(s) . (Add \`${TITLE_BYPASS_LABEL} \` to bypass.)"
62+ failures="${failures}\n- **Title** should be concise for release notes: ${MIN_WORDS}–${MAX_WORDS} words. Current: ${title_words}. (Add \`$BYPASS \` to bypass.)"
6063 fi
6164 fi
6265
63- # --- 2) Has pr:* label
66+ # ---- 2) At least one pr:* label
6467 if ! has_any_pr_label; then
65- failures="${failures}\n- Missing required label: at least one label starting with \`pr:\` ."
68+ failures="${failures}\n- Missing required label: at least one \`pr:\` label (e.g., \`pr:new-feature\`, \`pr:bug\`) ."
6669 fi
6770
68- # --- 3) Sections non-empty
71+ # ---- 3) Required sections (### headers): Goal / Implementation / Testing
72+ BODY="${BODY:-}"
73+
6974 section_nonempty () {
75+ # Extract text under '### <Header>' until next '###' (or end)
7076 local hdr="$1"
7177 local section
72- section="$(printf "%s" "$BODY" | awk -v h="^##[[:space:]]*$hdr[[:space:]]*$" '
78+ section="$(printf "%s" "$BODY" | awk -v h="^### [[:space:]]*$hdr[[:space:]]*$" '
7379 BEGIN { insec=0 }
7480 $0 ~ h { insec=1; next }
75- insec && $0 ~ /^##[[:space:]]/ { insec=0 }
81+ insec && $0 ~ /^### [[:space:]]/ { insec=0 }
7682 insec { print }
7783 ')"
84+ # Strip HTML comments and whitespace-only lines
7885 section="$(printf "%s" "$section" \
7986 | sed -E 's/<!--(.|\n)*?-->//g' \
8087 | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \
8491
8592 for hdr in Goal Implementation Testing; do
8693 if ! section_nonempty "$hdr"; then
87- failures="${failures}\n- Section **${hdr}** is missing or empty."
94+ failures="${failures}\n- Section **${hdr}** is missing or empty (use \`### ${hdr}\` with some content) ."
8895 fi
8996 done
9097
@@ -95,19 +102,22 @@ jobs:
95102 printf "%b\n" "$failures"
96103 echo 'EOF'
97104 } >> "$GITHUB_OUTPUT"
105+ # Fail job to block merging if branch protection requires this check
98106 exit 1
99107 else
100108 echo "has_failures=false" >> "$GITHUB_OUTPUT"
101109 fi
102110
103- - name : Find existing PR Quality Check comment
111+ # Find sticky comment by anchor (no author filter → works for forks)
112+ - name : Find PR Checklist comment
104113 id : find_comment
105114 uses : peter-evans/find-comment@v3
106115 with :
107116 issue-number : ${{ github.event.pull_request.number }}
108- comment-author : github-actions[bot]
109- body-includes : ${{ env.PR_QUALITY_ANCHOR }}
117+ body-includes : ${{ env.PR_CHECKLIST_ANCHOR }}
118+ direction : last
110119
120+ # Post or update the sticky comment to ❌ with details
111121 - name : Create or update failure comment
112122 if : failure()
113123 uses : peter-evans/create-or-update-comment@v4
@@ -116,18 +126,21 @@ jobs:
116126 comment-id : ${{ steps.find_comment.outputs.comment-id }}
117127 edit-mode : replace
118128 body : |
119- <!-- pr-quality -anchor -->
120- ### PR Quality Check ❌ Failed
129+ <!-- pr-checklist -anchor -->
130+ ### PR Checklist ❌
121131
122132 The following issues were detected:
123133
124134 ${{ steps.validate.outputs.failures }}
125135
126- **What we check**
127- 1. Title is concise (3–12 words) unless labeled `pr:ignore-for-release`.
128- 2. At least one `pr:` label exists (e.g., `pr:bug`, `pr:new-feature`).
129- 3. Sections `## Goal`, `## Implementation`, and `## Testing` contain content.
136+ **We check**
137+ 1. Title is concise (**${{ env.MIN_TITLE_WORDS }}–${{ env.MAX_TITLE_WORDS }} words**) unless labeled `pr:ignore-for-release`.
138+ 2. At least one `pr:` label exists.
139+ 3. Sections `### Goal`, `### Implementation`, and `### Testing` contain content.
140+
141+ _This comment updates automatically when you edit the PR title/body or labels._
130142
143+ # Flip the same sticky comment to ✅ when everything passes
131144 - name : Create or update success comment
132145 if : success()
133146 uses : peter-evans/create-or-update-comment@v4
@@ -136,12 +149,12 @@ jobs:
136149 comment-id : ${{ steps.find_comment.outputs.comment-id }}
137150 edit-mode : replace
138151 body : |
139- <!-- pr-quality -anchor -->
140- ### PR Quality Check ✅ Passed
152+ <!-- pr-checklist -anchor -->
153+ ### PR Checklist ✅
141154
142155 All required conditions are satisfied:
143- - Title length is OK (or ignored by label ).
156+ - Title length is OK (or ignored via `pr:ignore-for-release` ).
144157 - At least one `pr:` label exists.
145- - Sections `## Goal`, `## Implementation`, and `## Testing` are filled.
158+ - Sections `### Goal`, `### Implementation`, and `# ## Testing` are filled.
146159
147- 🎉 Great job! This PR is ready for review.
160+ 🎉 Looks good!
0 commit comments