| ... | ... | @@ -124,25 +124,49 @@ jobs: |
| 124 | 124 | group: pr-size-${{ github.event.pull_request.number }} |
| 125 | 125 | cancel-in-progress: true |
| 126 | 126 | steps: |
| 127 | - name: Checkout base repository |
| 128 | uses: actions/checkout@v4 |
| 129 | with: |
| 130 | fetch-depth: 0 |
| 127 | 131 | - name: Sync PR size label |
| 128 | 132 | uses: actions/github-script@v8 |
| 129 | 133 | env: |
| 130 | 134 | PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }} |
| 131 | 135 | with: |
| 132 | 136 | script: | |
| 137 | const { execFileSync } = require("node:child_process"); |
| 138 | |
| 133 | 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 | 143 | const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]"); |
| 135 | 144 | const managedLabelNames = new Set(managedLabels.map((label) => label.name)); |
| 136 | 145 | // Keep this aligned with the repo's test entrypoints and test-only support files. |
| 137 | | const testFilePatterns = [ |
| 138 | | /(^|\/)__tests__(\/|$)/, |
| 139 | | /(^|\/)tests?(\/|$)/, |
| 140 | | /^apps\/server\/integration\//, |
| 141 | | /\.(test|spec|browser|integration)\.[^.\/]+$/, |
| 146 | const testExcludePathspecs = [ |
| 147 | ":(glob,exclude)**/__tests__/**", |
| 148 | ":(glob,exclude)**/test/**", |
| 149 | ":(glob,exclude)**/tests/**", |
| 150 | ":(glob,exclude)apps/server/integration/**", |
| 151 | ":(glob,exclude)**/*.test.*", |
| 152 | ":(glob,exclude)**/*.spec.*", |
| 153 | ":(glob,exclude)**/*.browser.*", |
| 154 | ":(glob,exclude)**/*.integration.*", |
| 142 | 155 | ]; |
| 143 | 156 | |
| 144 | | const isTestFile = (filename) => |
| 145 | | testFilePatterns.some((pattern) => pattern.test(filename)); |
| 157 | const sumNumstat = (text) => |
| 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); |
| 146 | 170 | |
| 147 | 171 | const resolveSizeLabel = (totalChangedLines) => { |
| 148 | 172 | if (totalChangedLines < 10) { |
| ... | ... | @@ -168,40 +192,53 @@ jobs: |
| 168 | 192 | return "size:XXL"; |
| 169 | 193 | }; |
| 170 | 194 | |
| 171 | | const files = await github.paginate( |
| 172 | | github.rest.pulls.listFiles, |
| 195 | execFileSync("git", ["fetch", "--no-tags", "origin", baseSha], { |
| 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, |
| 175 | | repo: context.repo.repo, |
| 176 | | pull_number: issueNumber, |
| 177 | | per_page: 100, |
| 203 | stdio: "inherit", |
| 178 | 204 | }, |
| 179 | | (response) => response.data, |
| 180 | 205 | ); |
| 181 | 206 | |
| 182 | | if (files.length >= 3000) { |
| 207 | const resolvedHeadSha = execFileSync("git", ["rev-parse", headTrackingRef], { |
| 208 | encoding: "utf8", |
| 209 | }).trim(); |
| 210 | |
| 211 | if (resolvedHeadSha !== headSha) { |
| 183 | 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 | } |
| 187 | 216 | |
| 188 | | let testChangedLines = 0; |
| 189 | | let nonTestChangedLines = 0; |
| 190 | | |
| 191 | | for (const file of files) { |
| 192 | | const changedLinesForFile = (file.additions ?? 0) + (file.deletions ?? 0); |
| 193 | | |
| 194 | | if (changedLinesForFile === 0) { |
| 195 | | continue; |
| 196 | | } |
| 217 | execFileSync("git", ["cat-file", "-e", `${baseSha}^{commit}`], { |
| 218 | stdio: "inherit", |
| 219 | }); |
| 197 | 220 | |
| 198 | | if (isTestFile(file.filename)) { |
| 199 | | testChangedLines += changedLinesForFile; |
| 200 | | continue; |
| 201 | | } |
| 221 | const diffArgs = [ |
| 222 | "diff", |
| 223 | "--numstat", |
| 224 | "--ignore-all-space", |
| 225 | "--ignore-blank-lines", |
| 226 | `${baseSha}...${resolvedHeadSha}`, |
| 227 | ]; |
| 202 | 228 | |
| 203 | | nonTestChangedLines += changedLinesForFile; |
| 204 | | } |
| 229 | const totalChangedLines = sumNumstat( |
| 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); |
| 205 | 242 | |
| 206 | 243 | const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines; |
| 207 | 244 | const nextLabelName = resolveSizeLabel(changedLines); |