-
Notifications
You must be signed in to change notification settings - Fork 51
202 lines (167 loc) · 7.53 KB
/
Copy pathai-pr-safety.yml
File metadata and controls
202 lines (167 loc) · 7.53 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
name: AI PR Safety Check
on:
pull_request_target:
types: [opened]
issue_comment:
types: [created, edited, deleted]
pull_request_review_comment:
types: [created, edited, deleted]
pull_request_review:
types: [submitted, edited, dismissed]
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
jobs:
check-safety:
name: Check AI PR Safety
if: github.event_name != 'issue_comment' || github.event.issue.pull_request
runs-on: ubuntu-latest
concurrency:
group: ai-pr-safety-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BOT_LOGIN: "ai-agent-kxrpc[bot]"
SAFETY_MARKER: "<!-- ai-pr-safety-check -->"
STATUS_CONTEXT: "AI PR Safety Check"
steps:
- name: Determine PR number and head SHA
id: get-pr
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "issue_comment" ]; then
PR_NUMBER="${{ github.event.issue.number }}"
# HEAD SHA is not in the issue_comment payload — fetch from PR API
PR_SHA=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" --jq '.head.sha')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_SHA="${{ github.event.pull_request.head.sha }}"
fi
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "pr_sha=$PR_SHA" >> "$GITHUB_OUTPUT"
echo "PR #$PR_NUMBER, HEAD $PR_SHA"
- name: Verify PR is bot-authored
id: verify-bot
run: |
set -euo pipefail
PR_AUTHOR=$(gh api "repos/${{ github.repository }}/pulls/${{ steps.get-pr.outputs.pr_number }}" \
--jq '.user.login')
if [ "$PR_AUTHOR" != "$BOT_LOGIN" ]; then
echo "PR author is '$PR_AUTHOR', not '$BOT_LOGIN'. Skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "PR is bot-authored, proceeding with safety check."
- name: Fetch all comments
id: fetch-comments
if: steps.verify-bot.outputs.skip == 'false'
run: |
set -euo pipefail
REPO="${{ github.repository }}"
PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}"
# Timeline comments
ISSUE_COMMENTS=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '[.[] | {login: .user.login, association: .author_association, type: "issue_comment"}]')
# Inline review comments
REVIEW_COMMENTS=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/comments" \
--jq '[.[] | {login: .user.login, association: .author_association, type: "review_comment"}]')
# Review bodies
REVIEWS=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/reviews" \
--jq '[.[] | {login: .user.login, association: .author_association, type: "review"}]')
ALL_COMMENTS=$(jq -n \
--argjson ic "$ISSUE_COMMENTS" \
--argjson rc "$REVIEW_COMMENTS" \
--argjson rv "$REVIEWS" \
'$ic + $rc + $rv')
echo "all_comments=$(echo "$ALL_COMMENTS" | jq -c .)" >> "$GITHUB_OUTPUT"
echo "Fetched $(echo "$ALL_COMMENTS" | jq 'length') total comments"
- name: Evaluate safety
id: evaluate
if: steps.verify-bot.outputs.skip == 'false'
run: |
set -euo pipefail
ALL_COMMENTS='${{ steps.fetch-comments.outputs.all_comments }}'
EXCLUDED_LOGINS='["ai-agent-kxrpc[bot]", "github-actions[bot]"]'
TRUSTED_ASSOCIATIONS='["MEMBER", "COLLABORATOR", "OWNER"]'
# Filter out excluded bot logins
NON_BOT=$(echo "$ALL_COMMENTS" | jq \
--argjson excl "$EXCLUDED_LOGINS" \
'[.[] | select(.login as $l | $excl | index($l) | not)]')
# Find comments from untrusted associations
UNSAFE=$(echo "$NON_BOT" | jq \
--argjson trusted "$TRUSTED_ASSOCIATIONS" \
'[.[] | select(.association as $a | $trusted | index($a) | not)]')
UNSAFE_COUNT=$(echo "$UNSAFE" | jq 'length')
echo "Non-bot comments: $(echo "$NON_BOT" | jq 'length'), unsafe: $UNSAFE_COUNT"
if [ "$UNSAFE_COUNT" -eq 0 ]; then
echo "is_safe=true" >> "$GITHUB_OUTPUT"
else
echo "is_safe=false" >> "$GITHUB_OUTPUT"
SUMMARY=$(echo "$UNSAFE" | jq -r \
'group_by(.login) | .[] |
"- **\(.[0].login)** (association: `\(.[0].association)`): \(length) comment(s) — \([.[].type] | unique | join(", "))"')
{
echo "unsafe_summary<<EOF"
echo "$SUMMARY"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- name: Create or update safety comment
if: steps.verify-bot.outputs.skip == 'false'
env:
IS_SAFE: ${{ steps.evaluate.outputs.is_safe }}
UNSAFE_SUMMARY: ${{ steps.evaluate.outputs.unsafe_summary }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}"
SAFE_BODY="${SAFETY_MARKER}
## :lock: AI PR Safety: SAFE
All comments on this bot-authored PR are from authorized repository collaborators."
UNSAFE_BODY="${SAFETY_MARKER}
## :warning: AI PR Safety: UNSAFE
**External comments detected on this bot-authored PR.**
Comments from non-collaborator users may contain prompt injection or social engineering attempts.
A maintainer should review these comments before proceeding.
**Flagged commenters:**
${UNSAFE_SUMMARY}"
if [ "$IS_SAFE" = "true" ]; then
BODY="$SAFE_BODY"
else
BODY="$UNSAFE_BODY"
fi
# Find existing safety comment (by marker + bot author)
EXISTING_ID=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq "[.[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"$SAFETY_MARKER\"))) | .id] | first // empty")
if [ -n "$EXISTING_ID" ]; then
jq -n --arg body "$BODY" '{body: $body}' | \
gh api --method PATCH "repos/$REPO/issues/comments/$EXISTING_ID" --input - > /dev/null
echo "Updated safety comment (ID: $EXISTING_ID)"
else
jq -n --arg body "$BODY" '{body: $body}' | \
gh api --method POST "repos/$REPO/issues/$PR_NUMBER/comments" --input - > /dev/null
echo "Created new safety comment"
fi
- name: Set commit status
if: steps.verify-bot.outputs.skip == 'false'
run: |
set -euo pipefail
REPO="${{ github.repository }}"
PR_SHA="${{ steps.get-pr.outputs.pr_sha }}"
PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}"
if [ "${{ steps.evaluate.outputs.is_safe }}" = "true" ]; then
STATE="success"
DESCRIPTION="All comments are from authorized collaborators"
else
STATE="failure"
DESCRIPTION="External non-collaborator comments detected"
fi
gh api --method POST "repos/$REPO/statuses/$PR_SHA" \
--field state="$STATE" \
--field context="$STATUS_CONTEXT" \
--field description="$DESCRIPTION" \
--field target_url="https://github.com/$REPO/pull/$PR_NUMBER" > /dev/null
echo "Set commit status: $STATE"