88on :
99 pull_request :
1010 types : [opened, edited, synchronize, labeled, unlabeled, reopened]
11-
12- permissions :
13- contents : read
14- pull-requests : write
15- issues : write
16-
1711jobs :
1812 pr-checklist :
19- runs-on : ubuntu-latest
20-
21- steps :
22- - name : Validate PR quality
23- id : validate
24- env :
25- TITLE : ${{ github.event.pull_request.title }}
26- BODY : ${{ github.event.pull_request.body }}
27- LABELS_CSV : ${{ join(github.event.pull_request.labels.*.name, ',') }}
28- run : |
29- set -euo pipefail
30-
31- failures=""
32-
33- # ---- Settings ----
34- MIN_WORDS=5
35- MAX_WORDS=18
36- TITLE_BYPASS_LABEL="pr:ignore-for-release"
37-
38- has_label () {
39- case ",${LABELS_CSV}," in
40- *,"$1",*) return 0 ;;
41- *) return 1 ;;
42- esac
43- }
44-
45- has_any_pr_label () {
46- IFS=',' read -ra LBL <<< "${LABELS_CSV}"
47- for l in "${LBL[@]}"; do
48- l="$(echo "$l" | xargs)"
49- [[ $l == pr:* ]] && return 0
50- done
51- return 1
52- }
53-
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
58- 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.)"
60- fi
61- fi
62-
63- # --- 2) Has pr:* label
64- if ! has_any_pr_label; then
65- failures="${failures}\n- Missing required label: at least one label starting with \`pr:\`."
66- fi
67-
68- # --- 3) Sections non-empty
69- section_nonempty () {
70- local hdr="$1"
71- local section
72- section="$(printf "%s" "$BODY" | awk -v h="^###[[:space:]]*$hdr[[:space:]]*$" '
73- BEGIN { insec=0 }
74- $0 ~ h { insec=1; next }
75- insec && $0 ~ /^##[[:space:]]/ { insec=0 }
76- insec { print }
77- ')"
78- section="$(printf "%s" "$section" \
79- | sed -E 's/<!--(.|\n)*?-->//g' \
80- | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \
81- | sed '/^[[:space:]]*$/d')"
82- [ -n "$section" ]
83- }
84-
85- for hdr in Goal Implementation Testing; do
86- if ! section_nonempty "$hdr"; then
87- failures="${failures}\n- Section **${hdr}** is missing or empty."
88- fi
89- done
90-
91- if [ -n "$failures" ]; then
92- echo "has_failures=true" >> "$GITHUB_OUTPUT"
93- {
94- echo 'failures<<EOF'
95- printf "%b\n" "$failures"
96- echo 'EOF'
97- } >> "$GITHUB_OUTPUT"
98- exit 1
99- else
100- echo "has_failures=false" >> "$GITHUB_OUTPUT"
101- fi
102-
103- # Compute the latest sticky comment id (by our anchor). If none, output is empty.
104- - name : Compute sticky comment id
105- if : always()
106- id : sticky_comment
107- uses : actions/github-script@v7
108- with :
109- script : |
110- const anchor = "<!-- pr-quality-anchor -->"; // must match the body below
111- const { owner, repo } = context.repo;
112- const issue_number = context.payload.pull_request.number;
113-
114- // Get up to 100 comments; paginate to be safe.
115- const comments = await github.paginate(
116- github.rest.issues.listComments,
117- { owner, repo, issue_number, per_page: 100 }
118- );
119-
120- const matches = comments.filter(c => (c.body || "").includes(anchor));
121- if (matches.length === 0) {
122- core.setOutput("comment_id", "");
123- return;
124- }
125- // Pick the most recently updated one
126- matches.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
127- core.setOutput("comment_id", String(matches[0].id));
128-
129- - name : Sticky comment id
130- if : always()
131- run : echo "sticky comment-id=${{ steps.sticky_comment.outputs.comment_id }}"
132-
133- # Update/create the sticky comment on failure
134- - name : Create or update failure comment
135- if : failure()
136- uses : peter-evans/create-or-update-comment@v4
137- with :
138- issue-number : ${{ github.event.pull_request.number }}
139- comment-id : ${{ steps.sticky_comment.outputs.comment_id }} # empty => creates new
140- edit-mode : replace
141- body : |
142- <!-- pr-quality-anchor -->
143- ### PR checklist ❌
144-
145- The following issues were detected:
146-
147- ${{ steps.validate.outputs.failures }}
148-
149- **What we check**
150- 1. Title is concise (5–18 words) unless labeled `pr:ignore-for-release`.
151- 2. At least one `pr:` label exists (e.g., `pr:bug`, `pr:new-feature`).
152- 3. Sections `### Goal`, `### Implementation`, and `### Testing` contain content.
153-
154- # Flip the same sticky comment to ✅ on success
155- - name : Create or update success comment
156- if : success()
157- uses : peter-evans/create-or-update-comment@v4
158- with :
159- issue-number : ${{ github.event.pull_request.number }}
160- comment-id : ${{ steps.sticky_comment.outputs.comment_id }} # updates if found
161- edit-mode : replace
162- body : |
163- <!-- pr-quality-anchor -->
164- ### PR checklist ✅
165-
166- All required conditions are satisfied:
167- - Title length is OK (or ignored by label).
168- - At least one `pr:` label exists.
169- - Sections `### Goal`, `### Implementation`, and `### Testing` are filled.
170-
171- 🎉 Great job! This PR is ready for review.
13+ uses : GetStream/android-ci-actions/.github/workflows/pr-quality.yml@main
0 commit comments