ci: add safe pull request intake sanity checks (#570)
* fix(workflows): standardize runner configuration for security jobs * ci(actionlint): add Blacksmith runner label to config Add blacksmith-2vcpu-ubuntu-2404 to actionlint self-hosted-runner labels config to suppress "unknown label" warnings during workflow linting. This label is used across all workflows after the Blacksmith migration. * fix(actionlint): adjust indentation for self-hosted runner labels * feat(security): enhance security workflow with CodeQL analysis steps * fix(security): update CodeQL action to version 4 for improved analysis * fix(security): remove duplicate permissions in security workflow * fix(security): revert CodeQL action to v3 for stability The v4 version was causing workflow file validation failures. Reverting to proven v3 version that is working on main branch. * fix(security): remove duplicate permissions causing workflow validation failure The permissions block had duplicate security-events and actions keys, which caused YAML validation errors and prevented workflow execution. Fixes: workflow file validation failures on main branch * fix(security): remove pull_request trigger to reduce costs * fix(security): restore PR trigger but skip codeql on PRs * fix(security): resolve YAML syntax error in security workflow * refactor(security): split CodeQL into dedicated scheduled workflow * fix(security): update workflow name to Rust Package Security Audit * fix(codeql): remove push trigger, keep schedule and on-demand only * feat(codeql): add CodeQL configuration file to ignore specific paths * Potential fix for code scanning alert no. 39: Hard-coded cryptographic value Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(ci): resolve auto-response workflow merge markers * fix(build): restore ChannelMessage reply_target usage * ci(workflows): run workflow sanity on workflow pushes for all branches * ci(workflows): rename auto-response workflow to PR Auto Responder * ci(workflows): require owner approval for workflow file changes * ci: add lint-first PR feedback gate * ci(workflows): split label policy checks from workflow sanity * ci(workflows): consolidate policy and rust workflow setup * ci: add safe pull request intake sanity checks --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
parent
e85418eda4
commit
107d7b1ac4
2 changed files with 186 additions and 3 deletions
179
.github/workflows/pr-intake-sanity.yml
vendored
Normal file
179
.github/workflows/pr-intake-sanity.yml
vendored
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
name: PR Intake Sanity
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, edited, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: pr-intake-sanity-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
intake:
|
||||
name: Intake Sanity
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Run safe PR intake checks
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) return;
|
||||
|
||||
const marker = "<!-- 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 formatProblems = [];
|
||||
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)) {
|
||||
formatProblems.push(`${file.filename}:patch#${lineNo} contains tab characters`);
|
||||
}
|
||||
if (/[ \t]+$/.test(added)) {
|
||||
formatProblems.push(`${file.filename}:patch#${lineNo} contains trailing whitespace`);
|
||||
}
|
||||
if (/^(<<<<<<<|=======|>>>>>>>)/.test(added)) {
|
||||
formatProblems.push(`${file.filename}:patch#${lineNo} contains merge conflict markers`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workflowFilesChanged = files
|
||||
.map((file) => file.filename)
|
||||
.filter((name) => name.startsWith(".github/workflows/"));
|
||||
|
||||
const failures = [];
|
||||
if (missingSections.length > 0) {
|
||||
failures.push(`Missing required PR template sections: ${missingSections.join(", ")}`);
|
||||
}
|
||||
if (missingFields.length > 0) {
|
||||
failures.push(`Incomplete required PR template fields: ${missingFields.join(", ")}`);
|
||||
}
|
||||
if (formatProblems.length > 0) {
|
||||
failures.push(`Formatting/safety issues in added lines (${formatProblems.length})`);
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find((comment) => (comment.body || "").includes(marker));
|
||||
|
||||
if (failures.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 details = [];
|
||||
if (formatProblems.length > 0) {
|
||||
details.push(...formatProblems.slice(0, 20).map((entry) => `- ${entry}`));
|
||||
if (formatProblems.length > 20) {
|
||||
details.push(`- ...and ${formatProblems.length - 20} more issue(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
"### PR intake checks failed",
|
||||
"",
|
||||
"Fast safe checks ran before full CI and found issues:",
|
||||
...failures.map((entry) => `- ${entry}`),
|
||||
"",
|
||||
"Action items:",
|
||||
"1. Complete the required PR template sections/fields.",
|
||||
"2. Remove tabs, trailing whitespace, and 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 line issues (sample):",
|
||||
...(details.length > 0 ? details : ["- 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,
|
||||
});
|
||||
}
|
||||
|
||||
core.setFailed("PR intake sanity checks failed. See sticky comment for details.");
|
||||
Loading…
Add table
Add a link
Reference in a new issue