From f1e9347759207cce052eaa03cf236d00a932d29f Mon Sep 17 00:00:00 2001 From: forkwright Date: Wed, 26 Aug 2026 10:38:22 -0500 Subject: [PATCH] feat(hybrid-gate): let a repo with no Cargo manifest use the fleet gate dioptron and mneme are design-phase repos with no Cargo.toml anywhere. Their gate is a bare Gate-Passed trailer check, and the trailer is emitted only by `kanon gate --tier full`, which needs a build box the fleet does not declare. No PR in either repo can pass. They are not strict; they are unsatisfiable. An earlier attempt to point them here (dioptron#42, mneme#3) was closed with the reasoning that "the trailer-only passthrough (stamped locally via the nobuild gate tier) remains the correct gate class for no-code repos". That premise is false: `kanon gate --tier nobuild` runs only non-compiling stages and its own help says it "never emits the trailer". The nobuild tier cannot mint what those gates require, so the passthrough was never satisfiable either. Those PRs also failed at `cargo metadata` -- which is the concrete thing this change fixes. No new inputs. Every non-Rust behaviour comes from widening two mechanisms that already exist, because a caller-declared `rust_project: false` would be a second fact about the repo that drifts from reality the moment a Cargo.toml lands: - The existing Cargo-manifest detection moves ahead of toolchain setup so its output can gate more than the build cache. Toolchain install, cargo-nextest install and the Rust cache now all skip when there is no manifest -- the same condition the cache step already used. - fmt, clippy and nextest skip on an empty command, mirroring doctest_cmd's existing skip-on-empty exactly. `check` stays unconditional on purpose. It is the one step a non-Rust caller must wire something into, so a full-gate-build can never be configured to verify nothing. Non-regression, verified structurally rather than asserted: workflow_call input keys, defaults, types and required flags are byte-identical; job ids and job NAMES are byte-identical, so no check-run context is renamed and no repo's branch protection is stranded; the full-gate-build step-name set is unchanged and only reordered. All 11 confirmed callers have a root Cargo.toml and non-empty fmt/clippy/nextest commands, so every new condition evaluates true for them and every gated step still always runs. WARNING for a docs-phase caller: docs_only_exemption defaults true and its patterns match essentially all of such a repo's content, so leaving it at the default would exempt nearly every PR forever. A required check that samples rather than proves is worse than none, because it is trusted. Such a caller must set it false and wire a real command into check_cmd. This deliberately does NOT install the kanon binary. kanon ships no release asset and the repo is private, so the only precedent (epistole's kanon-lint) needs two private checkouts, FLEET_REPO_TOKEN and a ~10-minute cargo install, and is non-required for exactly that cost. That cost does not belong on the one required check; a repo wanting kanon-lint coverage adds a separate non-required job beside its gate call. --- .github/workflows/hybrid-gate.yml | 117 +++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/.github/workflows/hybrid-gate.yml b/.github/workflows/hybrid-gate.yml index 0d86003..b2a75be 100644 --- a/.github/workflows/hybrid-gate.yml +++ b/.github/workflows/hybrid-gate.yml @@ -73,22 +73,37 @@ on: required: false default: "" fmt_cmd: - description: The exact fmt-check command. + description: >- + The exact fmt-check command. Set to the empty string to skip this + step entirely (e.g. a non-Rust caller with no fmt-equivalent + stage) — mirrors doctest_cmd's existing skip-on-empty behavior. type: string required: false default: "cargo fmt --all -- --check" check_cmd: - description: The exact compile-check command. + description: >- + The exact compile-check command. This is the one check step with + no skip: it always runs, so a non-Rust caller (no Cargo.toml — + see the Detect-a-Cargo-manifest step below) wires its real check + pipeline here instead of leaving it empty. Chain multiple + commands with `&&` (the sphragis/hamma feature-matrix pattern) + for more than one stage. Deliberately excludes anything needing + the `kanon` binary — see the WHY above the fmt/check/clippy/ + nextest steps for that boundary. type: string required: false default: "cargo check --workspace --all-targets" clippy_cmd: - description: The exact clippy command. + description: >- + The exact clippy command. Set to the empty string to skip this + step entirely, same as fmt_cmd. type: string required: false default: "cargo clippy --workspace --all-targets -- -D warnings" nextest_cmd: - description: The exact nextest invocation. + description: >- + The exact nextest invocation. Set to the empty string to skip + this step entirely, same as fmt_cmd. type: string required: false default: "cargo nextest run --workspace" @@ -147,6 +162,13 @@ on: e.g. AGENTS.md or .github/CODEOWNERS.md, already matches the first pattern). Default true. Does not affect ai_attribution_check, which still runs on docs-only PRs. + + WARNING for a docs-phase caller (no Cargo.toml — see + check_cmd/fmt_cmd/clippy_cmd/nextest_cmd above): the default + patterns match nearly all of that repo's content, so leaving this + at true exempts essentially every PR from ever running check_cmd — + a gate that verifies nothing is worse than no gate. Set false so + check_cmd's real, non-Rust checks actually run. type: boolean required: false default: true @@ -302,33 +324,59 @@ jobs: echo "free disk after reclaim:" df -h / | tail -1 + # WHY this step now runs first: it gates three consumers below (the + # toolchain install, the nextest-CLI install, and rust-cache), not + # just rust-cache alone — a step's `if:` can only read an earlier + # step's output, so detection has to precede all three, not just the + # one it originally guarded. + # + # WHY the gate widened past rust-cache: rust-cache was ALREADY guarded + # because it shells out to `cargo metadata`, which fails outright on a + # repo with no Cargo.toml (`error: could not find Cargo.toml`) — but + # the toolchain install and the nextest-CLI install ran unconditionally + # regardless, which is pure waste for a repo with nothing to build and, + # worse, is not even sufficient: fmt/check/clippy/nextest below still + # invoke `cargo`, and a repo with no Cargo.toml has no cargo to invoke. + # This is not hypothetical — dioptron#42 and mneme#3 (both design-phase + # repos, no root Cargo.toml) tried adopting this reusable as-is and + # were closed unmerged: "the run failed at `cargo metadata`, not on + # content." Widening this gate to the toolchain and nextest-CLI steps, + # plus making fmt_cmd/clippy_cmd/nextest_cmd individually skippable + # (same pattern doctest_cmd already used), is what that failure needed. + # + # WHY still derived from Cargo.toml presence, not a new declared input: + # a caller stating "I am not a Rust repo" is a second fact that can + # drift from the first the moment a Cargo.toml lands (e.g. at a + # design-phase repo's promotion to real code) and nobody flips the + # input back — the file on disk is the one fact that cannot go stale. + - name: Detect a Cargo manifest + id: cargo-manifest + run: | + if [ -f Cargo.toml ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "no Cargo.toml at the repo root; skipping the Rust toolchain, cargo-nextest install, and build cache" + fi + # WHY: empty toolchain input is deliberate — actions-rust-lang/setup-rust-toolchain # auto-detects from the caller repo's own rust-toolchain.toml when no # explicit `toolchain:` override is given, keeping the toolchain pin # single-sourced in the file each repo already carries (never # duplicated into this workflow's inputs unless a repo has no such # file). + # + # WHY guarded: see the WHY above Detect a Cargo manifest — a repo with + # no Cargo.toml has no toolchain file for this action to detect either, + # and every consumer of a toolchain (fmt/check/clippy/nextest below) + # is cargo-shaped, so a non-Rust caller has no use for one. - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + if: steps.cargo-manifest.outputs.present == 'true' with: toolchain: ${{ inputs.rust_toolchain }} components: rustfmt, clippy cache: false - # WHY guarded: rust-cache shells out to `cargo metadata`, which fails outright on a - # repo with no Cargo.toml -- `error: could not find Cargo.toml`. Consumers of this gate - # are not all Rust repos: several pass `true` for fmt/clippy/nextest and use check_cmd - # for their own non-Rust checks, which works fine until this step runs unasked and - # fails the whole job for a cache it could never have populated. - - name: Detect a Cargo manifest - id: cargo-manifest - run: | - if [ -f Cargo.toml ]; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - echo "no Cargo.toml at the repo root; skipping the Rust build cache" - fi - - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2 if: steps.cargo-manifest.outputs.present == 'true' with: @@ -371,7 +419,12 @@ jobs: printf 'https://forkwright:%s@github.com\n' "${FLEET_REPO_TOKEN}" > ~/.git-credentials chmod 0600 ~/.git-credentials + # WHY guarded: same rationale as the toolchain-install step above — a + # repo with no Cargo.toml has no nextest_cmd invocation to serve (that + # step is itself skipped below when nextest_cmd is empty), so + # installing the CLI is pure cost with nothing that will call it. - name: Install cargo-nextest + if: steps.cargo-manifest.outputs.present == 'true' uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 with: tool: nextest @@ -380,16 +433,42 @@ jobs: # (fmt, check, clippy, nextest[, doctest]) — a Gate-Passed trailer and a # green full-gate-build must mean the same thing. The caller owns the # exact command strings; do not hardcode a repo's flags/features here. + # + # WHY fmt/clippy/nextest are individually skippable (empty string) but + # check is not: doctest_cmd already used empty-to-skip; fmt_cmd, + # clippy_cmd and nextest_cmd now match it, so a non-Rust caller with no + # fmt/clippy/nextest-equivalent stage can leave them empty rather than + # supplying a no-op command. check_cmd keeps no such escape — it is the + # one step guaranteed to run, so a caller cannot wire a full-gate-build + # that verifies nothing (the exact failure objection this reusable + # exists to prevent: see the WARNING on `docs_only_exemption` above, + # and kanon#2708, where two docs-phase repos' local gates attested only + # their own CI config and README and nothing else). + # + # WHY a `kanon lint`-dependent check does not belong in check_cmd for a + # docs-phase caller: the `kanon` binary is unpublished (no release + # artifacts — forkwright/kanon ships source only) and the repo housing + # it is private, so installing it here means two extra private + # checkouts, FLEET_REPO_TOKEN, and a `cargo install` on every PR — the + # exact cost epistole's own ci.yml comment cites for keeping its + # kanon-lint job deliberately NON-required (job-level `if`, skips + # clean rather than reporting a false green). check_cmd is the + # REQUIRED check; that cost does not belong on it. A caller that wants + # kanon-lint coverage adds it the way epistole does — a separate, + # non-required job beside the `gate:` call, not inside it. - name: fmt + if: inputs.fmt_cmd != '' run: ${{ inputs.fmt_cmd }} - name: check run: ${{ inputs.check_cmd }} - name: clippy + if: inputs.clippy_cmd != '' run: ${{ inputs.clippy_cmd }} - name: nextest + if: inputs.nextest_cmd != '' run: ${{ inputs.nextest_cmd }} - name: doctest