-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
237 lines (201 loc) · 8.65 KB
/
auto-label-pr.yml
File metadata and controls
237 lines (201 loc) · 8.65 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
name: Auto Label PR
on:
# Runs only on pull_request_target due to having access to a App token.
# This means PRs from forks will not be able to alter this workflow to get the tokens
pull_request_target:
types: [opened, reopened, synchronize, edited]
# Allow manual triggering for recheck functionality
workflow_dispatch:
inputs:
pr_number:
description: 'Pull Request number to process'
required: true
type: number
permissions:
pull-requests: write
contents: read
env:
BOT_NAME: "esphome[bot]"
jobs:
label:
runs-on: ubuntu-latest
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
- name: Auto Label PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.number }}
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const { owner, repo } = context.repo;
const pr_number = parseInt(process.env.PR_NUMBER);
console.log('Processing PR #' + pr_number);
// Get current labels
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr_number
});
const currentLabels = currentLabelsData.map(label => label.name);
// Define managed labels that this workflow controls
const managedLabels = currentLabels.filter(label =>
[
'current',
'next',
'has-parent',
'wrong-base-branch',
'labeller-recheck'
].includes(label) ||
label.startsWith('component: ')
);
console.log('Current labels:', currentLabels.join(', '));
console.log('Managed labels:', managedLabels.join(', '));
const labels = new Set();
// Strategy: Branch-based labeling
let baseRef, prBody;
if (context.eventName === 'workflow_dispatch') {
// Get PR details when called via workflow_dispatch
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr_number
});
baseRef = pr.base.ref;
prBody = pr.body || '';
} else {
// Use context when called via pull_request_target
baseRef = context.payload.pull_request.base.ref;
prBody = context.payload.pull_request.body || '';
}
console.log('Target branch:', baseRef);
if (baseRef === 'current') {
labels.add('current');
} else if (baseRef === 'next') {
labels.add('next');
}
// Strategy: Parent PR detection
// prBody is already set above based on event type
// Look for ESPHome PR links in PR body
// Patterns to match:
// - https://github.com/esphome/esphome/pull/1234
// - esphome/esphome#1234
const esphomePrPatterns = [
/https:\/\/github\.com\/esphome\/esphome\/pull\/(\d+)/,
/esphome\/esphome#(\d+)/
];
const hasEsphomeLink = esphomePrPatterns.some(pattern =>
pattern.test(prBody)
);
console.log('PR Body contains ESPHome link:', hasEsphomeLink);
if (hasEsphomeLink) {
labels.add('has-parent');
// Extract parent PR number and fetch its labels
for (const pattern of esphomePrPatterns) {
const match = prBody.match(pattern);
if (!match) continue; // Skip to next pattern if no match
try {
// Extract PR number from the capture group
const parentPrNumber = match[1];
console.log('Found parent PR number:', parentPrNumber);
// Fetch labels from the parent PR in esphome/esphome repository
const { data: parentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: 'esphome',
repo: 'esphome',
issue_number: parseInt(parentPrNumber)
});
console.log('Parent PR labels:', parentLabels.map(l => l.name).join(', '));
// Add all "component: " labels from parent PR
parentLabels.forEach(label => {
if (label.name.startsWith('component: ')) {
labels.add(label.name);
console.log('Added component label from parent:', label.name);
}
});
break; // Only process the first match
} catch (error) {
console.log('Failed to fetch parent PR labels:', error.message);
}
}
}
// Convert Set to Array
const finalLabels = Array.from(labels);
// Strategy: Wrong base branch detection
const hasParent = finalLabels.includes('has-parent');
const isCurrent = finalLabels.includes('current');
if (hasParent && isCurrent) {
finalLabels.push('wrong-base-branch');
console.log('Added wrong-base-branch label: has-parent targeting current branch');
}
console.log('Computed labels:', finalLabels.join(', '));
// Add new labels
if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Request changes if wrong base branch is detected
if (hasParent && isCurrent) {
console.log('Requesting changes due to wrong base branch');
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
event: 'REQUEST_CHANGES',
body: 'As this is a feature matched with a PR in https://github.com/esphome/esphome, please target your PR to the `next` branch and rebase.'
});
} else {
// Check if we should dismiss any existing review from this bot
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
// Find the most recent review from this bot that requested changes for wrong base branch
const botReview = reviews
.filter(review =>
review.user.login === process.env.BOT_NAME &&
review.state === 'CHANGES_REQUESTED' &&
review.body && review.body.includes('target your PR to the `next` branch')
)
.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at))[0];
if (botReview) {
console.log('Dismissing previous bot review as labels are now correct');
await github.rest.pulls.dismissReview({
owner,
repo,
pull_number: pr_number,
review_id: botReview.id,
message: 'Base branch has been corrected - dismissing previous review.'
});
}
}
// Remove old managed labels that are no longer needed
const labelsToRemove = managedLabels.filter(label =>
!finalLabels.includes(label)
);
for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}