feat: release alert badge, air-date countdown, and dropped-show filtering for Continue Watching #644
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 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."); | |
| } |