From 6f36dca481922c92eb21f704f3bbb652c4639ec3 Mon Sep 17 00:00:00 2001 From: Will Sarg <12886992+willsarg@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:20:08 -0500 Subject: [PATCH] ci: add lint-first PR feedback gate (#556) * 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 --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 114 ++++++++++++++++++++++++++++++++++++--- docs/ci-map.md | 1 + 2 files changed, 108 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93cc500..e377d15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: } >> "$GITHUB_OUTPUT" lint: - name: Format & Lint + name: Lint Gate (Format + Clippy) needs: [changes] if: needs.changes.outputs.rust_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-2404 @@ -146,7 +146,7 @@ jobs: run: ./scripts/ci/rust_quality_gate.sh lint-strict-delta: - name: Lint Strict Delta + name: Lint Gate (Strict Delta) needs: [changes] if: needs.changes.outputs.rust_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-2404 @@ -167,8 +167,8 @@ jobs: test: name: Test - needs: [changes] - if: needs.changes.outputs.rust_changed == 'true' + needs: [changes, lint, lint-strict-delta] + if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success' && needs.lint-strict-delta.result == 'success' runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 30 steps: @@ -182,8 +182,8 @@ jobs: build: name: Build (Smoke) - needs: [changes] - if: needs.changes.outputs.rust_changed == 'true' + needs: [changes, lint, lint-strict-delta] + if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success' && needs.lint-strict-delta.result == 'success' runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 20 @@ -269,6 +269,106 @@ jobs: if: steps.collect_links.outputs.count == '0' run: echo "No added links in changed docs lines. Link check skipped." + lint-feedback: + name: Lint Feedback + if: github.event_name == 'pull_request' + needs: [changes, lint, lint-strict-delta, docs-quality] + runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: write + issues: write + steps: + - name: Post actionable lint failure summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + RUST_CHANGED: ${{ needs.changes.outputs.rust_changed }} + DOCS_CHANGED: ${{ needs.changes.outputs.docs_changed }} + LINT_RESULT: ${{ needs.lint.result }} + LINT_DELTA_RESULT: ${{ needs.lint-strict-delta.result }} + DOCS_RESULT: ${{ needs.docs-quality.result }} + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issueNumber = context.payload.pull_request?.number; + if (!issueNumber) return; + + const marker = ""; + const rustChanged = process.env.RUST_CHANGED === "true"; + const docsChanged = process.env.DOCS_CHANGED === "true"; + const lintResult = process.env.LINT_RESULT || "skipped"; + const lintDeltaResult = process.env.LINT_DELTA_RESULT || "skipped"; + const docsResult = process.env.DOCS_RESULT || "skipped"; + + const failures = []; + if (rustChanged && !["success", "skipped"].includes(lintResult)) { + failures.push("`Lint Gate (Format + Clippy)` failed."); + } + if (rustChanged && !["success", "skipped"].includes(lintDeltaResult)) { + failures.push("`Lint Gate (Strict Delta)` failed."); + } + if (docsChanged && !["success", "skipped"].includes(docsResult)) { + failures.push("`Docs Quality` failed."); + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issueNumber, + 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("No lint/docs gate failures. No feedback comment required."); + return; + } + + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + const body = [ + marker, + "### CI lint feedback", + "", + "This PR failed one or more fast lint/documentation gates:", + "", + ...failures.map((item) => `- ${item}`), + "", + "Open the failing logs in this run:", + `- ${runUrl}`, + "", + "Local fix commands:", + "- `./scripts/ci/rust_quality_gate.sh`", + "- `./scripts/ci/rust_strict_delta_gate.sh`", + "- `./scripts/ci/docs_quality_gate.sh`", + "", + "After fixes, push a new commit and CI will re-run automatically.", + ].join("\n"); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + }); + } + workflow-owner-approval: name: Workflow Owner Approval needs: [changes] @@ -356,7 +456,7 @@ jobs: ci-required: name: CI Required Gate if: always() - needs: [changes, lint, lint-strict-delta, test, build, docs-only, non-rust, docs-quality, workflow-owner-approval] + needs: [changes, lint, lint-strict-delta, test, build, docs-only, non-rust, docs-quality, lint-feedback, workflow-owner-approval] runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Enforce required status diff --git a/docs/ci-map.md b/docs/ci-map.md index bdd471b..e642d36 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -11,6 +11,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u - `.github/workflows/ci.yml` (`CI`) - Purpose: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate on changed Rust lines, `test`, release build smoke) + docs quality checks when docs change (`markdownlint` blocks only issues on changed lines; link check scans only links added on changed lines) - Additional behavior: PRs that change `.github/workflows/**` require at least one approving review from a login in `WORKFLOW_OWNER_LOGINS` (repository variable fallback: `theonlyhennygod,willsarg`) + - Additional behavior: lint gates run before `test`/`build`; when lint/docs gates fail on PRs, CI posts an actionable feedback comment with failing gate names and local fix commands - Merge gate: `CI Required Gate` - `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`) - Purpose: lint GitHub workflow files (`actionlint`, tab checks)