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>
This commit is contained in:
Will Sarg 2026-02-17 11:20:08 -05:00 committed by GitHub
parent 62eba544e2
commit 6f36dca481
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 7 deletions

View file

@ -128,7 +128,7 @@ jobs:
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
lint: lint:
name: Format & Lint name: Lint Gate (Format + Clippy)
needs: [changes] needs: [changes]
if: needs.changes.outputs.rust_changed == 'true' if: needs.changes.outputs.rust_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404 runs-on: blacksmith-2vcpu-ubuntu-2404
@ -146,7 +146,7 @@ jobs:
run: ./scripts/ci/rust_quality_gate.sh run: ./scripts/ci/rust_quality_gate.sh
lint-strict-delta: lint-strict-delta:
name: Lint Strict Delta name: Lint Gate (Strict Delta)
needs: [changes] needs: [changes]
if: needs.changes.outputs.rust_changed == 'true' if: needs.changes.outputs.rust_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404 runs-on: blacksmith-2vcpu-ubuntu-2404
@ -167,8 +167,8 @@ jobs:
test: test:
name: Test name: Test
needs: [changes] needs: [changes, lint, lint-strict-delta]
if: needs.changes.outputs.rust_changed == 'true' if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success' && needs.lint-strict-delta.result == 'success'
runs-on: blacksmith-2vcpu-ubuntu-2404 runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
@ -182,8 +182,8 @@ jobs:
build: build:
name: Build (Smoke) name: Build (Smoke)
needs: [changes] needs: [changes, lint, lint-strict-delta]
if: needs.changes.outputs.rust_changed == 'true' if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success' && needs.lint-strict-delta.result == 'success'
runs-on: blacksmith-2vcpu-ubuntu-2404 runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 20 timeout-minutes: 20
@ -269,6 +269,106 @@ jobs:
if: steps.collect_links.outputs.count == '0' if: steps.collect_links.outputs.count == '0'
run: echo "No added links in changed docs lines. Link check skipped." 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 = "<!-- ci-lint-feedback -->";
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: workflow-owner-approval:
name: Workflow Owner Approval name: Workflow Owner Approval
needs: [changes] needs: [changes]
@ -356,7 +456,7 @@ jobs:
ci-required: ci-required:
name: CI Required Gate name: CI Required Gate
if: always() 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 runs-on: blacksmith-2vcpu-ubuntu-2404
steps: steps:
- name: Enforce required status - name: Enforce required status

View file

@ -11,6 +11,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- `.github/workflows/ci.yml` (`CI`) - `.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) - 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: 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` - Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`) - `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
- Purpose: lint GitHub workflow files (`actionlint`, tab checks) - Purpose: lint GitHub workflow files (`actionlint`, tab checks)