ci: separate PR checks from packaging and structure issue intake #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR size | |
| # Labels every pull request by the number of changed source lines and fails | |
| # oversized ones so they get split before review. Generated and documentation | |
| # files do not count. Maintainers bypass the limit with the `size-exempt` | |
| # label (a release integration branch is the usual case). | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, labeled, unlabeled] | |
| # Required checks must also report inside the merge queue; the size was | |
| # already enforced on the pull request, so this is a pass-through. | |
| merge_group: | |
| permissions: | |
| pull-requests: write | |
| concurrency: | |
| group: pr-size-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| pr-size: | |
| name: pr-size | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@v7 | |
| if: github.event_name == 'pull_request' | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const LIMIT = 1500; | |
| const IGNORED = [ | |
| /^Cargo\.lock$/, /(^|\/)package-lock\.json$/, /(^|\/)yarn\.lock$/, | |
| /^docs\//, /\.md$/, /\.snap$/, /\.svg$/, /\.png$/, /\.ico$/, | |
| /^assets\//, /^locales\//, /\.lock$/, | |
| ]; | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| ...context.repo, pull_number: pr.number, per_page: 100, | |
| }); | |
| let counted = 0, ignored = 0; | |
| for (const f of files) { | |
| const lines = f.additions + f.deletions; | |
| if (IGNORED.some((re) => re.test(f.filename))) ignored += lines; else counted += lines; | |
| } | |
| const size = counted <= 100 ? "S" : counted <= 500 ? "M" : counted <= LIMIT ? "L" : "XL"; | |
| const labels = new Set(pr.labels.map((l) => l.name)); | |
| const wanted = `size/${size}`; | |
| for (const l of labels) { | |
| if (l.startsWith("size/") && l !== wanted) { | |
| await github.rest.issues.removeLabel({ ...context.repo, issue_number: pr.number, name: l }).catch(() => {}); | |
| } | |
| } | |
| if (!labels.has(wanted)) { | |
| await github.rest.issues.addLabels({ ...context.repo, issue_number: pr.number, labels: [wanted] }); | |
| } | |
| core.info(`counted ${counted} lines (${ignored} ignored) in ${files.length} files -> ${wanted}`); | |
| if (size !== "XL" || labels.has("size-exempt")) return; | |
| const marker = "<!-- pr-size-guard -->"; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| ...context.repo, issue_number: pr.number, per_page: 100, | |
| }); | |
| if (!comments.some((c) => c.body && c.body.includes(marker))) { | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number: pr.number, | |
| body: [ | |
| marker, | |
| `This pull request changes **${counted}** source lines (limit ${LIMIT}; docs, lockfiles and assets excluded). Changes this large cannot be reviewed reliably, so please split it into independent pull requests — one behaviour change each, with its own tests.`, | |
| "", | |
| `本 PR 改动 **${counted}** 行源码(上限 ${LIMIT},不计文档、lockfile、资源)。这么大的改动无法可靠审查,请拆成多个相互独立的 PR,每个只做一件事并附测试。`, | |
| "", | |
| "Maintainers can add the `size-exempt` label for integration branches. / 维护者可对集成分支加 `size-exempt` 放行。", | |
| ].join("\n"), | |
| }); | |
| } | |
| core.setFailed(`${counted} changed source lines exceed the ${LIMIT}-line limit; split the pull request or add size-exempt.`); |