From 011c503b159fb881961d933d32ea83e3b12e3fcf Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 29 Jun 2026 21:40:03 -0500 Subject: [PATCH 1/2] feat(ci): canonical fleet-CI reusable workflows (Refs #2293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - security.yml: add osv-scanner job (nested reusable) + run_osv/osv_config/ osv_lockfile/cargo_deny_timeout_minutes inputs; bump cargo-deny-action SHA - gate-attestation.yml: fix actor→PR-author bug (Re-run-job flips github.actor to maintainer, re-arming trailer check on bot PRs); drop unused runner input - codeql.yml: new reusable (actions + rust, security-extended, inputs for both) - no-ai-attribution.yml: new reusable (commits + PR body/title, pattern-file input) - dependabot-auto-merge.yml: new reusable (patch + minor-dev; endswith() matcher tolerates caller-job/leaf-job prefix from reusable workflow check names) - release-please.yml: add RELEASE_PLEASE_TOKEN secret + config_file/manifest_file inputs; token falls back to GITHUB_TOKEN so existing callers are unaffected - actionlint.yml: new standalone (validates .github/workflows/** on PR; run: install of actionlint v1.7.7 — no third-party action SHA needed) - dependabot.yml: github-actions ecosystem, weekly — centralises SHA bumps fleet-wide --- .github/dependabot.yml | 10 +++ .github/workflows/actionlint.yml | 34 +++++++++ .github/workflows/codeql.yml | 84 +++++++++++++++++++++ .github/workflows/dependabot-auto-merge.yml | 79 +++++++++++++++++++ .github/workflows/gate-attestation.yml | 33 +++----- .github/workflows/no-ai-attribution.yml | 83 ++++++++++++++++++++ .github/workflows/release-please.yml | 17 ++++- .github/workflows/security.yml | 63 +++++++++------- 8 files changed, 354 insertions(+), 49 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/actionlint.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/no-ai-attribution.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a9aa1ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# WHY: Bump third-party action SHAs pinned inside the reusable workflows once, +# fleet-wide, rather than per-repo (spec §6.4). Weekly cadence; low noise. +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + reviewers: + - forkwright/maintainers diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..f8b7e9d --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,34 @@ +# WHY: Validate reusable workflow YAML before it merges to main, catching errors +# that would silently break all 11 fleet repos simultaneously. +name: actionlint + +on: + pull_request: + paths: + - ".github/workflows/**" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Download actionlint + run: | + ACTIONLINT_VERSION=1.7.7 + curl -sSfL \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | tar -xz actionlint + chmod +x actionlint + - name: Run actionlint + run: ./actionlint -color .github/workflows/*.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e56ad30 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# WHY: CodeQL static analysis (actions + rust) with security-extended queries, +# scheduled + on push to main. Reusable; toolchain + language toggles via inputs. +name: CodeQL (reusable) + +on: + workflow_call: + inputs: + analyze_actions: + type: boolean + default: true + required: false + analyze_rust: + type: boolean + default: true + required: false + rust_toolchain: + type: string + default: "stable" + required: false + queries: + type: string + default: "+security-extended" + required: false + actions_timeout_minutes: + type: number + default: 30 + required: false + rust_timeout_minutes: + type: number + default: 90 + required: false + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze-actions: + name: Analyze (actions) + if: ${{ inputs.analyze_actions }} + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.actions_timeout_minutes }} + permissions: + security-events: write + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: github/codeql-action/init@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + with: + languages: actions + queries: ${{ inputs.queries }} + - uses: github/codeql-action/autobuild@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + - uses: github/codeql-action/analyze@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + with: + category: /language:actions + + analyze-rust: + name: Analyze (rust) + if: ${{ inputs.analyze_rust }} + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.rust_timeout_minutes }} + permissions: + security-events: write + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: ${{ inputs.rust_toolchain }} + - uses: github/codeql-action/init@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + with: + languages: rust + queries: ${{ inputs.queries }} + - uses: github/codeql-action/autobuild@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + - uses: github/codeql-action/analyze@dd677812177e0c29f9c970a6c58d8607ae1bfefd # v4 + with: + category: /language:rust diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..01b3f8e --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,79 @@ +# WHY: auto-merge dependabot patch (+ minor dev-dep) bumps once REAL verification +# checks pass. Required-check names are fleet-invariant because security.yml + +# gate-attestation.yml are canonical. Matching uses endswith() to tolerate the +# "caller-job / reusable-job" check-name prefix introduced by reusable workflows. +name: Dependabot Auto-Merge (reusable) + +on: + workflow_call: {} + +permissions: + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + auto-merge: + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.actor == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Wait for CI checks to pass + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-patch' || + (steps.metadata.outputs.update-type == 'version-update:semver-minor' && + steps.metadata.outputs.dependency-type == 'direct:development') + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + if ! gh pr checks "$PR_URL" --watch --interval 30 --required; then + echo "Required CI checks did not pass; refusing auto-merge." >&2 + exit 1 + fi + failed=0 + # WHY: endswith — reusable-workflow checks are named "caller-job / leaf-job". + require_passed_check() { + local leaf="$1" + local bucket + bucket=$(gh pr checks "$PR_URL" --json name,bucket \ + --jq "[.[] | select(.name | ascii_downcase | endswith(\"$leaf\"))][0].bucket") + if [ -z "$bucket" ] || [ "$bucket" = "null" ]; then + echo "::error::Required verification check ending '${leaf}' was not reported."; failed=1; return + fi + if [ "$bucket" != "pass" ]; then + echo "::error::Required verification check ending '${leaf}' finished in bucket '${bucket}'."; failed=1 + fi + } + require_passed_check "gate-attestation" + require_passed_check "cargo deny" + require_passed_check "cargo audit" + require_passed_check "osv scanner" + [ "$failed" -eq 0 ] || { echo "Real verification checks missing/unsuccessful." >&2; exit 1; } + echo "Required real verification checks passed." + + - name: Auto-merge patch updates + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh pr merge --squash "$PR_URL" + + - name: Auto-merge minor dev dependency updates + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-minor' && + steps.metadata.outputs.dependency-type == 'direct:development' + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh pr merge --squash "$PR_URL" diff --git a/.github/workflows/gate-attestation.yml b/.github/workflows/gate-attestation.yml index 2f538e8..1657cd8 100644 --- a/.github/workflows/gate-attestation.yml +++ b/.github/workflows/gate-attestation.yml @@ -1,16 +1,9 @@ -# WHY: Canonical reusable gate-attestation workflow. Every non-automation PR -# must carry a Gate-Passed trailer proving the local gate ran; trusted bots -# (dependabot, release-please) are waived automatically. +# WHY: Every non-automation PR must carry a Gate-Passed trailer proving the local +# gate ran. Trusted bots are waived. Reusable; pure trailer verification, fully invariant. name: Gate Attestation (reusable) on: - workflow_call: - inputs: - runner: - description: "Runner label (ubuntu-latest or self-hosted)" - type: string - default: "ubuntu-latest" - required: false + workflow_call: {} permissions: contents: read @@ -22,40 +15,38 @@ concurrency: jobs: gate-attestation: name: gate-attestation - runs-on: ${{ inputs.runner }} + runs-on: ubuntu-latest steps: + # WHY: waiver keys off the PR author login, not github.actor — actor flips + # to a maintainer login on "Re-run failed jobs", re-arming the check on bot PRs. - name: Pass trusted automation PRs - if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'release-please[bot]' }} - run: echo "Gate attestation waived for trusted automation actor." + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'release-please[bot]' }} + run: echo "Gate attestation waived for trusted automation PR author ${{ github.event.pull_request.user.login }}." - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'release-please[bot]' }} + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' }} with: fetch-depth: 0 persist-credentials: false - name: Verify Gate-Passed trailer - if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'release-please[bot]' }} + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' }} run: | commits=$(git log --format="%H" "origin/${{ github.base_ref }}..HEAD") if [ -z "$commits" ]; then echo "ERROR: No commits found in PR" exit 1 fi - found=false for sha in $commits; do body=$(git log -1 --format="%b" "$sha") if echo "$body" | grep -q "^Gate-Passed:"; then - version=$(echo "$body" | grep "^Gate-Passed:" | head -1) - echo "Found gate attestation: $version" + echo "Found gate attestation: $(echo "$body" | grep '^Gate-Passed:' | head -1)" found=true break fi done - if [ "$found" = false ]; then echo "ERROR: No Gate-Passed trailer found in any PR commit." - echo "Run the local gate and commit with the trailer." exit 1 fi diff --git a/.github/workflows/no-ai-attribution.yml b/.github/workflows/no-ai-attribution.yml new file mode 100644 index 0000000..c57e8a9 --- /dev/null +++ b/.github/workflows/no-ai-attribution.yml @@ -0,0 +1,83 @@ +# WHY (#4278): generated-output attribution markers can leak via commit messages, +# PR title, and PR body (GitHub squashes the body into the merge commit). Scans all +# three. Pattern source: caller repo's .github/no-ai-attribution-patterns.txt. +name: No AI Attribution (reusable) + +on: + workflow_call: + inputs: + pattern_file: + type: string + default: ".github/no-ai-attribution-patterns.txt" + required: false + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + no-ai-attribution: + name: no-ai-attribution + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Pass trusted automation PRs + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'release-please[bot]' || github.event.pull_request.user.login == 'github-actions[bot]' }} + env: + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + run: echo "Attribution check waived for trusted automation PR author ${PR_AUTHOR}." + + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' && github.event.pull_request.user.login != 'github-actions[bot]' }} + with: + fetch-depth: 0 + persist-credentials: false + + - name: Scan PR body and title for attribution markers + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' && github.event.pull_request.user.login != 'github-actions[bot]' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PATTERN_FILE: ${{ inputs.pattern_file }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if [ ! -s "$PATTERN_FILE" ]; then + echo "::error::Missing attribution pattern source: ${PATTERN_FILE}" + exit 1 + fi + BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body) + TITLE=$(gh pr view "$PR_NUMBER" --json title --jq .title) + fail=0 + if printf '%s' "$BODY" | grep -niE -f "$PATTERN_FILE"; then + echo "::error::PR body contains attribution marker(s) above."; fail=1 + fi + if printf '%s' "$TITLE" | grep -niE -f "$PATTERN_FILE"; then + echo "::error::PR title contains attribution marker(s) above."; fail=1 + fi + [ "$fail" -eq 0 ] || exit 1 + echo "PR body and title clean of attribution markers." + + - name: Scan PR commits for attribution markers + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'release-please[bot]' && github.event.pull_request.user.login != 'github-actions[bot]' }} + env: + PATTERN_FILE: ${{ inputs.pattern_file }} + run: | + set -euo pipefail + if [ ! -s "$PATTERN_FILE" ]; then + echo "::error::Missing attribution pattern source: ${PATTERN_FILE}" + exit 1 + fi + commits=$(git log --format="%H" "origin/${{ github.base_ref }}..HEAD") + fail=0 + for sha in $commits; do + message=$(git log -1 --format="%B" "$sha") + if printf '%s' "$message" | grep -niE -f "$PATTERN_FILE"; then + echo "::error::Commit $sha contains attribution marker(s) above."; fail=1 + fi + done + [ "$fail" -eq 0 ] || { echo "::error::Reword/rebase the offending commit(s)."; exit 1; } + echo "All PR commits clean of attribution markers." diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 1494d33..d74f225 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,6 +5,18 @@ name: Release Please (reusable) on: workflow_call: + inputs: + config_file: + type: string + default: "release-please-config.json" + required: false + manifest_file: + type: string + default: ".release-please-manifest.json" + required: false + secrets: + RELEASE_PLEASE_TOKEN: + required: false outputs: release_created: description: "Whether a release was created" @@ -31,5 +43,6 @@ jobs: - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 id: release with: - config-file: release-please-config.json - manifest-file: .release-please-manifest.json + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + config-file: ${{ inputs.config_file }} + manifest-file: ${{ inputs.manifest_file }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index fb9a193..16e43fc 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,30 +1,43 @@ # WHY: Block merges on known vulnerabilities and license/source/ban violations. -# Runs cargo-audit (RustSec DB) and cargo-deny check (licenses, sources, bans, -# advisories) on every PR and daily against main so newly-published CVEs are -# caught even when no PR is open. See standards/CI.md § Vulnerability scanning. +# cargo-audit (RustSec) + cargo-deny (licenses/sources/bans/advisories) + osv-scanner. +# Reusable across the fleet; per-repo advisory ignores live in deny.toml/osv-scanner.toml. name: Security (reusable) on: workflow_call: inputs: runner: - description: "Runner label (ubuntu-latest or self-hosted)" type: string default: "ubuntu-latest" required: false + cargo_deny_timeout_minutes: + type: number + default: 15 + required: false cargo_audit_timeout_minutes: - description: "Timeout in minutes for cargo-audit (large workspaces may need 30)" type: number default: 15 required: false has_private_deps: - description: "Set to true to configure FLEET_REPO_TOKEN git credentials" + description: "Configure FLEET_REPO_TOKEN git credentials for cross-repo private deps" type: boolean default: false required: false + run_osv: + description: "Run google/osv-scanner (uploads SARIF to code scanning)" + type: boolean + default: true + required: false + osv_config: + type: string + default: "osv-scanner.toml" + required: false + osv_lockfile: + type: string + default: "Cargo.lock" + required: false secrets: FLEET_REPO_TOKEN: - description: "Token for private fleet dependencies (required when has_private_deps=true)" required: false permissions: @@ -39,17 +52,13 @@ env: jobs: cargo-deny: - # WHY: checks licenses, banned crates, source registries, and the RustSec - # advisory DB using deny.toml's ignore list. Any finding fails the job; - # suppressions require a deny.toml entry with a WHY comment. name: cargo deny runs-on: ${{ inputs.runner }} - timeout-minutes: 15 + timeout-minutes: ${{ inputs.cargo_deny_timeout_minutes }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - - name: Configure git credentials for private fleet deps if: ${{ inputs.has_private_deps }} env: @@ -62,28 +71,21 @@ jobs: git config --global credential.helper store printf 'https://forkwright:%s@github.com\n' "${FLEET_REPO_TOKEN}" > ~/.git-credentials chmod 0600 ~/.git-credentials - - - uses: EmbarkStudios/cargo-deny-action@a531616d8ce3b9177443e48a1159bc945a099823 # v2.0.19 + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 with: - # WHY: run every check explicitly so a schema regression in one - # section can't silently disable the others. command: check advisories licenses bans sources arguments: --all-features credentials: ${{ inputs.has_private_deps && format('https://forkwright:{0}@github.com', secrets.FLEET_REPO_TOKEN) || '' }} use-git-cli: ${{ inputs.has_private_deps }} cargo-audit: - # WHY: cargo-deny's advisories check overlaps but uses a different code - # path; running cargo-audit independently protects against bugs or - # config drift disabling one of the two. See standards/CI.md. name: cargo audit runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.cargo_audit_timeout_minutes }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - - name: Configure git credentials for private fleet deps if: ${{ inputs.has_private_deps }} env: @@ -96,7 +98,8 @@ jobs: git config --global credential.helper store printf 'https://forkwright:%s@github.com\n' "${FLEET_REPO_TOKEN}" > ~/.git-credentials chmod 0600 ~/.git-credentials - + # WHY: setup-rust-toolchain auto-reads rust-toolchain.toml, so nightly-pinned + # repos (theatron) get nightly and stable repos get stable with no input. - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: @@ -104,7 +107,15 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit --locked --version ^0.22 - name: cargo audit - # WHY: --deny unmaintained,unsound,yanked escalates those categories - # to errors. Suppressions go through .cargo/audit.toml — no silent - # --ignore flags here. run: cargo audit --deny unmaintained --deny unsound --deny yanked + + osv-scanner: + name: osv scanner + if: ${{ inputs.run_osv }} + permissions: + actions: read + contents: read + security-events: write + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 + with: + scan-args: "--config=${{ inputs.osv_config }} --lockfile=${{ inputs.osv_lockfile }}" From 319a90975faf5aecfeb70990841cc3ada35a3ed4 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 29 Jun 2026 21:42:15 -0500 Subject: [PATCH 2/2] chore(ci): drop non-existent maintainers reviewer from dependabot config --- .github/dependabot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a9aa1ed..9850828 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,5 +6,3 @@ updates: directory: "/" schedule: interval: weekly - reviewers: - - forkwright/maintainers