chore: merge devsecops into main (#546)

* 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

---------

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 10:10:14 -05:00 committed by GitHub
parent bb641d28c2
commit 500e6bd0ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 8 deletions

2
.github/CODEOWNERS vendored
View file

@ -10,7 +10,7 @@
/Cargo.lock @theonlyhennygod /Cargo.lock @theonlyhennygod
# CI # CI
/.github/workflows/** @willsarg /.github/workflows/** @theonlyhennygod @willsarg
/.github/codeql/** @willsarg /.github/codeql/** @willsarg
/.github/dependabot.yml @willsarg /.github/dependabot.yml @willsarg

View file

@ -1,4 +1,4 @@
name: Auto Response name: PR Auto Responder
on: on:
issues: issues:

View file

@ -24,6 +24,7 @@ jobs:
docs_only: ${{ steps.scope.outputs.docs_only }} docs_only: ${{ steps.scope.outputs.docs_only }}
docs_changed: ${{ steps.scope.outputs.docs_changed }} docs_changed: ${{ steps.scope.outputs.docs_changed }}
rust_changed: ${{ steps.scope.outputs.rust_changed }} rust_changed: ${{ steps.scope.outputs.rust_changed }}
workflow_changed: ${{ steps.scope.outputs.workflow_changed }}
docs_files: ${{ steps.scope.outputs.docs_files }} docs_files: ${{ steps.scope.outputs.docs_files }}
base_sha: ${{ steps.scope.outputs.base_sha }} base_sha: ${{ steps.scope.outputs.base_sha }}
steps: steps:
@ -55,6 +56,7 @@ jobs:
echo "docs_only=false" echo "docs_only=false"
echo "docs_changed=false" echo "docs_changed=false"
echo "rust_changed=true" echo "rust_changed=true"
echo "workflow_changed=false"
echo "base_sha=" echo "base_sha="
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
write_empty_docs_files write_empty_docs_files
@ -67,6 +69,7 @@ jobs:
echo "docs_only=false" echo "docs_only=false"
echo "docs_changed=false" echo "docs_changed=false"
echo "rust_changed=false" echo "rust_changed=false"
echo "workflow_changed=false"
echo "base_sha=$BASE" echo "base_sha=$BASE"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
write_empty_docs_files write_empty_docs_files
@ -76,10 +79,15 @@ jobs:
docs_only=true docs_only=true
docs_changed=false docs_changed=false
rust_changed=false rust_changed=false
workflow_changed=false
docs_files=() docs_files=()
while IFS= read -r file; do while IFS= read -r file; do
[ -z "$file" ] && continue [ -z "$file" ] && continue
if [[ "$file" == .github/workflows/* ]]; then
workflow_changed=true
fi
if [[ "$file" == docs/* ]] \ if [[ "$file" == docs/* ]] \
|| [[ "$file" == *.md ]] \ || [[ "$file" == *.md ]] \
|| [[ "$file" == *.mdx ]] \ || [[ "$file" == *.mdx ]] \
@ -112,6 +120,7 @@ jobs:
echo "docs_only=$docs_only" echo "docs_only=$docs_only"
echo "docs_changed=$docs_changed" echo "docs_changed=$docs_changed"
echo "rust_changed=$rust_changed" echo "rust_changed=$rust_changed"
echo "workflow_changed=$workflow_changed"
echo "base_sha=$BASE" echo "base_sha=$BASE"
echo "docs_files<<EOF" echo "docs_files<<EOF"
printf '%s\n' "${docs_files[@]}" printf '%s\n' "${docs_files[@]}"
@ -260,10 +269,94 @@ 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."
workflow-owner-approval:
name: Workflow Owner Approval
needs: [changes]
if: github.event_name == 'pull_request' && needs.changes.outputs.workflow_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
steps:
- name: Require owner approval for workflow file changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_OWNER_LOGINS: ${{ vars.WORKFLOW_OWNER_LOGINS || 'theonlyhennygod,willsarg' }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.setFailed("Missing pull_request context.");
return;
}
const ownerAllowlist = (process.env.WORKFLOW_OWNER_LOGINS || "")
.split(",")
.map((login) => login.trim().toLowerCase())
.filter(Boolean);
if (ownerAllowlist.length === 0) {
core.setFailed("WORKFLOW_OWNER_LOGINS is empty. Set a repository variable or use a fallback value.");
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const workflowFiles = files
.map((file) => file.filename)
.filter((name) => name.startsWith(".github/workflows/"));
if (workflowFiles.length === 0) {
core.info("No workflow files changed in this PR.");
return;
}
core.info(`Workflow files changed:\n- ${workflowFiles.join("\n- ")}`);
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const latestReviewByUser = new Map();
for (const review of reviews) {
const login = review.user?.login;
if (!login) continue;
latestReviewByUser.set(login.toLowerCase(), review.state);
}
const approvedUsers = [...latestReviewByUser.entries()]
.filter(([, state]) => state === "APPROVED")
.map(([login]) => login);
if (approvedUsers.length === 0) {
core.setFailed("Workflow files changed but no approving review is present.");
return;
}
const ownerApprover = approvedUsers.find((login) => ownerAllowlist.includes(login));
if (!ownerApprover) {
core.setFailed(
`Workflow files changed. Approvals found (${approvedUsers.join(", ")}), but none match WORKFLOW_OWNER_LOGINS.`,
);
return;
}
core.info(`Workflow owner approval present: @${ownerApprover}`);
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] needs: [changes, lint, lint-strict-delta, test, build, docs-only, non-rust, docs-quality, 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
@ -273,10 +366,17 @@ jobs:
docs_changed="${{ needs.changes.outputs.docs_changed }}" docs_changed="${{ needs.changes.outputs.docs_changed }}"
rust_changed="${{ needs.changes.outputs.rust_changed }}" rust_changed="${{ needs.changes.outputs.rust_changed }}"
workflow_changed="${{ needs.changes.outputs.workflow_changed }}"
docs_result="${{ needs.docs-quality.result }}" docs_result="${{ needs.docs-quality.result }}"
workflow_owner_result="${{ needs.workflow-owner-approval.result }}"
if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then
echo "docs=${docs_result}" echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_result}"
if [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Docs-only change touched markdown docs, but docs-quality did not pass." echo "Docs-only change touched markdown docs, but docs-quality did not pass."
exit 1 exit 1
@ -288,6 +388,11 @@ jobs:
if [ "$rust_changed" != "true" ]; then if [ "$rust_changed" != "true" ]; then
echo "rust_changed=false (non-rust fast path)" echo "rust_changed=false (non-rust fast path)"
echo "docs=${docs_result}" echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_result}"
if [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Docs changed but docs-quality did not pass." echo "Docs changed but docs-quality did not pass."
exit 1 exit 1
@ -306,12 +411,18 @@ jobs:
echo "test=${test_result}" echo "test=${test_result}"
echo "build=${build_result}" echo "build=${build_result}"
echo "docs=${docs_result}" echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_result}"
if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then
echo "Required CI jobs did not pass." echo "Required CI jobs did not pass."
exit 1 exit 1
fi fi
if [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Docs changed but docs-quality did not pass." echo "Docs changed but docs-quality did not pass."
exit 1 exit 1

View file

@ -7,7 +7,6 @@ on:
- ".github/*.yml" - ".github/*.yml"
- ".github/*.yaml" - ".github/*.yaml"
push: push:
branches: [main]
paths: paths:
- ".github/workflows/**" - ".github/workflows/**"
- ".github/*.yml" - ".github/*.yml"

View file

@ -10,6 +10,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`)
- 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)
@ -39,7 +40,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- Additional behavior: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection - Additional behavior: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection
- High-risk heuristic paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**` - High-risk heuristic paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`
- Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation - Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation
- `.github/workflows/auto-response.yml` (`Auto Response`) - `.github/workflows/auto-response.yml` (`PR Auto Responder`)
- Purpose: first-time contributor onboarding + label-driven response routing (`r:support`, `r:needs-repro`, etc.) - Purpose: first-time contributor onboarding + label-driven response routing (`r:support`, `r:needs-repro`, etc.)
- Additional behavior: applies contributor tiers on issues by merged PR count (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50), matching PR tier thresholds exactly - Additional behavior: applies contributor tiers on issues by merged PR count (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50), matching PR tier thresholds exactly
- Additional behavior: contributor-tier labels are treated as automation-managed (manual add/remove on PR/issue is auto-corrected) - Additional behavior: contributor-tier labels are treated as automation-managed (manual add/remove on PR/issue is auto-corrected)
@ -59,7 +60,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- `Security Audit`: push to `main`, PRs to `main`, weekly schedule - `Security Audit`: push to `main`, PRs to `main`, weekly schedule
- `Workflow Sanity`: PR/push when `.github/workflows/**`, `.github/*.yml`, or `.github/*.yaml` change - `Workflow Sanity`: PR/push when `.github/workflows/**`, `.github/*.yml`, or `.github/*.yaml` change
- `PR Labeler`: `pull_request_target` lifecycle events - `PR Labeler`: `pull_request_target` lifecycle events
- `Auto Response`: issue opened/labeled, `pull_request_target` opened/labeled - `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
- `Stale`: daily schedule, manual dispatch - `Stale`: daily schedule, manual dispatch
- `Dependabot`: weekly dependency maintenance windows - `Dependabot`: weekly dependency maintenance windows
- `PR Hygiene`: every 12 hours schedule, manual dispatch - `PR Hygiene`: every 12 hours schedule, manual dispatch

View file

@ -41,6 +41,7 @@ Maintain these branch protection rules on `main`:
- Require check `CI Required Gate`. - Require check `CI Required Gate`.
- Require pull request reviews before merge. - Require pull request reviews before merge.
- Require CODEOWNERS review for protected paths. - Require CODEOWNERS review for protected paths.
- For `.github/workflows/**`, require owner approval via `CI Required Gate` (`WORKFLOW_OWNER_LOGINS`) and keep branch/ruleset bypass limited to org owners.
- Dismiss stale approvals when new commits are pushed. - Dismiss stale approvals when new commits are pushed.
- Restrict force-push on protected branches. - Restrict force-push on protected branches.
@ -55,7 +56,7 @@ Maintain these branch protection rules on `main`:
- Maintainers can run `PR Labeler` manually (`workflow_dispatch`) in `audit` mode for drift visibility or `repair` mode to normalize managed label metadata repository-wide. - Maintainers can run `PR Labeler` manually (`workflow_dispatch`) in `audit` mode for drift visibility or `repair` mode to normalize managed label metadata repository-wide.
- Hovering a label in GitHub shows its auto-managed description (rule/threshold summary). - Hovering a label in GitHub shows its auto-managed description (rule/threshold summary).
- Managed label colors are arranged by display order to create a smooth gradient across long label rows. - Managed label colors are arranged by display order to create a smooth gradient across long label rows.
- `Auto Response` posts first-time guidance, handles label-driven routing for low-signal items, and auto-applies issue contributor tiers using the same thresholds as `PR Labeler` (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50). - `PR Auto Responder` posts first-time guidance, handles label-driven routing for low-signal items, and auto-applies issue contributor tiers using the same thresholds as `PR Labeler` (`trusted` >=5, `experienced` >=10, `principal` >=20, `distinguished` >=50).
### Step B: Validation ### Step B: Validation
@ -159,7 +160,7 @@ Issue triage discipline:
Automation side-effect guards: Automation side-effect guards:
- `Auto Response` deduplicates label-based comments to avoid spam. - `PR Auto Responder` deduplicates label-based comments to avoid spam.
- Automated close routes are limited to issues, not PRs. - Automated close routes are limited to issues, not PRs.
- Maintainers can freeze automated risk recalculation with `risk: manual` when context demands human override. - Maintainers can freeze automated risk recalculation with `risk: manual` when context demands human override.