Skip to content

ci: adopt the fleet reusable CI workflow - #5

Draft
h4x0r wants to merge 5 commits into
mainfrom
ci/adopt-reusable-workflow
Draft

ci: adopt the fleet reusable CI workflow#5
h4x0r wants to merge 5 commits into
mainfrom
ci/adopt-reusable-workflow

Conversation

@h4x0r

@h4x0r h4x0r commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Pilot 3 of 3 — workspace with fuzz (ntfs-core + ntfs-forensic, root
fuzz/). 172 lines become 44.

This one surfaces a real finding. The workflow being replaced runs
cargo llvm-cov --lib with no features; the fleet default is
--workspace --all-features. Running the shared gate locally at the fleet
default surfaces 37 uncovered lines in core/src/vfs.rs — the vfs feature
adapter, which the narrower scope never measured at all. The old job is named
"Coverage (100% lines)" and its number was true for what it measured, and silent
about a whole feature-gated module.

Those 37 lines are left visible on purpose. The two honest ways to clear them are
to test the vfs adapter, or to declare the exclusion explicitly with
coverage-ignore-regex: 'core/src/vfs\.rs' — which is at least reviewable.
Narrowing scope back to --lib would hide the gap again. Scope is part of the
coverage semantics, not a detail.

This check cannot pass yet. The reusable workflow it calls lives at
SecurityRonin/fleet-config/.github/workflows/rust-ci.yml, and that repo
does not exist
— creating it is outward-facing and not yet authorized. The
ci job will fail to resolve the callee until it lands. The workflow is built
and committed locally at /Users/4n6h4x0r/.claude/jobs/6125d941/tmp/fleet-ci/,
pinned here by the SHA it will carry when pushed (c30b7708…, which git
preserves on push since SHAs are content-addressed).

The callee repo must be created PUBLIC. A public caller can only reference
reusable workflows in public repos; all 91 fleet repos are public.

Why

The fleet carries 91 copies of ci.yml in 89 distinct normalized variants
across 49 feature profiles
. Where a shared mechanism exists the fleet
converges completely — release-plz.yml has 87 copies in 2 variants, and
rust-toolchain.toml is uniform at 1.96.0 across all 90. CI drifted because
nothing held it together.

Four gaps the single workflow closes:

Gap Before After
Secret scan 48 of 92 repos had none every repo, always on
SHA-pinned actions 10 of 92 fully pinned; the rest floating every action pinned, tag verified via the GitHub API
permissions: block 22 of 92 declared one — 70 did not contents: read, workflow-wide
Coverage gate 6 implementations, 3 semantics one implementation

Correction. An earlier revision of this PR body said 0 of 91 repos
declared permissions:. That was wrong — the real figure is 22 (all
top-level, none job-level). The scan regex was ^\s*permissions: without
re.MULTILINE, so ^ anchored to the start of the file and matched nothing.
Confirmed by grep -rl --include=ci.yml '^permissions:' (23 files, one a
packaged copy under codec/xpress-huffman/target/package/). The design is
unchanged — hardcoding contents: read is still right and 70 repos lacking it
is still the finding — but "no repo has ever set permissions" is a different
and more alarming claim than the truth.

The pins are sound; one provenance label is fiction

9bdad043e88c75890e36ad3bbc8d27f0090dd609 appears in 56 fleet repos, across 87
workflow files, 292 times
— 276 commented # v2.7.8, 16 commented # v2. It is
a real Swatinem/rust-cache commit dated 2024-05-03 (fix: usage of deprecated version of node, PR #197), but it matches no release tag: v2.7.3 →
23bce251a8cd2ffc3c1075eaa2367cf899916d84, v2.7.5 →
82a92a6e8fbeee089604da2575dc567ae9ddeaab, v2.7.7 →
f0deed1e0edfc6a9be95417288c0e1099b1eeec3, v2.7.8 →
9d47c6ad4b02e050fd481d890b2ea34778fd09d6, v2.8.0 →
98c8021b550208e191a6a3145459bfc9fb29c4c0.

This is a traceability defect, not a vulnerability. A SHA is immutable, so the
security control is doing its job — the pinned bytes cannot change under anyone.
What fails is provenance: 56 repos run untagged mid-tree code while the comment
beside it asserts a release, so an audit answering "are we on released
versions?"
from those comments gets a wrong answer, and Renovate's
digest-pinning may not map SHA→version cleanly either.

orchestration/issen and parser/browser-forensic pin both SHAs in different
workflows, so they are internally inconsistent about which rust-cache they run.

Every SHA in the shared workflow was resolved with
gh api repos/<owner>/<repo>/commits/<tag> --jq .sha; rust-cache is
c19371144df3bb44fab255c43d04cbc2ab54d1c4, genuinely v2.9.1.

The coverage gate, and why these semantics

The fleet's gate existed in three forms that disagree about what "100%" asserts:

  1. Per-line, cov:unreachable-aware (36 repos) — walks every DA:<n>,0
    record, reads the source line, exempts annotated ones, names what failed.
  2. Aggregate floor --fail-under-lines N (10 repos, N ∈ {85, 89, 90, 92, 95,
    96, 97}).
  3. Naive grep -qE '^DA:[0-9]+,0$' — strict 100%, no exemption at all.

The shared workflow implements (1). It is what the fleet constitution already
requires, and it is the only form that preserves defence in depth: under a floor,
an unreachable guard is indistinguishable from an untested one; under the naive
grep, the only way to go green is to delete the guard. Both pressure the author
toward removing exactly the code that keeps a parser safe on hostile input. A
floor also hides which lines rot — at 85%, 15% of the crate can decay silently,
and every floor in the fleet carried a "ratchet it up later" comment that had
never been ratcheted.

Repos not yet at per-line coverage set coverage-gate: floor, which renders in
the checks UI as Coverage (FLOOR N% — migration debt, not the fleet gate) and
emits a warning. The debt stays visible instead of looking like a passing gate.

Design points worth reviewing

  • MSRV is derived, not restated. The job reads the lowest rust-version
    across workspace members from cargo metadata. Restating it in CI is how a
    repo ends up verifying 1.85 while promising 1.81 downstream. Verified against
    browser-forensic, whose members declare both 1.80 and 1.85 — it derives 1.80.
    If no member declares one, the job fails loudly rather than guessing a floor it
    would then claim to have verified.
  • Fuzz is discovered, not configured. Any directory named fuzz containing a
    Cargo.toml, at any depth. 79 fleet repos have a fuzz crate but only 70 keep
    it at fuzz/; the other 9 use core/fuzz, forensic/fuzz, or
    crates/<member>/fuzz. A root-only check would skip those silently, and a
    fuzz job that passes because it found nothing is worse than none at all.
    Discovery was run against all 92 repos: exact match, zero mismatches.
  • A repo-local .gitleaks.toml is honoured — 60 repos ship one, allowlisting
    forensic fixtures that look like credentials.
  • Repo-specific jobs stay in the caller (72 of 91 repos have at least one).
    docs (34 repos) and geiger (19) are the obvious next consolidation
    candidates, but they do not block adoption.

Validation

  • Reusable workflow and all caller stubs parse as YAML; the gate's embedded
    Python compiles.
  • The coverage gate was exercised against a synthetic lcov covering every branch
    (covered / uncovered / annotated exemption / bare marker / delimiter artifact /
    unreadable source) — correct exit code and classification in each.
  • It was then run against real cargo llvm-cov output (ntfs-forensic: 34
    files, 12,906 DA records, 97 zero-hit). That caught a genuine bug my own
    fixture had missed: the marker regex was anchored to // and so dropped the
    trailing block-comment form back into the failure set. Fixed and re-verified.
  • The workflow itself has never executed. No CI run has exercised it end to
    end, because the callee does not exist.

Action pins are behaviour-neutral

Every action is pinned to exactly what the fleet runs today, so this changes
one variable — where CI is defined — not two:

Action Pin Version
actions/checkout 11bd71901bbe5b1630ceea73d27597364c9af683 v4.2.2
Swatinem/rust-cache 9d47c6ad4b02e050fd481d890b2ea34778fd09d6 v2.7.8
taiki-e/install-action 59012be0884e296ca2da49b530610e72c49039ad v2.81.6

An earlier revision pinned checkout at v7.0.1 and rust-cache at v2.9.1. Both
were reverted — a version bump hidden inside a 91-repo consolidation is
unreviewable, and when CI breaks nobody can tell whether the migration or the
upgrade caused it. rust-cache is now the genuine v2.7.8, the version 56
repos already believe they run, so this fixes the provenance label without
changing behaviour. Upgrades become a separate reviewable PR against one file.

cargo deny argument order

--config belongs to the check subcommand; --all-features, --exclude
and --target are global options that precede it. Both forms were run
against cargo-deny 0.19.0:

$ cargo deny --config deny.toml check all
error: unexpected argument '--config' found
  tip: 'check --config' exists

$ cargo deny check --config deny.toml all
advisories ok, bans ok, licenses ok, sources ok

The workflow emits cargo deny [globals] check --config <shared> all. A
deny-args input carries the three genuinely per-repo settings a shared config
cannot hold: --all-features (4 repos — kept out of the shared file because at
fleet scope it pulls optional GPL-2.0/GPL-3.0 crates like lzo1x and noalloc
into the graph), --exclude for *-fuzz members (10 repos), and --target
(winevt-forensic only, which per cargo deny --help "overrides the top-level
targets = [] configuration value").

h4x0r and others added 5 commits August 2, 2026 02:41
Replaces this repo's hand-maintained ci.yml with a call to the shared
workflow_call workflow. Repo-specific jobs stay here alongside the call.

The fleet carried 91 copies of ci.yml in 89 distinct normalized variants
across 49 feature profiles. The per-repo copies left four gaps this closes:

- Secret scan: 48 of 91 repos had none.
- SHA pinning: 10 of 91 were fully pinned; 81 carried a floating tag.
- permissions: 0 of 91 declared a block.
- Coverage: 6 implementations with 3 incompatible semantics.

The shared coverage gate is per-line and honours cov:unreachable, because
that is the only semantics that preserves defence in depth. An aggregate
floor has no per-line hook, so it cannot honour an exemption and never names
the rotting lines; a naive DA:n,0 grep has no exemption at all, so the only
way to green is to delete the guard. Both pressure the author toward
removing exactly the code that keeps a parser safe on hostile input.

The callee does not exist yet -- creating the repo that hosts it is not yet
authorized -- so the `ci` check will fail to resolve until it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous pin predates the rust-cache provenance correction, so a
reviewer following the SHA would read the wrong figures (the finding is 56
repos / 87 files / 292 occurrences, not "~36 repos"). Workflow bytes are
unchanged; only the README differs between the two commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts actions/checkout v7.0.1 -> v4.2.2 and rust-cache v2.9.1 -> the
genuine v2.7.8 in this repo's own jobs, matching the shared workflow. Every
action now pins exactly what the fleet runs today, so adopting the reusable
workflow changes one variable -- where CI is defined -- and not two.

Repins fleet-config to the commit that fixes the cargo-deny argument order
(--config belongs to the `check` subcommand; the previous form would have
failed at runtime) and corrects the permissions figure to 22 of 92 repos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous fleet-config commit still had one checkout on v7.0.1 -- the
deny job's `Fetch shared deny.toml` step, whose `uses:` is indented under a
`- name:` key and so escaped a whole-file replace. All 25 action refs in the
callee are now pinned and each SHA re-verified against its commented tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The caller stub referenced
SecurityRonin/fleet-config/.github/workflows/rust-ci.yml, but the reusable
workflow lives in SecurityRonin/fleet-ci. fleet-config holds the shared
deny.toml and the legal templates and has no workflows directory, so the
reference could never have resolved.

Also repins from 29f8011 to f49dff5, which carries the fix for a defect the
workflow had inherited from the repos it consolidates: a bare "cargo fetch"
re-resolves and rewrites Cargo.lock in the runner, so the "cargo vet --locked"
on the following line validated a lock CI had just generated for itself rather
than the committed bytes. 80 of 81 fleet repos share that shape. Consolidating
the workflow as previously pinned would have propagated it to all 91 repos in a
single shared file everyone assumes was reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant