* feat: Add GitHub Actions workflows for security audits, CodeQL analysis, contributor updates, performance benchmarks, integration tests, fuzz testing, and reusable Rust build jobs - Implemented `sec-audit.yml` for Rust package security audits using `rustsec/audit-check` and `cargo-deny-action`. - Created `sec-codeql.yml` for CodeQL analysis scheduled twice daily. - Added `sync-contributors.yml` to update the NOTICE file with new contributors automatically. - Introduced `test-benchmarks.yml` for performance benchmarks using Criterion. - Established `test-e2e.yml` for running integration and end-to-end tests. - Developed `test-fuzz.yml` for fuzz testing with configurable runtime. - Created `test-rust-build.yml` as a reusable job for executing Rust commands with customizable parameters. - Documented main branch delivery flows in `main-branch-flow.md` for clarity on CI/CD processes. * ci(workflows): update workflow scripts and rename for clarity; remove obsolete lint feedback script * chore(ci): externalize workflow scripts and relocate main flow doc
190 lines
6.4 KiB
JavaScript
190 lines
6.4 KiB
JavaScript
// Run safe intake checks for PR events and maintain a single sticky comment.
|
|
// Used by .github/workflows/pr-intake-checks.yml via actions/github-script.
|
|
|
|
module.exports = async ({ github, context, core }) => {
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const pr = context.payload.pull_request;
|
|
if (!pr) return;
|
|
|
|
const marker = "<!-- pr-intake-checks -->";
|
|
const legacyMarker = "<!-- pr-intake-sanity -->";
|
|
const requiredSections = [
|
|
"## Summary",
|
|
"## Validation Evidence (required)",
|
|
"## Security Impact (required)",
|
|
"## Privacy and Data Hygiene (required)",
|
|
"## Rollback Plan (required)",
|
|
];
|
|
const body = pr.body || "";
|
|
|
|
const missingSections = requiredSections.filter((section) => !body.includes(section));
|
|
const missingFields = [];
|
|
const requiredFieldChecks = [
|
|
["summary problem", /- Problem:\s*\S+/m],
|
|
["summary why it matters", /- Why it matters:\s*\S+/m],
|
|
["summary what changed", /- What changed:\s*\S+/m],
|
|
["validation commands", /Commands and result summary:\s*[\s\S]*```/m],
|
|
["security risk/mitigation", /- New permissions\/capabilities\?\s*\(`Yes\/No`\):\s*\S+/m],
|
|
["privacy status", /- Data-hygiene status\s*\(`pass\|needs-follow-up`\):\s*\S+/m],
|
|
["rollback plan", /- Fast rollback command\/path:\s*\S+/m],
|
|
];
|
|
for (const [name, pattern] of requiredFieldChecks) {
|
|
if (!pattern.test(body)) {
|
|
missingFields.push(name);
|
|
}
|
|
}
|
|
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
per_page: 100,
|
|
});
|
|
|
|
const formatWarnings = [];
|
|
const dangerousProblems = [];
|
|
for (const file of files) {
|
|
const patch = file.patch || "";
|
|
if (!patch) continue;
|
|
const lines = patch.split("\n");
|
|
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
const line = lines[idx];
|
|
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
const added = line.slice(1);
|
|
const lineNo = idx + 1;
|
|
if (/\t/.test(added)) {
|
|
formatWarnings.push(`${file.filename}:patch#${lineNo} contains tab characters`);
|
|
}
|
|
if (/[ \t]+$/.test(added)) {
|
|
formatWarnings.push(`${file.filename}:patch#${lineNo} contains trailing whitespace`);
|
|
}
|
|
if (/^(<<<<<<<|=======|>>>>>>>)/.test(added)) {
|
|
dangerousProblems.push(`${file.filename}:patch#${lineNo} contains merge conflict markers`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const workflowFilesChanged = files
|
|
.map((file) => file.filename)
|
|
.filter((name) => name.startsWith(".github/workflows/"));
|
|
|
|
const advisoryFindings = [];
|
|
const blockingFindings = [];
|
|
if (missingSections.length > 0) {
|
|
advisoryFindings.push(`Missing required PR template sections: ${missingSections.join(", ")}`);
|
|
}
|
|
if (missingFields.length > 0) {
|
|
advisoryFindings.push(`Incomplete required PR template fields: ${missingFields.join(", ")}`);
|
|
}
|
|
if (formatWarnings.length > 0) {
|
|
advisoryFindings.push(`Formatting issues in added lines (${formatWarnings.length})`);
|
|
}
|
|
if (dangerousProblems.length > 0) {
|
|
blockingFindings.push(`Dangerous patch markers found (${dangerousProblems.length})`);
|
|
}
|
|
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
per_page: 100,
|
|
});
|
|
const existing = comments.find((comment) => {
|
|
const body = comment.body || "";
|
|
return body.includes(marker) || body.includes(legacyMarker);
|
|
});
|
|
|
|
if (advisoryFindings.length === 0 && blockingFindings.length === 0) {
|
|
if (existing) {
|
|
await github.rest.issues.deleteComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existing.id,
|
|
});
|
|
}
|
|
core.info("PR intake sanity checks passed.");
|
|
return;
|
|
}
|
|
|
|
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
|
const advisoryDetails = [];
|
|
if (formatWarnings.length > 0) {
|
|
advisoryDetails.push(...formatWarnings.slice(0, 20).map((entry) => `- ${entry}`));
|
|
if (formatWarnings.length > 20) {
|
|
advisoryDetails.push(`- ...and ${formatWarnings.length - 20} more issue(s)`);
|
|
}
|
|
}
|
|
const blockingDetails = [];
|
|
if (dangerousProblems.length > 0) {
|
|
blockingDetails.push(...dangerousProblems.slice(0, 20).map((entry) => `- ${entry}`));
|
|
if (dangerousProblems.length > 20) {
|
|
blockingDetails.push(`- ...and ${dangerousProblems.length - 20} more issue(s)`);
|
|
}
|
|
}
|
|
|
|
const isBlocking = blockingFindings.length > 0;
|
|
|
|
const ownerApprovalNote = workflowFilesChanged.length > 0
|
|
? [
|
|
"",
|
|
"Workflow files changed in this PR:",
|
|
...workflowFilesChanged.map((name) => `- \`${name}\``),
|
|
"",
|
|
"Reminder: workflow changes require owner approval via `CI Required Gate`.",
|
|
].join("\n")
|
|
: "";
|
|
|
|
const commentBody = [
|
|
marker,
|
|
isBlocking
|
|
? "### PR intake checks failed (blocking)"
|
|
: "### PR intake checks found warnings (non-blocking)",
|
|
"",
|
|
isBlocking
|
|
? "Fast safe checks found blocking safety issues:"
|
|
: "Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.",
|
|
...(blockingFindings.length > 0 ? blockingFindings.map((entry) => `- ${entry}`) : []),
|
|
...(advisoryFindings.length > 0 ? advisoryFindings.map((entry) => `- ${entry}`) : []),
|
|
"",
|
|
"Action items:",
|
|
"1. Complete required PR template sections/fields.",
|
|
"2. Remove tabs, trailing whitespace, and merge conflict markers from added lines.",
|
|
"3. Re-run local checks before pushing:",
|
|
" - `./scripts/ci/rust_quality_gate.sh`",
|
|
" - `./scripts/ci/rust_strict_delta_gate.sh`",
|
|
" - `./scripts/ci/docs_quality_gate.sh`",
|
|
"",
|
|
`Run logs: ${runUrl}`,
|
|
"",
|
|
"Detected blocking line issues (sample):",
|
|
...(blockingDetails.length > 0 ? blockingDetails : ["- none"]),
|
|
"",
|
|
"Detected advisory line issues (sample):",
|
|
...(advisoryDetails.length > 0 ? advisoryDetails : ["- none"]),
|
|
ownerApprovalNote,
|
|
].join("\n");
|
|
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existing.id,
|
|
body: commentBody,
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
body: commentBody,
|
|
});
|
|
}
|
|
|
|
if (isBlocking) {
|
|
core.setFailed("PR intake sanity checks found blocking issues. See sticky comment for details.");
|
|
return;
|
|
}
|
|
|
|
core.info("PR intake sanity checks found advisory issues only.");
|
|
};
|