-
Notifications
You must be signed in to change notification settings - Fork 330
206 lines (199 loc) · 8.8 KB
/
check-pull-request-labels.yaml
File metadata and controls
206 lines (199 loc) · 8.8 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
name: Validate PR Label Format
on:
pull_request:
types: [opened, edited, ready_for_review, labeled, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_pr_labels:
name: Check pull request labels
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Flag AI-generated pull requests
id: flag_ai_generated
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Skip draft pull requests
if (context.payload.pull_request.draft) {
return
}
const prNumber = context.payload.pull_request.number
const owner = context.repo.owner
const repo = context.repo.repo
const aiGeneratedLabel = 'tag: ai generated'
let isAiGenerated = false
let labelsStale = false
/*
* Check for 'Bits AI' label and remove it.
*/
const bitsAiLabel = 'Bits AI'
const prLabels = context.payload.pull_request.labels.map(l => l.name)
if (prLabels.includes(bitsAiLabel)) {
isAiGenerated = true
// Remove label from the PR
try {
await github.rest.issues.removeLabel({
owner, repo,
issue_number: prNumber,
name: bitsAiLabel
})
} catch (e) {
core.warning(`Could not remove '${bitsAiLabel}' label from PR: ${e.message}`)
}
labelsStale = true
// Delete label from the repository
try {
await github.rest.issues.deleteLabel({ owner, repo, name: bitsAiLabel })
} catch (e) {
core.warning(`Could not delete '${bitsAiLabel}' label from repo: ${e.message}`)
}
}
/*
* Inspect commits for AI authorship signals.
*/
if (context.payload.pull_request.labels.some(l => l.name === aiGeneratedLabel)) {
core.info(`PR #${prNumber} is already labeled as AI-generated, skipping commit scan.`)
core.setOutput('labels_stale', String(labelsStale))
return
}
const aiRegex = /\b(anthropic|chatgpt|codex|copilot|cursor|openai)\b/i
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner, repo,
pull_number: prNumber,
per_page: 100
})
for (const { commit } of commits) {
const authorName = commit.author?.name ?? ''
const authorEmail = commit.author?.email ?? ''
const committerName = commit.committer?.name ?? ''
const committerEmail = commit.committer?.email ?? ''
// Extract Co-authored-by trailer lines from commit message
const coAuthors = (commit.message ?? '').split('\n')
.filter(line => /^co-authored-by:/i.test(line.trim()))
const fieldsToCheck = [authorName, authorEmail]
// Skip GitHub's generic noreply for committer
if (committerEmail !== 'noreply@github.com') {
fieldsToCheck.push(committerName, committerEmail)
}
fieldsToCheck.push(...coAuthors)
if (fieldsToCheck.some(field => aiRegex.test(field))) {
isAiGenerated = true
break
}
}
/*
* Add 'tag: ai generated' label if AI-generated.
*/
if (isAiGenerated) {
// Re-fetch labels only if they were modified above (Bits AI removal)
let currentLabels
if (labelsStale) {
const { data: currentPr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
currentLabels = currentPr.labels.map(l => l.name)
} else {
currentLabels = context.payload.pull_request.labels.map(l => l.name)
}
if (!currentLabels.includes(aiGeneratedLabel)) {
try {
await github.rest.issues.addLabels({
owner, repo,
issue_number: prNumber,
labels: [aiGeneratedLabel]
})
core.info(`Added '${aiGeneratedLabel}' label to PR #${prNumber}`)
} catch (e) {
core.setFailed(`Could not add '${aiGeneratedLabel}' label to PR #${prNumber}: ${e.message}`)
}
}
}
core.setOutput('labels_stale', String(labelsStale))
- name: Check pull request labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
env:
LABELS_STALE: ${{ steps.flag_ai_generated.outputs.labels_stale }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Skip draft pull requests
if (context.payload.pull_request.draft) {
return
}
// Define valid label categories
const validCategories = [
'type:',
'comp:',
'inst:',
'tag:',
'mergequeue-status:',
'team:',
'performance:', // To refactor to 'ci: ' in the future
'run-tests:' // Unused since GitLab migration
]
// Re-fetch labels only if the previous step modified them (ex: "Bits AI" removal)
let prLabels
if (process.env.LABELS_STALE === 'true') {
const { data: currentPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
})
prLabels = currentPr.labels
} else {
prLabels = context.payload.pull_request.labels
}
// Look for invalid labels
const invalidLabels = prLabels
.map(label => label.name)
.filter(label => validCategories.every(prefix => !label.startsWith(prefix)))
const hasInvalidLabels = invalidLabels.length > 0
// Get existing comments to check for blocking comment
const comments = await github.rest.issues.listComments({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo
})
const commentMarker = '<!-- dd-trace-java-check-pr-labels-workflow -->'
let blockingComment = comments.data.find(comment => comment.body.includes(commentMarker))
// Create or update blocking comment if there are invalid labels
if (hasInvalidLabels) {
const commentBody = '**PR Blocked - Invalid Label**\n\n' +
`The pull request introduced unexpected labels:\n\n` +
invalidLabels.map(label => `* \`${label}\``).join('\n') + '\n\n' +
'**This PR is blocked until:**\n' +
'1. The invalid labels are deleted, and\n' +
'2. A maintainer deletes this comment to unblock the PR\n\n' +
'**Note:** Simply removing labels from the pull request is not enough - a maintainer must delete this comment then remove the label to remove the block.\n\n' +
commentMarker
if (blockingComment) {
// Update existing blocking comment
await github.rest.issues.updateComment({
comment_id: blockingComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
} else {
// Create new blocking comment
await github.rest.issues.createComment({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
})
}
blockingComment = true
}
if (blockingComment) {
// Block the PR by failing the workflow
if (hasInvalidLabels) {
core.setFailed(`PR blocked: Invalid labels detected: (${invalidLabels.join(', ')}). A maintainer must delete the blocking comment after fixing the labels to allow merging.`)
} else {
core.setFailed(`PR blocked: A previous blocking comment still exists. A maintainer must delete it to allow merging.`)
}
}