94 lines
4.4 KiB
YAML
94 lines
4.4 KiB
YAML
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}`;
|
|
const oversized = size === "XL" && !labels.has("size-exempt");
|
|
core.info(`counted ${counted} lines (${ignored} ignored) in ${files.length} files -> ${wanted}`);
|
|
await core.summary
|
|
.addRaw(`PR size: **${size}**, ${counted} counted lines (${ignored} ignored), limit ${LIMIT}.`)
|
|
.write();
|
|
if (oversized) {
|
|
core.setFailed(`${counted} changed source lines exceed the ${LIMIT}-line limit; split the pull request or add size-exempt.`);
|
|
}
|
|
// Fork tokens are read-only. The size decision above is still enforced;
|
|
// optional labels and comments must not turn a valid PR into a 403 failure.
|
|
if (pr.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
|
|
core.info("Fork PR: size is reported in the summary; no label/comment writes.");
|
|
return;
|
|
}
|
|
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] });
|
|
}
|
|
|
|
if (!oversized) 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"),
|
|
});
|
|
}
|