mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-06 05:55:17 +01:00
9de89278cd
* Move inline workflow mjs scripts to dedicated files, add to eslint config * Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
196 lines
5.5 KiB
JavaScript
196 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Checks that a pull request follows the contribution standards: it must use the
|
|
// PR template, tick exactly one "Type of change" option, and describe the change.
|
|
// Labels and comments the PR when it does not, and fails the check so it blocks
|
|
// merging. Invoked from the `check` job in .github/workflows/pull-request-standards.yaml
|
|
// via actions/github-script:
|
|
//
|
|
// const { default: checkStandards } =
|
|
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`);
|
|
// await checkStandards({ github, context, core });
|
|
|
|
export default async function checkPullRequestStandards({
|
|
github,
|
|
context,
|
|
core,
|
|
}) {
|
|
const pr = context.payload.pull_request;
|
|
|
|
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
|
if (pr.user.type === "Bot") {
|
|
core.info(`Skipping bot author: ${pr.user.login}`);
|
|
return;
|
|
}
|
|
if (pr.draft) {
|
|
core.info("Skipping draft pull request");
|
|
return;
|
|
}
|
|
try {
|
|
await github.rest.orgs.checkMembershipForUser({
|
|
org: "home-assistant",
|
|
username: pr.user.login,
|
|
});
|
|
core.info(`Skipping organization member: ${pr.user.login}`);
|
|
return;
|
|
} catch (_error) {
|
|
core.info(
|
|
`${pr.user.login} is not an organization member, checking standards`
|
|
);
|
|
}
|
|
|
|
const label = "Needs Template";
|
|
const marker = "<!-- pr-standards-check -->";
|
|
const { owner, repo } = context.repo;
|
|
const issue_number = pr.number;
|
|
|
|
let body = pr.body || "";
|
|
let previous;
|
|
do {
|
|
previous = body;
|
|
body = body.replace(/<!--[\s\S]*?-->/g, "");
|
|
} while (body !== previous);
|
|
const normalized = body.toLowerCase();
|
|
|
|
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
|
const ignoreMissing = async (fn) => {
|
|
try {
|
|
await fn();
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
core.info("Target already removed, nothing to do");
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
|
const setMinimized = async (subjectId, minimized) => {
|
|
const mutation = minimized
|
|
? `mutation($id: ID!) {
|
|
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
|
clientMutationId
|
|
}
|
|
}`
|
|
: `mutation($id: ID!) {
|
|
unminimizeComment(input: { subjectId: $id }) {
|
|
clientMutationId
|
|
}
|
|
}`;
|
|
try {
|
|
await github.graphql(mutation, { id: subjectId });
|
|
} catch (error) {
|
|
core.info(
|
|
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// Content of a "## <name>" section, or null when the heading is absent.
|
|
const section = (name) => {
|
|
const match = body.match(
|
|
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
|
);
|
|
return match ? match[1] : null;
|
|
};
|
|
|
|
const problems = [];
|
|
|
|
const requiredHeadings = [
|
|
"## proposed change",
|
|
"## type of change",
|
|
"## checklist",
|
|
];
|
|
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
|
problems.push(
|
|
"Use the pull request template without removing its sections."
|
|
);
|
|
}
|
|
|
|
const typeOfChange = section("type of change");
|
|
if (typeOfChange !== null) {
|
|
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
|
if (ticked !== 1) {
|
|
problems.push('Select exactly one option under "Type of change".');
|
|
}
|
|
}
|
|
|
|
const proposedChange = section("proposed change");
|
|
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
|
problems.push('Describe your changes under "Proposed change".');
|
|
}
|
|
|
|
const isValid = problems.length === 0;
|
|
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
per_page: 100,
|
|
});
|
|
const existing = comments.find((c) => c.body.includes(marker));
|
|
const hasLabel = pr.labels.some((l) => l.name === label);
|
|
|
|
if (isValid) {
|
|
core.info("Pull request standards met");
|
|
|
|
if (hasLabel) {
|
|
await ignoreMissing(() =>
|
|
github.rest.issues.removeLabel({
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
name: label,
|
|
})
|
|
);
|
|
}
|
|
if (existing) {
|
|
await setMinimized(existing.node_id, true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
|
|
|
if (!hasLabel) {
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
labels: [label],
|
|
});
|
|
}
|
|
|
|
const message =
|
|
`${marker}\n` +
|
|
`Hey @${pr.user.login}!\n\n` +
|
|
`Thank you for your contribution! To help reviewers, please update ` +
|
|
`this pull request to follow our pull request standards:\n\n` +
|
|
problems.map((p) => `- ${p}`).join("\n") +
|
|
`\n\n` +
|
|
`Please complete the ` +
|
|
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
|
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
|
`for more on creating a great pull request (see point 6).`;
|
|
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existing.id,
|
|
body: message,
|
|
});
|
|
await setMinimized(existing.node_id, false);
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
body: message,
|
|
});
|
|
}
|
|
|
|
// Fail this check so it can block the PR from being merged
|
|
core.setFailed(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
|
}
|