-
Notifications
You must be signed in to change notification settings - Fork 1.8k
202 lines (182 loc) · 8.33 KB
/
Copy pathlabeler.yml
File metadata and controls
202 lines (182 loc) · 8.33 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: 🗂️ Auto Label PR
on:
# pull_request_target (not pull_request) is required so the labeler holds
# pull-requests: write on fork PRs — a pull_request run gets a read-only token
# on forks and can't apply labels. This is safe because no job here checks out
# or executes PR-controlled code: actions/labeler and github-script operate
# purely through the GitHub API on PR metadata (numbers, filenames, labels),
# never on the PR's file contents. See the dangerous-triggers audit rationale:
# https://docs.zizmor.sh/audits/#dangerous-triggers
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
labeler:
name: PR Labeler - Apply and Sync Labels
runs-on: ubuntu-latest
timeout-minutes: 1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
pull-requests: write
steps:
- name: Apply and Sync Labels
uses: actions/labeler@v7
with:
sync-labels: true # Remove labels if the file paths no longer match
configuration-path: .github/labeler.yml
size-labeler:
name: Label PR by size
runs-on: ubuntu-latest
timeout-minutes: 2
# Run after the path-based labeler, never alongside it. actions/labeler's
# sync-labels sweep writes an authoritative label set from a snapshot taken
# when it started; if it overlaps the size step it drops the size label it
# never saw. Ordering the size step after the sync closes that window.
#
# needs orders it after labeler; !cancelled() lets it still run when labeler
# fails (e.g. a flaky action download) so size labeling isn't coupled to the
# path labeler's success, while still skipping when the run is superseded.
needs: labeler
if: ${{ !cancelled() }}
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Compute size and apply label
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
// Uniform charcoal chip for every size label so the size metadata reads as one
// recessive family and doesn't compete with semantic labels (bug, Frontend, …) on
// a busy PR list. The cool -> hot ramp lives entirely in the per-bucket emoji
// (🔵 smallest -> 🔴 largest), which keeps its crisp shape on a neutral background.
// maxSize is the inclusive upper bound of changed LOC (additions + deletions) for
// the bucket; the last entry (null) catches everything above it.
const CHIP_COLOR = "2d3139";
const BUCKETS = [
{ name: "🔵 size/XS", color: CHIP_COLOR, maxSize: 19 },
{ name: "🟢 size/S", color: CHIP_COLOR, maxSize: 100 },
{ name: "🟡 size/M", color: CHIP_COLOR, maxSize: 300 },
{ name: "🟠 size/L", color: CHIP_COLOR, maxSize: 600 },
{ name: "🔴 size/XL", color: CHIP_COLOR, maxSize: null },
];
const MANAGED_LABELS = BUCKETS.map((b) => b.name);
// Lockfiles and generated clients dwarf the human-reviewed diff, so exclude them
// from the count — otherwise a lockfile bump mislabels a tiny PR as XL.
const IGNORE_GLOBS = [
"**/package-lock.json",
"**/yarn.lock",
"**/pnpm-lock.yaml",
"**/poetry.lock",
"**/uv.lock",
"sdks/python/src/opik/rest_api/**",
"sdks/typescript/src/opik/rest_api/**",
"**/*.snap",
"**/*.svg",
"**/*.png",
];
const globToRegExp = (glob) => {
let re = "";
for (let i = 0; i < glob.length; i++) {
if (glob.startsWith("**/", i)) { re += "(?:[^/]+/)*"; i += 2; continue; }
if (glob.startsWith("**", i)) { re += ".*"; i += 1; continue; }
const c = glob[i];
if (c === "*") { re += "[^/]*"; continue; }
if (".+^${}()|[]\\".includes(c)) { re += "\\" + c; continue; }
re += c;
}
return new RegExp("^" + re + "$");
};
const ignoreRes = IGNORE_GLOBS.map(globToRegExp);
const isIgnored = (path) => ignoreRes.some((re) => re.test(path));
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const total = files
.filter((f) => !isIgnored(f.filename))
.reduce((sum, f) => sum + f.additions + f.deletions, 0);
const bucket =
BUCKETS.find((b) => b.maxSize !== null && total <= b.maxSize) ||
BUCKETS[BUCKETS.length - 1];
core.info(`PR #${prNumber}: ${total} changed LOC (excluding ignored files) -> ${bucket.name}`);
// Keep the label's color/emoji authoritative regardless of how it was first created.
try {
const { data: existing } = await github.rest.issues.getLabel({
owner,
repo,
name: bucket.name,
});
if (existing.color !== bucket.color) {
await github.rest.issues.updateLabel({
owner,
repo,
name: bucket.name,
color: bucket.color,
});
core.info(`Updated label color: ${bucket.name} -> #${bucket.color}`);
}
} catch (err) {
if (err.status !== 404) throw err;
await github.rest.issues.createLabel({
owner,
repo,
name: bucket.name,
color: bucket.color,
});
core.info(`Created label: ${bucket.name} (#${bucket.color})`);
}
// Read the live label set rather than the event payload, which is a stale snapshot
// from when the event fired and predates the labeler job's sync.
const live = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const current = live.map((l) => l.name);
core.info(`PR #${prNumber} live labels: [${current.join(", ")}]`);
// Reconcile with an incremental delta that touches only the managed size buckets:
// remove any bucket that isn't the target, add the target if missing. The needs:
// labeler ordering already serializes this against the path labeler, so there is no
// concurrent writer to interleave with — and unlike setLabels, a delta never rewrites
// the full set, so a label added by anyone else in the meantime is left untouched.
for (const stale of current.filter(
(name) => MANAGED_LABELS.includes(name) && name !== bucket.name
)) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: stale,
});
core.info(`Removed stale size label: ${stale}`);
} catch (err) {
// The label may have been removed by someone else between the read and now;
// a 404 is benign. Swallow it so the loop and the add below still run.
if (err.status !== 404) throw err;
core.info(`Stale size label already gone (404): ${stale}`);
}
}
if (!current.includes(bucket.name)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [bucket.name],
});
core.info(`Added size label: ${bucket.name}`);
} else {
core.info(`Size label already present, no add needed: ${bucket.name}`);
}