Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,15 @@ jobs:
# Must match `release-plz.yml`'s `cache-warm-linux` exactly or that job
# writes an entry this one cannot read, which is the current failure
# with extra steps.
key: ci
shared-key: ci
# The profile is part of the key: `[profile.*]` lives in the root
# `Cargo.toml`, which rust-cache does NOT consider (it reads
# `Cargo.lock` and `crates/batten/Cargo.toml` only). Without it a
# profile change leaves the key untouched, the restore reports
# `full match: true`, and every artifact it hands back was built
# under the old profile — measured 2026-09-01: 0 -> 121 `Compiling`
# lines. In `shared-key` rather than `key` because `key` is an
# if/else fallback rust-cache ignores whenever `shared-key` is set.
shared-key: ci-${{ hashFiles('Cargo.toml') }}
# ASK WHETHER THIS DIFF CAN MOVE THE SLOW TIER (CLOUD-398). Measured
# 2026-08-21: this job is ~13 of the ~14.7 billed minutes a non-Rust pull
# request costs, `test:bats` is ~81% of it, and a one-file change to
Expand Down Expand Up @@ -525,8 +532,8 @@ jobs:
# discarded. Its own key costs one more entry and no contention.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: bats
shared-key: bats
# The profile is part of the key — see the `ci` job above.
shared-key: bats-${{ hashFiles('Cargo.toml') }}
# THE SAME QUESTION THE `ci` JOB ASKS, and it must stay in-job rather than
# becoming a `paths:` filter or a job `if:` — see this job's header and
# `final`'s. `test:bats` is one of the six `slow`-tagged steps, so a diff
Expand Down Expand Up @@ -632,7 +639,8 @@ jobs:
# semver carries its own.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: perf
# The profile is part of the key — see the `ci` job above.
shared-key: perf-${{ hashFiles('Cargo.toml') }}
# CLOUD-1331. The BASE arm is a pure function of the merge-base SHA, the
# pinned toolchain and `[profile.release]`, and `main` advances only by
# fast-forward to already-judged SHAs — so consecutive pull requests share a
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ jobs:
with:
# Instrumented builds are a different profile from every other job's,
# so a shared cache entry would thrash.
key: coverage
# The profile is part of the key too — see `ci.yml`'s `ci` job.
shared-key: coverage-${{ hashFiles('Cargo.toml') }}
# Redirected, never piped. A pipeline hands the exit status to its LAST
# stage, so `mise run coverage | tee ...` would report success whenever
# `tee` succeeded — the false-green shape `mise run run-shape-guard`
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ jobs:
with:
# Sanitizer-instrumented nightly builds share no artifacts with any
# other job's profile, so a shared cache entry would only thrash.
key: fuzz
# The profile is part of the key too — see `ci.yml`'s `ci` job.
shared-key: fuzz-${{ hashFiles('Cargo.toml') }}
# The working corpus, carried between runs rather than committed. A fuzzer
# that restarts from the seeds every week re-derives the same shallow
# coverage every week and never gets past it; accumulating it in git
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ jobs:
with:
# A release profile, which no other job builds, so a shared cache entry
# would thrash.
key: perf
# The profile is part of the key too — see `ci.yml`'s `ci` job.
shared-key: perf-${{ hashFiles('Cargo.toml') }}
# Redirected into a file and read back, never piped into the gate. A
# pipeline hands its exit status to the LAST stage, so `perf |
# perf-assert` would discard exactly the measurement failure that must
Expand Down
19 changes: 10 additions & 9 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,11 @@ jobs:
# `id` and the guard together.
id: rust-cache
with:
# Both spellings must match `ci.yml`'s job exactly or this writes an
# entry that job cannot read.
key: ci
shared-key: ci
# Must match `ci.yml`'s job exactly or this writes an entry that job
# cannot read. One spelling now, not two: `key` is an if/else
# fallback rust-cache ignores whenever `shared-key` is set, so the
# pair could drift while looking deliberate.
shared-key: ci-${{ hashFiles('Cargo.toml') }}
# Compile only when there is nothing to restore. The evidence and the
# failure directions are recorded on the Windows job below; the same
# reasoning applies unchanged, and the same property refuses an unguarded
Expand Down Expand Up @@ -196,11 +197,11 @@ jobs:
# warm. `ci-local-parity` property 17 holds the pair together.
id: rust-cache
with:
# Both spellings must match `rust.yml`'s `windows` job exactly or this
# writes an entry that job cannot read, which is the current failure
# with extra steps.
key: windows
shared-key: windows
# Must match `rust.yml`'s `windows` job exactly or this writes an
# entry that job cannot read. One spelling now, not two — and that
# job carried `key` where this one carried both, an asymmetry that
# happened to match and could not be explained from the source.
shared-key: windows-${{ hashFiles('Cargo.toml') }}
# COMPILE ONLY WHEN THERE IS NOTHING TO RESTORE, and the evidence for this
# is that every warm cycle after the first was pure waste (CLOUD-840).
#
Expand Down
67 changes: 41 additions & 26 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ jobs:
# tool it touches. See the ci job for why these lists are narrow.
install_args: rust
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# The profile is part of the key — see `ci.yml`'s `ci` job.
shared-key: cross-${{ hashFiles('Cargo.toml') }}
- run: mise run cross-check
env:
# Only the triple cross-check type-checks. doctor's default pair would
Expand Down Expand Up @@ -210,7 +213,8 @@ jobs:
with:
# A per-target key: a leg's artifacts are target-specific and would
# otherwise thrash a shared entry if the second leg is restored.
key: ${{ matrix.target }}
# The profile is part of the key too — see `ci.yml`'s `ci` job.
shared-key: ${{ matrix.target }}-${{ hashFiles('Cargo.toml') }}
- run: mise run darwin-link ${{ matrix.target }}
env:
# Exactly the target being linked; doctor's default would also fetch
Expand Down Expand Up @@ -288,7 +292,8 @@ jobs:
# carries its own.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: semver
# The profile is part of the key — see `ci.yml`'s `ci` job.
shared-key: semver-${{ hashFiles('Cargo.toml') }}
- run: mise run semver

# THE OS MATRIX, and the only job here that EXECUTES rather than type-checks
Expand Down Expand Up @@ -435,33 +440,43 @@ jobs:
install_args: rust hk aqua:jqlang/jq github:nextest-rs/nextest
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# A distinct key: this job's target dir is built by a different host
# toolchain than every other job's, so sharing an entry would thrash
# it — the same reason semver and perf carry their own.
key: windows
# AND A SHARED KEY, which is what lets a job in ANOTHER workflow fill
# this entry (CLOUD-840). rust-cache composes
# `v0-rust-{key}-{shared-key or job id}-{os}-{arch}-{hashes}`, read off
# two live entries rather than from the docs — so without this the job
# ID is part of the key and a warm job under any other name computes a
# different one and the two never meet.
# A DISTINCT KEY, because this job's target dir is built by a different
# host toolchain than every other job's, so sharing an entry would
# thrash it — the same reason semver and perf carry their own. And a
# SHARED one, which is what lets a job in ANOTHER workflow fill this
# entry (CLOUD-840): without it the job ID is part of the key, and a
# warm job under any other name computes a different one.
#
# `key` IS GONE, AND ITS ABSENCE IS THE POINT. This block carried both
# `key: windows` and `shared-key: windows`, and the pair read as
# deliberate. It was inert: rust-cache v2.9.2 `src/config.ts` takes
# `shared-key` in an if/else and never reads `key` when it is set —
#
# if (sharedKey) { key += `-${sharedKey}`; }
# else { const inputKey = core.getInput("key"); … }
#
# which is exactly what the two live entries recorded here already
# showed: `v0-rust-windows-windows-Windows_NT-x64-…` before the shared
# key was added (key + job id), `v0-rust-windows-Windows_NT-x64-…`
# after (shared key alone). One component, not two. The composition
# this comment used to give, `{key}-{shared-key or job id}`, was the
# inference drawn from those strings and it was wrong; the strings
# themselves were right.
#
# IT REPLACES THE JOB COMPONENT RATHER THAN FILLING IT, and the first
# warm run is what proved it. This comment previously claimed the
# composed string stayed byte-identical because the value matches
# `key`; the two live entries say otherwise —
# `v0-rust-windows-windows-Windows_NT-x64-…` before,
# `v0-rust-windows-Windows_NT-x64-…` after. One component, not two.
# So adding this DID orphan every prior entry. Harmless, because those
# were per-PR and unreadable across pull requests anyway, and the
# migration is a single cold build on each side — but the claim was
# wrong and a comment that states a measured fact has to state the one
# that was measured.
# THE PROFILE IS PART OF THE KEY (2026-09-01). `[profile.*]` lives in
# the root `Cargo.toml`, which rust-cache does NOT consider — it reads
# `Cargo.lock` and `crates/batten/Cargo.toml` only. So `f54a9c05`
# changed the dev profile, left the key untouched, and every restore
# since has reported `full match: true` while handing back artifacts
# built under the old profile: this job went from 1 `Compiling` line to
# 288, and `ci` from 0 to 121. The warm jobs could not repair it
# either, since they compile only on `cache-hit != 'true'` and the key
# still hit.
#
# The warm side is `cache-warm-windows` in `release-plz.yml`. Both
# spellings must agree; that they are two places is the cost of the
# only trigger this repository is allowed to hang a warm job on.
shared-key: windows
# must agree; that they are two places is the cost of the only trigger
# this repository is allowed to hang a warm job on.
shared-key: windows-${{ hashFiles('Cargo.toml') }}
# A JOB THAT DECLARES A TOOL MUST BE ABLE TO REACH IT, asserted before the
# suite rather than discovered inside it. `ci-tools-check` holds these
# names against `mise.toml`, which is a different question: it cannot know
Expand Down
2 changes: 1 addition & 1 deletion batten.toml
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ spent by then."""
[[fact]]
name = "review-answered"
returns = "json-array"
command = "gh api graphql -F id=$(gh pr view --json id --jq .id) -f query='query($id:ID!){node(id:$id){... on PullRequest{author{login} reviews(first:100){pageInfo{hasNextPage} nodes{author{login}}} reviewThreads(first:100){pageInfo{hasNextPage} nodes{id isResolved}}}}}' --jq '.data.node as $p | [($p.reviewThreads.nodes[] | select(.isResolved == false) | .id), (if ([$p.reviews.nodes[] | select(.author.login != $p.author.login)] | length) == 0 then $p.author.login else empty end), (if $p.reviewThreads.pageInfo.hasNextPage then true else empty end), (if $p.reviews.pageInfo.hasNextPage then true else empty end)]'"
command = "gh api graphql -F id=$(gh pr view --json id --jq .id) -f query='query($id:ID!){node(id:$id){... on PullRequest{author{login} reviews(first:100){pageInfo{hasNextPage} nodes{author{login}}} reviewThreads(first:100){pageInfo{hasNextPage} nodes{id isResolved}} comments(last:100){nodes{author{login} body}}}}}' --jq '.data.node as $p | [($p.reviewThreads.nodes[] | select(.isResolved == false) | .id), (if (([$p.reviews.nodes[] | select(.author.login != $p.author.login)] | length) == 0) and (([$p.comments.nodes[] | select(.author.login != $p.author.login) | select(.body | test(\"auto-generated comment: summarize by\")) | select(.body | test(\"rate limited by\") | not)] | length) == 0) then $p.author.login else empty end), (if $p.reviewThreads.pageInfo.hasNextPage then true else empty end), (if $p.reviews.pageInfo.hasNextPage then true else empty end)]'"

# The DID-YOU-LOOK half. A `receipt` row is also what makes the fact reach Rego
# at all: `required_checks_for` reads the receipt rows, and `agent_records`
Expand Down
6 changes: 5 additions & 1 deletion completions/batten.bash
Original file line number Diff line number Diff line change
Expand Up @@ -2574,12 +2574,16 @@ _batten() {
return 0
;;
batten__subcmd__enforce)
opts="-J -q -v -y -h --json --strictness --fail-on-warning --config-from --config-in --silent --quiet --verbose --debug --trace --log-level --no-color --no-input --yes --help"
opts="-J -q -v -y -h --rule --json --strictness --fail-on-warning --config-from --config-in --silent --quiet --verbose --debug --trace --log-level --no-color --no-input --yes --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--rule)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--strictness)
COMPREPLY=($(compgen -W "permissive standard strict" -- "${cur}"))
return 0
Expand Down
1 change: 1 addition & 0 deletions completions/batten.fish
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ complete -c batten -n "__fish_batten_using_subcommand check" -l no-color -d 'Nev
complete -c batten -n "__fish_batten_using_subcommand check" -l no-input -d 'Never prompt; treat the run as unattended'
complete -c batten -n "__fish_batten_using_subcommand check" -s y -l yes -d 'Confirm a destructive operation that would otherwise refuse'
complete -c batten -n "__fish_batten_using_subcommand check" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c batten -n "__fish_batten_using_subcommand enforce" -l rule -d 'Run only the declared rule with this id' -r
complete -c batten -n "__fish_batten_using_subcommand enforce" -l strictness -d 'Raise how strictly gates apply (an override may only tighten policy)' -r -f -a "permissive\t'Advisory: findings are reported without failing the run'
standard\t'The default: a finding is a violation'
strict\t'Everything `Standard` fails on, plus anything advisory'"
Expand Down
1 change: 1 addition & 0 deletions completions/batten.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ trace\:"Add everything"))' \
;;
(enforce)
_arguments "${_arguments_options[@]}" : \
'--rule=[Run only the declared rule with this id]: :_default' \
'--strictness=[Raise how strictly gates apply (an override may only tighten policy)]: :((permissive\:"Advisory\: findings are reported without failing the run"
standard\:"The default\: a finding is a violation"
strict\:"Everything \`Standard\` fails on, plus anything advisory"))' \
Expand Down
32 changes: 26 additions & 6 deletions crates/batten/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,36 @@ pub struct CheckFlags {
pub since: Option<String>,
}

/// `enforce`'s flags, which travel together for `CheckFlags`'s reason.
///
/// A payload struct rather than fields on the variant, and the shape is a
/// REPAIR rather than symmetry for its own sake. `Enforce` carried its flags
/// inline, so adding `--rule` to it was `enum_struct_variant_field_added` — a
/// major break `semver` refused, on a verb whose flag set is the one most likely
/// to grow again. `#[non_exhaustive]` on the enum does not reach a variant's
/// fields; only a named struct does. Paying the break once here is what stops
/// the next flag paying it again.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct EnforceFlags {
/// Emit findings as byte-stable JSON instead of pointer lines.
pub json: bool,
/// Run only the declared row with this id, or every applicable row.
///
/// The narrowing `check` has carried since CLOUD-1051, extended to the
/// spawning verb for the one caller `check` cannot serve: a case whose
/// SUBJECT is a `kind = "command"` row. See `surface::ENFORCE_RULE`.
pub rule: Option<String>,
}

/// The top-level subcommands.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Command {
/// Run the applicable read-only gates against the repository.
Check(CheckFlags),
/// Run every configured rule, including kinds that execute a configured command.
Enforce {
/// Emit findings as byte-stable JSON instead of pointer lines.
json: bool,
},
Enforce(EnforceFlags),
/// Inspect configuration.
Config {
/// The chosen sub-verb.
Expand Down Expand Up @@ -1694,9 +1713,10 @@ fn command_of((name, matches): (&str, &ArgMatches)) -> Option<Command> {
staged: flag(matches, "staged"),
since: matches.get_one::<String>("since").cloned(),
})),
"enforce" => Some(Command::Enforce {
"enforce" => Some(Command::Enforce(EnforceFlags {
json: flag(matches, "json"),
}),
rule: matches.get_one::<String>("rule").cloned(),
})),
"config" => config_of(matches).map(|command| Command::Config { command }),
"lint" => lint_of(matches).map(|command| Command::Lint { command }),
"spec" => matches
Expand Down
Loading
Loading