NuvioStreaming/.github/workflows/pr-template-check.yml
2026-05-13 18:38:21 +05:30

148 lines
6 KiB
YAML

name: PR Template Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
pull-requests: read
jobs:
validate_pr_body:
runs-on: ubuntu-latest
steps:
- name: Validate required PR sections
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const body = (pr.body || "").trim();
function sectionContent(title) {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`^##\\s+${escaped}\\s*$`, "m");
const match = body.match(re);
if (!match) return null;
const start = match.index + match[0].length;
const rest = body.slice(start);
const next = rest.search(/^##\s+/m);
return (next === -1 ? rest : rest.slice(0, next)).trim();
}
function cleanedContent(title) {
const content = sectionContent(title);
if (content === null) return null;
return content
.replace(/<!--[\s\S]*?-->/g, "")
.replace(/`/g, "")
.replace(/\s+/g, " ")
.trim();
}
function checkedLines(content) {
return (content || "").match(/^\s*-\s+\[[xX]\]\s+.+$/gm) || [];
}
function uncheckedLines(content) {
return (content || "").match(/^\s*-\s+\[\s\]\s+.+$/gm) || [];
}
function hasIssueReference(content) {
return /(^|\s)(#\d+|https:\/\/github\.com\/\S+\/issues\/\d+)/i.test(content || "");
}
const required = [
"Summary",
"PR type",
"Why",
"Issue or approval",
"UI / behavior impact",
"Policy check",
"Scope boundaries",
"Testing",
"Screenshots / Video",
"Breaking changes",
"Linked issues",
];
const missing = [];
const empty = [];
const failedRules = [];
for (const name of required) {
const content = sectionContent(name);
if (content === null) {
missing.push(name);
continue;
}
const cleaned = cleanedContent(name);
const normalized = cleaned.toLowerCase();
const allowsNone = name === "Breaking changes" || name === "Screenshots / Video";
if (
cleaned.length < 4 ||
(!allowsNone && ["none", "n/a", "na", "not applicable"].includes(normalized)) ||
normalized.includes("what changed in this pr") ||
normalized.includes("why this change is needed") ||
normalized.includes("what you tested") ||
normalized.includes("example: fixes #123")
) {
empty.push(name);
}
}
const prTypeContent = sectionContent("PR type") || "";
const prTypeChecked = checkedLines(prTypeContent);
if (sectionContent("PR type") !== null && prTypeChecked.length !== 1) {
failedRules.push("Check exactly one PR type.");
}
const impactContent = sectionContent("UI / behavior impact") || "";
const impactChecked = checkedLines(impactContent);
if (sectionContent("UI / behavior impact") !== null && impactChecked.length === 0) {
failedRules.push("Check at least one UI / behavior impact box.");
}
const policyContent = sectionContent("Policy check") || "";
const policyChecked = checkedLines(policyContent);
const policyUnchecked = uncheckedLines(policyContent);
if (sectionContent("Policy check") !== null && policyChecked.length === 0) {
failedRules.push("Policy check must include checked boxes.");
}
if (policyUnchecked.length) {
failedRules.push("Every Policy check box must be checked.");
}
const checkedTypeText = prTypeChecked.join(" ");
const issueRequired =
/bug fix|ui glitch|behavior bug|approved larger|approved directional/i.test(checkedTypeText);
const issueText = [
cleanedContent("Issue or approval") || "",
cleanedContent("Linked issues") || "",
].join(" ");
if (issueRequired && !hasIssueReference(issueText)) {
failedRules.push("Bug fixes, UI glitch fixes, behavior fixes, and approved changes must link an issue or approved request.");
}
const uiChanged = checkedLines(impactContent).some((line) =>
/UI changed only to fix a documented glitch\/bug|UI change has explicit maintainer approval/i.test(line)
);
const screenshotText = (cleanedContent("Screenshots / Video") || "").toLowerCase();
if (uiChanged && ["none", "n/a", "na", "not a ui change", "not applicable"].includes(screenshotText)) {
failedRules.push("UI changes must include before/after screenshots or video.");
}
if (missing.length || empty.length || failedRules.length) {
const lines = [
"PR description is missing required detail.",
"",
];
if (missing.length) lines.push(`Missing sections: ${missing.join(", ")}`);
if (empty.length) lines.push(`Incomplete sections: ${empty.join(", ")}`);
if (failedRules.length) lines.push(`Failed policy rules: ${failedRules.join(" ")}`);
lines.push("");
lines.push("Please complete the PR template and make sure the PR fits CONTRIBUTING.md before review.");
core.setFailed(lines.join("\n"));
} else {
core.info("PR template check passed.");
}