-
Notifications
You must be signed in to change notification settings - Fork 129
127 lines (112 loc) · 6.25 KB
/
Copy pathpr-update.yml
File metadata and controls
127 lines (112 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# This workflow uses pull_request_target, which grants write access to
# the repo even for PRs from forks. This is safe ONLY because the checkout
# fetches the base branch (main), not the PR head. Do NOT add:
# - actions/checkout with ref: ${{ github.event.pull_request.head.sha }}
# - run: steps that execute scripts from the PR branch
# - any step that sources or evals PR-controlled content
# Doing so would let a malicious fork run arbitrary code with write permissions.
name: PR Bot
on:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, edited]
jobs:
manage-checklist-comment:
runs-on: ubuntu-latest
# Serialize per-PR runs so rapid edited/opened events can't race —
# without this, two runs could both fetch comments before either
# posts, both see no marker, and both POST → duplicate comments.
concurrency:
group: pr-bot-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout code # Checks out the base branch, not PR branch.
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Sync checklist comment with checked type
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# 1. Detect the first checked type box. Empty string if none.
# -iF treats brackets literally (no regex false positives) and
# matches both [x] and [X], which GitHub renders identically.
# First-match-wins on multiple checked boxes — the template
# asks users to split such PRs, but we warn below if it happens.
CURRENT_TYPE=""
if printf '%s' "$PR_BODY" | grep -qiF '[x] Component'; then CURRENT_TYPE="component"
elif printf '%s' "$PR_BODY" | grep -qiF '[x] Requirement'; then CURRENT_TYPE="requirement"
elif printf '%s' "$PR_BODY" | grep -qiF '[x] Sampling Strategy'; then CURRENT_TYPE="sampling"
elif printf '%s' "$PR_BODY" | grep -qiF '[x] Tool'; then CURRENT_TYPE="tool"
fi
# Warn (but don't fail) if more than one type box is checked, so
# maintainers can spot the misconfiguration in the workflow log.
MATCH_COUNT=0
for label in '[x] Component' '[x] Requirement' '[x] Sampling Strategy' '[x] Tool'; do
if printf '%s' "$PR_BODY" | grep -qiF "$label"; then
MATCH_COUNT=$((MATCH_COUNT + 1))
fi
done
if [ "$MATCH_COUNT" -gt 1 ]; then
echo "::warning::Multiple type boxes checked (${MATCH_COUNT}); using first match '${CURRENT_TYPE}'. Consider splitting into separate PRs."
fi
# 2. Find any prior bot comments by their embedded marker.
# Marker shape: <!-- mellea-pr-checklist-comment: TYPE -->
# Keep the first marker comment as the canonical one; delete
# any extras (a prior bug or manual action could leak duplicates).
COMMENTS_JSON=$(gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" --paginate)
MARKER_IDS=$(printf '%s' "$COMMENTS_JSON" \
| jq -r '.[] | select(.body | test("<!-- mellea-pr-checklist-comment:")) | .id')
EXISTING_ID=$(printf '%s\n' "$MARKER_IDS" | head -n1)
EXTRA_IDS=$(printf '%s\n' "$MARKER_IDS" | tail -n +2)
EXISTING_TYPE=$(printf '%s' "$COMMENTS_JSON" \
| jq -r '[.[] | select(.body | test("<!-- mellea-pr-checklist-comment:"))] | .[0].body // empty' \
| sed -n 's/.*<!-- mellea-pr-checklist-comment: \([a-z]*\) -->.*/\1/p' | head -n1)
if [ -n "$EXTRA_IDS" ]; then
while IFS= read -r extra_id; do
[ -z "$extra_id" ] && continue
echo "Deleting duplicate marker comment ${extra_id}."
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${extra_id}"
done <<< "$EXTRA_IDS"
fi
echo "current_type='${CURRENT_TYPE}' existing_type='${EXISTING_TYPE}' existing_id='${EXISTING_ID}'"
# 3. Decide what to do.
if [ -z "$CURRENT_TYPE" ] && [ -z "$EXISTING_ID" ]; then
echo "No type checked and no prior comment — nothing to do."
exit 0
fi
if [ -z "$CURRENT_TYPE" ] && [ -n "$EXISTING_ID" ]; then
echo "Type unchecked; deleting stale comment ${EXISTING_ID}."
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${EXISTING_ID}"
exit 0
fi
if [ -n "$CURRENT_TYPE" ] && [ "$CURRENT_TYPE" = "$EXISTING_TYPE" ]; then
echo "Comment already matches '${CURRENT_TYPE}' — no change."
exit 0
fi
# Need to post (no existing) or replace (different type).
TEMPLATE_FILE=".github/PULL_REQUEST_TEMPLATE/${CURRENT_TYPE}.md"
if [ ! -f "$TEMPLATE_FILE" ]; then
echo "::error::Template file not found: $TEMPLATE_FILE"
exit 1
fi
# Friendly heads-up + machine marker. The marker line must stay
# intact for the bot to find this comment again on later edits;
# if it gets stripped, the next event will post a duplicate.
BODY=$(printf '<!-- mellea-pr-checklist-comment: %s -->\n> _This comment is managed by a bot. Editing it is fine — checking off boxes, adding notes — but please leave the HTML comment marker on the first line alone, otherwise checklist updates will break._\n\n%s\n' \
"$CURRENT_TYPE" "$(cat "$TEMPLATE_FILE")")
if [ -n "$EXISTING_ID" ]; then
echo "Updating comment ${EXISTING_ID}: ${EXISTING_TYPE:-?} -> ${CURRENT_TYPE}."
jq -n --arg body "$BODY" '{body: $body}' \
| gh api -X PATCH "repos/${GH_REPO}/issues/comments/${EXISTING_ID}" --input -
else
echo "Posting new ${CURRENT_TYPE} checklist comment."
jq -n --arg body "$BODY" '{body: $body}' \
| gh api -X POST "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" --input -
fi