1# PR Size Check — Compute
2#
3# Calculates PR size and saves the result as an artifact. A companion
4# workflow (pr-size-label.yml) picks up the artifact via workflow_run
5# and applies labels + comments with write permissions.
6#
7# This two-workflow split is required because fork PRs receive a
8# read-only GITHUB_TOKEN. The compute step needs no write access;
9# the label/comment step runs via workflow_run on the base repo with
10# full write permissions.
11#
12# Security note: This workflow only reads PR file data via the JS API
13# and writes a JSON artifact. No untrusted input is interpolated into
14# shell commands.
15
16name: PR Size Check
17
18on:
19 pull_request:
20 types: [opened, synchronize]
21
22permissions:
23 contents: read
24 pull-requests: read
25
26jobs:
27 compute-size:
28 if: github.repository_owner == 'zed-industries'
29 runs-on: ubuntu-latest
30 timeout-minutes: 5
31 steps:
32 - name: Calculate PR size
33 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
34 with:
35 script: |
36 const fs = require('fs');
37
38 const { data: files } = await github.rest.pulls.listFiles({
39 owner: context.repo.owner,
40 repo: context.repo.repo,
41 pull_number: context.issue.number,
42 per_page: 300,
43 });
44
45 // Sum additions + deletions, excluding generated/lock files
46 const IGNORED_PATTERNS = [
47 /\.lock$/,
48 /^Cargo\.lock$/,
49 /pnpm-lock\.yaml$/,
50 /\.generated\./,
51 /\/fixtures\//,
52 /\/snapshots\//,
53 ];
54
55 let totalChanges = 0;
56 for (const file of files) {
57 const ignored = IGNORED_PATTERNS.some(p => p.test(file.filename));
58 if (!ignored) {
59 totalChanges += file.additions + file.deletions;
60 }
61 }
62
63 // Assign size bracket
64 const SIZE_BRACKETS = [
65 ['Size S', 0, 100, '0e8a16'],
66 ['Size M', 100, 400, 'fbca04'],
67 ['Size L', 400, 800, 'e99695'],
68 ['Size XL', 800, Infinity, 'b60205'],
69 ];
70
71 let sizeLabel = 'Size S';
72 let labelColor = '0e8a16';
73 for (const [label, min, max, color] of SIZE_BRACKETS) {
74 if (totalChanges >= min && totalChanges < max) {
75 sizeLabel = label;
76 labelColor = color;
77 break;
78 }
79 }
80
81 // Check if the author wrote content in the "How to Review" section.
82 const rawBody = context.payload.pull_request.body || '';
83 const howToReview = rawBody.match(/## How to Review\s*\n([\s\S]*?)(?=\n## |$)/i);
84 const hasReviewGuidance = howToReview
85 ? howToReview[1].replace(/<!--[\s\S]*?-->/g, '').trim().length > 0
86 : false;
87
88 const result = {
89 pr_number: context.issue.number,
90 total_changes: totalChanges,
91 size_label: sizeLabel,
92 label_color: labelColor,
93 has_review_guidance: hasReviewGuidance,
94 };
95
96 console.log(`PR #${result.pr_number}: ${totalChanges} LOC, ${sizeLabel}`);
97
98 fs.mkdirSync('pr-size', { recursive: true });
99 fs.writeFileSync('pr-size/result.json', JSON.stringify(result));
100
101 - name: Upload size result
102 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
103 with:
104 name: pr-size-result
105 path: pr-size/
106 retention-days: 1
107defaults:
108 run:
109 shell: bash -euxo pipefail {0}