authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-03-28 19:49:53-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-28 15:49:53-07:00
log5b1d5c1220165db816ea5eee15436df778a11aac
tree36a4e7186266c229667be7a85fb64d2c09d5bf25
parentf4617e7b19830111d88b1d914b3ca4e1396912a3
signature Signed by PGP key B5690EEEBB952194

fix(ci): use whitespace-insensitive PR size diff (#1500)


1 files changed, 68 insertions(+), 31 deletions(-)

.github/workflows/pr-size.yml+68-31
...@@ -124,25 +124,49 @@ jobs:...@@ -124,25 +124,49 @@ jobs:
124 group: pr-size-${{ github.event.pull_request.number }}124 group: pr-size-${{ github.event.pull_request.number }}
125 cancel-in-progress: true125 cancel-in-progress: true
126 steps:126 steps:
127 - name: Checkout base repository
128 uses: actions/checkout@v4
129 with:
130 fetch-depth: 0
127 - name: Sync PR size label131 - name: Sync PR size label
128 uses: actions/github-script@v8132 uses: actions/github-script@v8
129 env:133 env:
130 PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }}134 PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }}
131 with:135 with:
132 script: |136 script: |
137 const { execFileSync } = require("node:child_process");
138
133 const issueNumber = context.payload.pull_request.number;139 const issueNumber = context.payload.pull_request.number;
140 const baseSha = context.payload.pull_request.base.sha;
141 const headSha = context.payload.pull_request.head.sha;
142 const headTrackingRef = `refs/remotes/pr-size/${issueNumber}`;
134 const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");143 const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");
135 const managedLabelNames = new Set(managedLabels.map((label) => label.name));144 const managedLabelNames = new Set(managedLabels.map((label) => label.name));
136 // Keep this aligned with the repo's test entrypoints and test-only support files.145 // Keep this aligned with the repo's test entrypoints and test-only support files.
137 const testFilePatterns = [146 const testExcludePathspecs = [
138 /(^|\/)__tests__(\/|$)/,147 ":(glob,exclude)**/__tests__/**",
139 /(^|\/)tests?(\/|$)/,148 ":(glob,exclude)**/test/**",
140 /^apps\/server\/integration\//,149 ":(glob,exclude)**/tests/**",
141 /\.(test|spec|browser|integration)\.[^.\/]+$/,150 ":(glob,exclude)apps/server/integration/**",
151 ":(glob,exclude)**/*.test.*",
152 ":(glob,exclude)**/*.spec.*",
153 ":(glob,exclude)**/*.browser.*",
154 ":(glob,exclude)**/*.integration.*",
142 ];155 ];
143156
144 const isTestFile = (filename) =>157 const sumNumstat = (text) =>
145 testFilePatterns.some((pattern) => pattern.test(filename));158 text
159 .split("\n")
160 .filter(Boolean)
161 .reduce((total, line) => {
162 const [insertionsRaw = "0", deletionsRaw = "0"] = line.split("\t");
163 const additions =
164 insertionsRaw === "-" ? 0 : Number.parseInt(insertionsRaw, 10) || 0;
165 const deletions =
166 deletionsRaw === "-" ? 0 : Number.parseInt(deletionsRaw, 10) || 0;
167
168 return total + additions + deletions;
169 }, 0);
146170
147 const resolveSizeLabel = (totalChangedLines) => {171 const resolveSizeLabel = (totalChangedLines) => {
148 if (totalChangedLines < 10) {172 if (totalChangedLines < 10) {
...@@ -168,40 +192,53 @@ jobs:...@@ -168,40 +192,53 @@ jobs:
168 return "size:XXL";192 return "size:XXL";
169 };193 };
170194
171 const files = await github.paginate(195 execFileSync("git", ["fetch", "--no-tags", "origin", baseSha], {
172 github.rest.pulls.listFiles,196 stdio: "inherit",
197 });
198
199 execFileSync(
200 "git",
201 ["fetch", "--no-tags", "origin", `+refs/pull/${issueNumber}/head:${headTrackingRef}`],
173 {202 {
174 owner: context.repo.owner,203 stdio: "inherit",
175 repo: context.repo.repo,
176 pull_number: issueNumber,
177 per_page: 100,
178 },204 },
179 (response) => response.data,
180 );205 );
181206
182 if (files.length >= 3000) {207 const resolvedHeadSha = execFileSync("git", ["rev-parse", headTrackingRef], {
208 encoding: "utf8",
209 }).trim();
210
211 if (resolvedHeadSha !== headSha) {
183 core.warning(212 core.warning(
184 "The GitHub pull request files API may truncate results at 3,000 files; PR size may be undercounted.",213 `Fetched head SHA ${resolvedHeadSha} does not match pull request head SHA ${headSha}; using fetched ref for sizing.`,
185 );214 );
186 }215 }
187216
188 let testChangedLines = 0;217 execFileSync("git", ["cat-file", "-e", `${baseSha}^{commit}`], {
189 let nonTestChangedLines = 0;218 stdio: "inherit",
190219 });
191 for (const file of files) {
192 const changedLinesForFile = (file.additions ?? 0) + (file.deletions ?? 0);
193
194 if (changedLinesForFile === 0) {
195 continue;
196 }
197220
198 if (isTestFile(file.filename)) {221 const diffArgs = [
199 testChangedLines += changedLinesForFile;222 "diff",
200 continue;223 "--numstat",
201 }224 "--ignore-all-space",
225 "--ignore-blank-lines",
226 `${baseSha}...${resolvedHeadSha}`,
227 ];
202228
203 nonTestChangedLines += changedLinesForFile;229 const totalChangedLines = sumNumstat(
204 }230 execFileSync(
231 "git",
232 diffArgs,
233 { encoding: "utf8" },
234 ),
235 );
236 const nonTestChangedLines = sumNumstat(
237 execFileSync("git", [...diffArgs, "--", ".", ...testExcludePathspecs], {
238 encoding: "utf8",
239 }),
240 );
241 const testChangedLines = Math.max(0, totalChangedLines - nonTestChangedLines);
205242
206 const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines;243 const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines;
207 const nextLabelName = resolveSizeLabel(changedLines);244 const nextLabelName = resolveSizeLabel(changedLines);