Skip to content

Commit 75836f3

Browse files
authored
ci: add PR size labeling workflow (#2042)
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent e8044ed commit 75836f3

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: PR size label
2+
3+
# Applies a "size: XS/S/M/L/XL" label to PRs from a weighted diff score
4+
# (code + 50% docs + 50% tests), excluding poetry.lock and recorded test data
5+
# so generated fixtures do not dominate the size.
6+
#
7+
# Uses pull_request_target so it has a write token on fork PRs. Safe because the
8+
# job only reads the PR file list via the API and calls the labels API; it never
9+
# checks out or executes PR head code. Do not add a checkout of the PR head here.
10+
11+
on:
12+
# zizmor: ignore[dangerous-triggers] -- no checkout or execution of PR head
13+
# code; the job reads the PR file list and calls the labels API only.
14+
pull_request_target:
15+
types: [opened, synchronize, reopened]
16+
17+
permissions: {}
18+
19+
concurrency:
20+
group: pr-size-label-${{ github.event.pull_request.number }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
size-label:
25+
runs-on: ubuntu-latest
26+
permissions:
27+
pull-requests: write
28+
steps:
29+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7
30+
with:
31+
script: |
32+
const files = await github.paginate(github.rest.pulls.listFiles, {
33+
owner: context.repo.owner,
34+
repo: context.repo.repo,
35+
pull_number: context.payload.pull_request.number,
36+
});
37+
38+
const isExcluded = (name) =>
39+
name === "poetry.lock" ||
40+
name.includes("/cassettes/") ||
41+
name.includes("/recordings/") ||
42+
name.includes("/fixtures/");
43+
44+
const isDocs = (name) =>
45+
name.startsWith("docs/") || name.endsWith(".md");
46+
47+
const isTests = (name) => name.startsWith("tests/");
48+
49+
let code = 0;
50+
let docs = 0;
51+
let tests = 0;
52+
for (const file of files) {
53+
if (isExcluded(file.filename)) continue;
54+
const changed = file.additions + file.deletions;
55+
if (isDocs(file.filename)) docs += changed;
56+
else if (isTests(file.filename)) tests += changed;
57+
else code += changed;
58+
}
59+
60+
const score = code + Math.floor(docs / 2) + Math.floor(tests / 2);
61+
const size =
62+
score < 10 ? "XS" :
63+
score < 50 ? "S" :
64+
score < 250 ? "M" :
65+
score < 1000 ? "L" : "XL";
66+
const label = `size: ${size}`;
67+
68+
const current = context.payload.pull_request.labels
69+
.map((existing) => existing.name)
70+
.filter((name) => name.startsWith("size: "));
71+
72+
for (const stale of current) {
73+
if (stale === label) continue;
74+
try {
75+
await github.rest.issues.removeLabel({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
issue_number: context.payload.pull_request.number,
79+
name: stale,
80+
});
81+
} catch (error) {
82+
if (error.status !== 404) throw error;
83+
}
84+
}
85+
86+
if (!current.includes(label)) {
87+
await github.rest.issues.addLabels({
88+
owner: context.repo.owner,
89+
repo: context.repo.repo,
90+
issue_number: context.payload.pull_request.number,
91+
labels: [label],
92+
});
93+
}

0 commit comments

Comments
 (0)