Merge pull request #47898 from is-a-dev/automated-pr-template

feat(ci): pr template validation
This commit is contained in:
William Harrison authored and GitHub committed 2026-08-19 11:57:41 +08:00
commit 1a5f4f9b91
2 files changed
+210

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
name: PR Template
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
contents: read
pull-requests: write
issues: write
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.repository.default_branch }}
- name: Validate PR template
id: validate
continue-on-error: true
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: node scripts/validate-pr-template.js
- name: Handle incomplete PR
if: steps.validate.outcome == 'failure'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT }}
script: |
const label = "reason: incomplete pr";
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label
});
} catch (error) {
if (error.status === 404) {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label,
color: "D93F0B",
description: "Your PR template is not complete. (e.g. missing the requirements form, missing website preview)"
});
} else {
throw error;
}
}
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
if (!labels.data.some((item) => item.name === label)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [label]
});
}
const marker = "<!-- is-a-dev-bot: incomplete-pr-template -->";
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
}
);
const alreadyCommented = comments.some(
(comment) =>
comment.user?.login === "is-a-dev-bot" &&
comment.body?.includes(marker)
);
if (!alreadyCommented) {
const body = [
marker,
"## ❌ Incomplete PR template",
"",
"This pull request is missing required information from the PR template.",
"",
"Please make sure that:",
"",
"- Every required checkbox is checked.",
"- **Website Preview** is filled out.",
"- **Website Purpose** is filled out.",
"",
"Once the template has been completed, this check will run again automatically."
].join("\n");
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
}
- name: Remove incomplete label
if: steps.validate.outcome == 'success'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.BOT }}
script: |
const label = "reason: incomplete pr";
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: label
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
+75
View File
@@ -0,0 +1,75 @@
const body = process.env.PR_BODY || "";
const REQUIRED_CHECKBOXES = [
"I agree to the [Terms of Service]",
"My file is following the [domain structure]",
"My website is reachable and completed.",
"My website is software development related.",
"My website is not for commercial use.",
"I have provided sufficient contact information in the `owner` key.",
"I have provided a link to my website below.",
];
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function isRequiredCheckboxChecked(text) {
const regex = new RegExp(
`-\\s*\\[[xX]\\]\\s*(?:<!--[^>]*-->\\s*)?${escapeRegExp(text)}`,
"i",
);
return regex.test(body);
}
function getSection(startMarker, endMarker) {
const regex = new RegExp(
`${escapeRegExp(startMarker)}([\\s\\S]*?)${escapeRegExp(endMarker)}`,
"i",
);
const match = body.match(regex);
return match ? match[1].trim() : "";
}
const errors = [];
const uncheckedRequirements = REQUIRED_CHECKBOXES.filter(
(requirement) => !isRequiredCheckboxChecked(requirement),
);
if (uncheckedRequirements.length > 0) {
errors.push(
"The following required checkboxes have not been checked:",
...uncheckedRequirements.map((requirement) => `- ${requirement}`),
);
}
if (
!getSection(
"<!-- WEBSITE_PREVIEW_START -->",
"<!-- WEBSITE_PREVIEW_END -->",
)
) {
errors.push("The **Website Preview** section has not been filled out.");
}
if (
!getSection(
"<!-- WEBSITE_PURPOSE_START -->",
"<!-- WEBSITE_PURPOSE_END -->",
)
) {
errors.push("The **Website Purpose** section has not been filled out.");
}
if (errors.length > 0) {
console.error("PR template validation failed.");
console.error("");
console.error(errors.join("\n"));
process.exit(1);
}
console.log("PR template validation passed.");