* 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 * ci(security): switch audit to pinned rustsec audit-check * fix(providers): clarify reliable failure entries for custom providers * ci(pr-intake): make template/format checks advisory Keep PR Intake Sanity non-blocking for template completeness and formatting findings, while still failing on dangerous merge-conflict markers in added lines. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
206 lines
8.2 KiB
YAML
206 lines
8.2 KiB
YAML
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 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) => (comment.body || "").includes(marker));
|
|
|
|
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.");
|