2222 with :
2323 scope : DataDog/dd-trace-java
2424 policy : self.check-pull-request-labels
25- - name : Flag AI-generated pull requests
26- id : flag_ai_generated
25+ - name : Clean up tool labels
26+ id : clean_up_labels
2727 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 9.0.0
2828 with :
2929 github-token : ${{ steps.generate-token.outputs.token }}
@@ -35,101 +35,102 @@ jobs:
3535 const prNumber = context.payload.pull_request.number
3636 const owner = context.repo.owner
3737 const repo = context.repo.repo
38- const aiGeneratedLabel = 'tag: ai generated'
39- let isAiGenerated = false
40- let labelsStale = false
41-
42- /*
43- * Check for 'Bits AI' label and remove it.
44- */
45- const bitsAiLabel = 'Bits AI'
38+ // Labels applied by external tooling that are never allowed on a pull request
39+ const toolLabels = [
40+ 'Bits AI', // Applied by the Bits AI tooling
41+ 'campaigner-automated-change' // Applied by the Campaigner tooling
42+ ]
4643 const prLabels = context.payload.pull_request.labels.map(l => l.name)
47- if ( prLabels.includes(bitsAiLabel)) {
48- isAiGenerated = true
44+ const cleanedLabels = toolLabels.filter(label => prLabels.includes(label))
45+ for (const label of cleanedLabels) {
4946 // Remove label from the PR
5047 try {
5148 await github.rest.issues.removeLabel({
5249 owner, repo,
5350 issue_number: prNumber,
54- name: bitsAiLabel
51+ name: label
5552 })
5653 } catch (e) {
57- core.warning(`Could not remove '${bitsAiLabel }' label from PR: ${e.message}`)
54+ core.warning(`Could not remove '${label }' label from PR: ${e.message}`)
5855 }
59- labelsStale = true
60- // Delete label from the repository
56+ // Delete label from the repository as the tooling applying it also recreates it
6157 try {
62- await github.rest.issues.deleteLabel({ owner, repo, name: bitsAiLabel })
58+ await github.rest.issues.deleteLabel({ owner, repo, name: label })
6359 } catch (e) {
64- core.warning(`Could not delete '${bitsAiLabel }' label from repo: ${e.message}`)
60+ core.warning(`Could not delete '${label }' label from repo: ${e.message}`)
6561 }
6662 }
63+ core.setOutput('cleaned_labels', JSON.stringify(cleanedLabels))
6764
68- /*
69- * Inspect commits for AI authorship signals.
70- */
65+ - name : Flag AI-generated pull requests
66+ id : flag_ai_generated
67+ uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 9.0.0
68+ env :
69+ CLEANED_LABELS : ${{ steps.clean_up_labels.outputs.cleaned_labels }}
70+ with :
71+ github-token : ${{ steps.generate-token.outputs.token }}
72+ script : |
73+ // Skip draft pull requests
74+ if (context.payload.pull_request.draft) {
75+ return
76+ }
77+ const prNumber = context.payload.pull_request.number
78+ const owner = context.repo.owner
79+ const repo = context.repo.repo
80+ // Skip if the PR is already labeled as AI-generated
81+ const aiGeneratedLabel = 'tag: ai generated'
7182 if (context.payload.pull_request.labels.some(l => l.name === aiGeneratedLabel)) {
7283 core.info(`PR #${prNumber} is already labeled as AI-generated, skipping commit scan.`)
73- core.setOutput('labels_stale', String(labelsStale))
7484 return
7585 }
76- const aiRegex = /\b(anthropic|chatgpt|codex|copilot|cursor|openai)\b/i
77- const commits = await github.paginate(github.rest.pulls.listCommits, {
78- owner, repo,
79- pull_number: prNumber,
80- per_page: 100
81- })
82- for (const { commit } of commits) {
83- const authorName = commit.author?.name ?? ''
84- const authorEmail = commit.author?.email ?? ''
85- const committerName = commit.committer?.name ?? ''
86- const committerEmail = commit.committer?.email ?? ''
87- // Extract Co-authored-by trailer lines from commit message
88- const coAuthors = (commit.message ?? '').split('\n')
89- .filter(line => /^co-authored-by:/i.test(line.trim()))
90- const fieldsToCheck = [authorName, authorEmail]
91- // Skip GitHub's generic noreply for committer
92- if (committerEmail !== 'noreply@github.com') {
93- fieldsToCheck.push(committerName, committerEmail)
94- }
95- fieldsToCheck.push(...coAuthors)
96- if (fieldsToCheck.some(field => aiRegex.test(field))) {
97- isAiGenerated = true
98- break
86+ // The cleaned up 'Bits AI' label flags an AI-generated pull request
87+ let isAiGenerated = JSON.parse(process.env.CLEANED_LABELS || '[]').includes('Bits AI')
88+ // Inspect commits for AI authorship signals
89+ if (!isAiGenerated) {
90+ const aiRegex = /\b(anthropic|chatgpt|codex|copilot|cursor|openai)\b/i
91+ const commits = await github.paginate(github.rest.pulls.listCommits, {
92+ owner, repo,
93+ pull_number: prNumber,
94+ per_page: 100
95+ })
96+ for (const { commit } of commits) {
97+ const authorName = commit.author?.name ?? ''
98+ const authorEmail = commit.author?.email ?? ''
99+ const committerName = commit.committer?.name ?? ''
100+ const committerEmail = commit.committer?.email ?? ''
101+ // Extract Co-authored-by trailer lines from commit message
102+ const coAuthors = (commit.message ?? '').split('\n')
103+ .filter(line => /^co-authored-by:/i.test(line.trim()))
104+ const fieldsToCheck = [authorName, authorEmail]
105+ // Skip GitHub's generic noreply for committer
106+ if (committerEmail !== 'noreply@github.com') {
107+ fieldsToCheck.push(committerName, committerEmail)
108+ }
109+ fieldsToCheck.push(...coAuthors)
110+ if (fieldsToCheck.some(field => aiRegex.test(field))) {
111+ isAiGenerated = true
112+ break
113+ }
99114 }
100115 }
101-
102- /*
103- * Add 'tag: ai generated' label if AI-generated.
104- */
116+ // Add 'tag: ai generated' label if AI-generated
105117 if (isAiGenerated) {
106- // Re-fetch labels only if they were modified above (Bits AI removal)
107- let currentLabels
108- if (labelsStale) {
109- const { data: currentPr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
110- currentLabels = currentPr.labels.map(l => l.name)
111- } else {
112- currentLabels = context.payload.pull_request.labels.map(l => l.name)
113- }
114- if (!currentLabels.includes(aiGeneratedLabel)) {
115- try {
116- await github.rest.issues.addLabels({
117- owner, repo,
118- issue_number: prNumber,
119- labels: [aiGeneratedLabel]
120- })
121- core.info(`Added '${aiGeneratedLabel}' label to PR #${prNumber}`)
122- } catch (e) {
123- core.setFailed(`Could not add '${aiGeneratedLabel}' label to PR #${prNumber}: ${e.message}`)
124- }
118+ try {
119+ await github.rest.issues.addLabels({
120+ owner, repo,
121+ issue_number: prNumber,
122+ labels: [aiGeneratedLabel]
123+ })
124+ core.info(`Added '${aiGeneratedLabel}' label to PR #${prNumber}`)
125+ } catch (e) {
126+ core.setFailed(`Could not add '${aiGeneratedLabel}' label to PR #${prNumber}: ${e.message}`)
125127 }
126128 }
127- core.setOutput('labels_stale', String(labelsStale))
128129
129130 - name : Check pull request labels
130131 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 9.0.0
131132 env :
132- LABELS_STALE : ${{ steps.flag_ai_generated .outputs.labels_stale }}
133+ CLEANED_LABELS : ${{ steps.clean_up_labels .outputs.cleaned_labels }}
133134 with :
134135 github-token : ${{ steps.generate-token.outputs.token }}
135136 script : |
@@ -143,19 +144,12 @@ jobs:
143144 'comp:',
144145 'inst:',
145146 'tag:',
146- 'mergequeue-status:',
147- 'team:',
148147 'performance:', // To refactor to 'ci: ' in the future
149148 'run-tests:' // Unused since GitLab migration
150149 ]
151- // Exact-match labels that don't fit a category prefix (e.g. labels applied
152- // by external automation tooling).
153- const exactAllowlist = [
154- 'campaigner-automated-change'
155- ]
156- // Re-fetch labels only if the previous step modified them (ex: "Bits AI" removal)
150+ // Re-fetch labels only if the clean up step removed some of them
157151 let prLabels
158- if (process.env.LABELS_STALE === 'true' ) {
152+ if (JSON.parse( process.env.CLEANED_LABELS || '[]').length > 0 ) {
159153 const { data: currentPr } = await github.rest.pulls.get({
160154 owner: context.repo.owner,
161155 repo: context.repo.repo,
@@ -168,10 +162,7 @@ jobs:
168162 // Look for invalid labels
169163 const invalidLabels = prLabels
170164 .map(label => label.name)
171- .filter(label =>
172- !exactAllowlist.includes(label) &&
173- validCategories.every(prefix => !label.startsWith(prefix))
174- )
165+ .filter(label => validCategories.every(prefix => !label.startsWith(prefix)))
175166 const hasInvalidLabels = invalidLabels.length > 0
176167 // Get existing comments to check for blocking comment
177168 const comments = await github.rest.issues.listComments({
0 commit comments