mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 20:01:15 +02:00
- Restrict release workflow defaults to read-only access - Grant `id-token` only to the publish job - Document safe handling for `pull_request_target` in PR size checks
295 lines
10 KiB
YAML
295 lines
10 KiB
YAML
name: PR Size
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
prepare-config:
|
|
name: Prepare PR size config
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
labels_json: ${{ steps.config.outputs.labels_json }}
|
|
steps:
|
|
- id: config
|
|
name: Build PR size label config
|
|
uses: actions/github-script@v8
|
|
with:
|
|
result-encoding: string
|
|
script: |
|
|
const managedLabels = [
|
|
{
|
|
name: "size:XS",
|
|
color: "0e8a16",
|
|
description: "0-9 effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
{
|
|
name: "size:S",
|
|
color: "5ebd3e",
|
|
description: "10-29 effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
{
|
|
name: "size:M",
|
|
color: "fbca04",
|
|
description: "30-99 effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
{
|
|
name: "size:L",
|
|
color: "fe7d37",
|
|
description: "100-499 effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
{
|
|
name: "size:XL",
|
|
color: "d93f0b",
|
|
description: "500-999 effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
{
|
|
name: "size:XXL",
|
|
color: "b60205",
|
|
description: "1,000+ effective changed lines (test files excluded in mixed PRs).",
|
|
},
|
|
];
|
|
|
|
core.setOutput("labels_json", JSON.stringify(managedLabels));
|
|
sync-label-definitions:
|
|
name: Sync PR size label definitions
|
|
needs: prepare-config
|
|
if: github.event_name != 'pull_request_target'
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
steps:
|
|
- name: Ensure PR size labels exist
|
|
uses: actions/github-script@v8
|
|
env:
|
|
PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }}
|
|
with:
|
|
script: |
|
|
const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");
|
|
|
|
for (const label of managedLabels) {
|
|
try {
|
|
const { data: existing } = await github.rest.issues.getLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: label.name,
|
|
});
|
|
|
|
if (
|
|
existing.color !== label.color ||
|
|
(existing.description ?? "") !== label.description
|
|
) {
|
|
await github.rest.issues.updateLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: label.name,
|
|
color: label.color,
|
|
description: label.description,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
await github.rest.issues.createLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: label.name,
|
|
color: label.color,
|
|
description: label.description,
|
|
});
|
|
} catch (createError) {
|
|
if (createError.status !== 422) {
|
|
throw createError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
label:
|
|
name: Label PR size
|
|
needs: prepare-config
|
|
if: github.event_name == 'pull_request_target'
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
issues: read
|
|
pull-requests: write
|
|
concurrency:
|
|
group: pr-size-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
steps:
|
|
# This pull_request_target job may fetch untrusted PR commits only as passive
|
|
# git data. Do not add dependency installs, build/test scripts, or cache
|
|
# actions here; use pull_request plus workflow_run for that pattern instead.
|
|
- name: Checkout base repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Sync PR size label
|
|
uses: actions/github-script@v8
|
|
env:
|
|
PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }}
|
|
with:
|
|
script: |
|
|
const { execFileSync } = require("node:child_process");
|
|
|
|
const issueNumber = context.payload.pull_request.number;
|
|
const baseSha = context.payload.pull_request.base.sha;
|
|
const headSha = context.payload.pull_request.head.sha;
|
|
const headTrackingRef = `refs/remotes/pr-size/${issueNumber}`;
|
|
const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]");
|
|
const managedLabelNames = new Set(managedLabels.map((label) => label.name));
|
|
// Keep this aligned with the repo's test entrypoints and test-only support files.
|
|
const testExcludePathspecs = [
|
|
":(glob,exclude)**/__tests__/**",
|
|
":(glob,exclude)**/test/**",
|
|
":(glob,exclude)**/tests/**",
|
|
":(glob,exclude)apps/server/integration/**",
|
|
":(glob,exclude)**/*.test.*",
|
|
":(glob,exclude)**/*.spec.*",
|
|
":(glob,exclude)**/*.browser.*",
|
|
":(glob,exclude)**/*.integration.*",
|
|
];
|
|
|
|
const sumNumstat = (text) =>
|
|
text
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.reduce((total, line) => {
|
|
const [insertionsRaw = "0", deletionsRaw = "0"] = line.split("\t");
|
|
const additions =
|
|
insertionsRaw === "-" ? 0 : Number.parseInt(insertionsRaw, 10) || 0;
|
|
const deletions =
|
|
deletionsRaw === "-" ? 0 : Number.parseInt(deletionsRaw, 10) || 0;
|
|
|
|
return total + additions + deletions;
|
|
}, 0);
|
|
|
|
const resolveSizeLabel = (totalChangedLines) => {
|
|
if (totalChangedLines < 10) {
|
|
return "size:XS";
|
|
}
|
|
|
|
if (totalChangedLines < 30) {
|
|
return "size:S";
|
|
}
|
|
|
|
if (totalChangedLines < 100) {
|
|
return "size:M";
|
|
}
|
|
|
|
if (totalChangedLines < 500) {
|
|
return "size:L";
|
|
}
|
|
|
|
if (totalChangedLines < 1000) {
|
|
return "size:XL";
|
|
}
|
|
|
|
return "size:XXL";
|
|
};
|
|
|
|
execFileSync("git", ["fetch", "--no-tags", "origin", baseSha], {
|
|
stdio: "inherit",
|
|
});
|
|
|
|
execFileSync(
|
|
"git",
|
|
["fetch", "--no-tags", "origin", `+refs/pull/${issueNumber}/head:${headTrackingRef}`],
|
|
{
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
|
|
const resolvedHeadSha = execFileSync("git", ["rev-parse", headTrackingRef], {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
|
|
if (resolvedHeadSha !== headSha) {
|
|
core.warning(
|
|
`Fetched head SHA ${resolvedHeadSha} does not match pull request head SHA ${headSha}; using fetched ref for sizing.`,
|
|
);
|
|
}
|
|
|
|
execFileSync("git", ["cat-file", "-e", `${baseSha}^{commit}`], {
|
|
stdio: "inherit",
|
|
});
|
|
|
|
const diffArgs = [
|
|
"diff",
|
|
"--numstat",
|
|
"--ignore-all-space",
|
|
"--ignore-blank-lines",
|
|
`${baseSha}...${resolvedHeadSha}`,
|
|
];
|
|
|
|
const totalChangedLines = sumNumstat(
|
|
execFileSync(
|
|
"git",
|
|
diffArgs,
|
|
{ encoding: "utf8" },
|
|
),
|
|
);
|
|
const nonTestChangedLines = sumNumstat(
|
|
execFileSync("git", [...diffArgs, "--", ".", ...testExcludePathspecs], {
|
|
encoding: "utf8",
|
|
}),
|
|
);
|
|
const testChangedLines = Math.max(0, totalChangedLines - nonTestChangedLines);
|
|
|
|
const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines;
|
|
const nextLabelName = resolveSizeLabel(changedLines);
|
|
|
|
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
per_page: 100,
|
|
});
|
|
|
|
for (const label of currentLabels) {
|
|
if (!managedLabelNames.has(label.name) || label.name === nextLabelName) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
name: label.name,
|
|
});
|
|
} catch (removeError) {
|
|
if (removeError.status !== 404) {
|
|
throw removeError;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!currentLabels.some((label) => label.name === nextLabelName)) {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
labels: [nextLabelName],
|
|
});
|
|
}
|
|
|
|
const classification =
|
|
nonTestChangedLines === 0
|
|
? testChangedLines > 0
|
|
? "test-only PR"
|
|
: "no line changes"
|
|
: testChangedLines > 0
|
|
? "test lines excluded"
|
|
: "all non-test changes";
|
|
|
|
core.info(
|
|
`PR #${issueNumber}: ${nonTestChangedLines} non-test lines, ${testChangedLines} test lines, ${changedLines} effective lines -> ${nextLabelName} (${classification})`,
|
|
);
|