-
Notifications
You must be signed in to change notification settings - Fork 148
258 lines (222 loc) · 10.2 KB
/
pr-linked-issue.yml
File metadata and controls
258 lines (222 loc) · 10.2 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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: "Linked Issue Check"
on:
# Re-check when PR is opened or body is edited (author adds Fixes #N).
pull_request_target:
types: [opened, edited, synchronize, reopened]
branches: [main]
# Re-check open PRs when a maintainer adds the "triaged" label to an issue.
issues:
types: [labeled]
permissions:
contents: read
pull-requests: write
issues: read
jobs:
# ── Job 1: validate linked issue on PR events ─────────────────────────
# SECURITY: This workflow uses pull_request_target to get write access for
# posting comments on fork PRs. It MUST NOT check out or execute code from
# the PR branch. All inputs from the PR (body, author) are read via API
# only. Adding actions/checkout here would run untrusted fork code with
# base repo write permissions.
check:
if: >-
github.repository_owner == 'NVIDIA-NeMo'
&& github.event_name != 'issues'
runs-on: ubuntu-latest
steps:
- name: Check author permissions
id: author
env:
GH_TOKEN: ${{ github.token }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
USER="$PR_AUTHOR"
# Bots that are always allowed (match DCO allowlist pattern).
if [ "$USER" = "dependabot[bot]" ]; then
echo "is_collaborator=true" >> "$GITHUB_OUTPUT"
exit 0
fi
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${USER}/permission" \
--jq '.permission' 2>/dev/null || echo "none")
echo "permission=${PERMISSION}"
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "write" ]; then
echo "is_collaborator=true" >> "$GITHUB_OUTPUT"
else
echo "is_collaborator=false" >> "$GITHUB_OUTPUT"
fi
- name: Parse issue reference from PR body
id: parse
if: steps.author.outputs.is_collaborator != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
if [ -z "$PR_BODY" ] || [ "$PR_BODY" = "null" ]; then
echo "issue_num=" >> "$GITHUB_OUTPUT"
echo "No PR body found"
exit 0
fi
# Case-insensitive match for Fixes #N, Closes #N, Resolves #N.
printf '%s' "$PR_BODY" > /tmp/pr-body-raw.txt
ISSUE_NUM=$(grep -ioP '(?:fixes|closes|resolves)\s+#\K\d+' /tmp/pr-body-raw.txt | head -1 || true)
echo "issue_num=${ISSUE_NUM}" >> "$GITHUB_OUTPUT"
echo "Parsed issue number: ${ISSUE_NUM:-<none>}"
- name: Validate issue exists and is triaged
id: validate
if: steps.author.outputs.is_collaborator != 'true' && steps.parse.outputs.issue_num != ''
env:
GH_TOKEN: ${{ github.token }}
ISSUE_NUM: ${{ steps.parse.outputs.issue_num }}
run: |
RESPONSE=$(gh api "repos/${{ github.repository }}/issues/${ISSUE_NUM}" 2>/dev/null) || {
echo "issue_exists=false" >> "$GITHUB_OUTPUT"
echo "is_triaged=false" >> "$GITHUB_OUTPUT"
echo "Issue #${ISSUE_NUM} not found"
exit 0
}
# Verify it's an issue, not a PR (GitHub's issues API returns both).
IS_PR=$(echo "$RESPONSE" | jq -r 'has("pull_request")')
if [ "$IS_PR" = "true" ]; then
echo "issue_exists=false" >> "$GITHUB_OUTPUT"
echo "is_triaged=false" >> "$GITHUB_OUTPUT"
echo "#${ISSUE_NUM} is a pull request, not an issue"
exit 0
fi
echo "issue_exists=true" >> "$GITHUB_OUTPUT"
TRIAGED=$(echo "$RESPONSE" | jq -r '[.labels[].name] | any(. == "triaged")')
echo "is_triaged=${TRIAGED}" >> "$GITHUB_OUTPUT"
echo "Issue #${ISSUE_NUM} exists, triaged=${TRIAGED}"
- name: Build comment body and post result
id: comment
env:
GH_TOKEN: ${{ github.token }}
IS_COLLABORATOR: ${{ steps.author.outputs.is_collaborator }}
ISSUE_NUM: ${{ steps.parse.outputs.issue_num }}
ISSUE_EXISTS: ${{ steps.validate.outputs.issue_exists }}
IS_TRIAGED: ${{ steps.validate.outputs.is_triaged }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
MARKER="<!-- linked-issue-check -->"
# Find existing bot comment with our marker.
COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq "[.[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"${MARKER}\"))] | last | .id // empty" \
2>/dev/null || echo "")
if [ "$IS_COLLABORATOR" = "true" ]; then
echo "status=pass" >> "$GITHUB_OUTPUT"
# Clean up any leftover comment from before the author became a collaborator.
if [ -n "$COMMENT_ID" ]; then
gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" || true
fi
exit 0
fi
# Build comment body.
if [ -z "$ISSUE_NUM" ]; then
STATUS="fail"
cat > /tmp/comment-body.md <<'MSG'
<!-- linked-issue-check -->
### Linked Issue Check
This PR does not reference an issue. External contributions must link to
a triaged issue before the PR can be merged.
Add one of the following to your PR description:
- `Fixes #<issue-number>`
- `Closes #<issue-number>`
- `Resolves #<issue-number>`
If no issue exists yet, [open one](https://github.com/NVIDIA-NeMo/DataDesigner/issues/new/choose)
and a maintainer will triage it.
See [CONTRIBUTING.md](https://github.com/NVIDIA-NeMo/DataDesigner/blob/main/CONTRIBUTING.md)
for details.
MSG
elif [ "$ISSUE_EXISTS" != "true" ]; then
STATUS="fail"
cat > /tmp/comment-body.md <<MSG
<!-- linked-issue-check -->
### Linked Issue Check
The referenced issue #${ISSUE_NUM} was not found. Please check the issue
number in your PR description.
MSG
elif [ "$IS_TRIAGED" != "true" ]; then
STATUS="fail"
cat > /tmp/comment-body.md <<MSG
<!-- linked-issue-check -->
### Linked Issue Check
Issue #${ISSUE_NUM} has not been triaged yet. A maintainer needs to review
the issue and add the \`triaged\` label before this PR can be merged.
You can continue working on the PR in the meantime. The check will
re-run automatically once the issue is triaged.
MSG
else
STATUS="pass"
fi
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
# Post, update, or delete the comment.
if [ "$STATUS" = "fail" ]; then
if [ -n "$COMMENT_ID" ]; then
gh api -X PATCH "repos/${REPO}/issues/comments/${COMMENT_ID}" \
-f body="$(cat /tmp/comment-body.md)"
else
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
-f body="$(cat /tmp/comment-body.md)"
fi
elif [ -n "$COMMENT_ID" ]; then
gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" || true
fi
- name: Set check result
if: steps.comment.outputs.status == 'fail'
run: |
echo "::error::Linked issue check failed. See the PR comment for details."
exit 1
# ── Job 2: re-trigger check when an issue gets triaged ────────────────
retrigger:
if: >-
github.repository_owner == 'NVIDIA-NeMo'
&& github.event_name == 'issues'
&& github.event.label.name == 'triaged'
runs-on: ubuntu-latest
steps:
- name: Find PRs referencing this issue
id: find-prs
env:
GH_TOKEN: ${{ github.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
# List open PRs and find those whose body references this issue.
PRS=$(gh pr list --repo "${{ github.repository }}" --state open \
--json number,body --limit 200 \
| jq -r "[.[] | select(.body != null) | select(.body | test(\"(?i)(fixes|closes|resolves)\\\\s+#${ISSUE_NUMBER}\\\\b\")) | .number] | .[]")
if [ -z "$PRS" ]; then
echo "No open PRs reference issue #${ISSUE_NUMBER}"
echo "prs=" >> "$GITHUB_OUTPUT"
else
echo "Found PRs: ${PRS}"
echo "prs=$(echo "$PRS" | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
fi
- name: Re-trigger linked issue check
if: steps.find-prs.outputs.prs != ''
env:
GH_TOKEN: ${{ github.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_NUMBERS: ${{ steps.find-prs.outputs.prs }}
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
for PR_NUM in $PR_NUMBERS; do
echo "Re-triggering check for PR #${PR_NUM}..."
# Read current PR body to a file to avoid shell expansion issues.
gh pr view "$PR_NUM" --repo "${{ github.repository }}" --json body -q '.body' > /tmp/current-body.txt
# Append or update hidden timestamp to trigger the 'edited' event,
# which re-runs the check job.
MARKER="<!-- triaged-recheck"
if grep -q "$MARKER" /tmp/current-body.txt; then
sed "s|<!-- triaged-recheck[^>]*-->|<!-- triaged-recheck: ${TIMESTAMP} -->|" \
/tmp/current-body.txt > /tmp/pr-body.md
else
cp /tmp/current-body.txt /tmp/pr-body.md
printf '\n<!-- triaged-recheck: %s -->' "$TIMESTAMP" >> /tmp/pr-body.md
fi
gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --body-file /tmp/pr-body.md
# Post a visible comment so the author knows what happened.
gh pr comment "$PR_NUM" --repo "${{ github.repository }}" --body \
"Issue #${ISSUE_NUMBER} has been triaged. The linked issue check is being re-evaluated."
done