Repository navigation
policy(cache): detect kill-switched layers and green default-branch runs that save nothing (fleet-wide) #7
Description
Context
policy-rust.md already states the intent (RUST-004: a successful Dylint run must not silently skip saving reusable outputs). #1 covers one mechanism, the short-vs-qualified nightly marker mismatch. #6 adds a symptom guard: CACHE-005 refuses saves below a family's min size, and heal deletes a poisoned entry.
Nothing yet detects the causes that leave a repository permanently cold while every signal stays green:
- (a) an env or config kill-switch that disables a cache layer the setup action still reports as active;
- (b) a successful default-branch run (the only writer flow under policy(rust): make coverage-per-compile-minute the objective; require cheap cross-target checks, a cache budget, and a toolchain sourcing order #5/feat: make template-python-rust-cmd the lean, machine-checked rust-pypi-app reference (ci.toml v3, ci-lint, tag flows, self-healing cache) #6) that saves nothing, or saves an empty payload;
- (c) restores that "hit" an empty archive, forever.
zackees/clud has all three at once in its required Dylint job. This is a fleet checker request, not a clud bug report. Any repository using setup-soldr with zccache-unit / dylint-foundation / dylint-output layers can be in the same state, and a green check never reveals it.
Evidence (zackees/clud)
Kill-switch contradicting the declared layers
.github/workflows/_dylint.ymlsetsZCCACHE_DISABLE: "1". It arrived incidentally in clud #487 (commitf31368f7, 2026-07-06), a PR about Codex fallback instructions.- setup-soldr still logs
cache-policy layers=toolchain+cargo-registry+zccache-unit+dylint-foundation+dylint-output, yet zccache never runs:final zccache session stats: missing. - The only Dylint entry on
mainissetup-soldr-buildcache-v2-linux-x64-beb1de84d481fcb7-dylint-fd206842bb772458, 253 bytes. PR restores warnproduced an unusable payload: archive=22B extracted_files=0 extracted_bytes=0and treat it as a miss. feat: make template-python-rust-cmd the lean, machine-checked rust-pypi-app reference (ci.toml v3, ci-lint, tag flows, self-healing cache) #6 records the same entry missing on 4/4 main pushes.
A green default-branch run saves nothing
mainpush run36463271709, job109067163056("Dylint / Dylint", conclusion success, 18:10:45→18:14:51) logs:dylint-cache: no matching successful Dylint marker - skipping savedylint-output-cache: Dylint did not complete successfully - skipping savefinal cache summary: … build=miss/saved id=-1
- Mechanism so far:
- soldr writes
SOLDR_DYLINT_SUCCESS_MARKER(contentplan.cache_identity()) only on the cargo front-door path (crates/soldr-cli/src/cargo_front_door/run.rs~L888 →dylint_toolchain.rs::write_success_marker, gated ondylint_entrypoint). - setup-soldr
src/post.ts~L2330 requires the marker text to equal its owncacheIdentity. - clud invokes
soldr dylint --all …. Not yet determined: whether that path writes no marker, or writes one whose identity mismatches (the fix: detect Dylint recompilation and cache identity drift in fast CI #1 class).
- soldr writes
- Either way, the lint passes and the cache is never written.
Cost: every run on every branch re-checks the whole dependency graph cold. From PR clud#1541, run 36467947289, which adds Windows/macOS cross-target passes:
| Pass | Dependency units checked | First workspace unit at | Pass finished at |
|---|---|---|---|
host x86_64-unknown-linux-gnu |
405 | 112.8 s | 144.0 s |
--target x86_64-pc-windows-msvc |
336 | 113.3 s | 147.2 s |
--target aarch64-apple-darwin |
278 | 31.5 s | 60.2 s |
- About 75–80% of each pass is dependency type-checking; the workspace crates take roughly 30 s per pass.
- The Dylint job runs about 7m45s (18:50:28→18:58:13), versus 4m06s on
mainbefore the cross passes. - Dylint lints only workspace crates, but dependencies must be checked by the same pinned nightly, so stable build caches cannot substitute. Only a nightly-keyed dependency layer removes this cost: per-unit zccache, or
soldr dylint cook(Linux-only, saved from the default branch per policy(rust): make coverage-per-compile-minute the objective; require cheap cross-target checks, a cache budget, and a toolchain sourcing order #5).
Proposal: candidate rules
These IDs continue after CACHE-001…009 (#6), RUST-008…012 and GEN-009…010.
| Candidate ID | Mechanical signal |
|---|---|
CACHE-010 |
A workflow or job sets a cache-layer kill-switch while its setup step declares that layer active. Examples: ZCCACHE_DISABLE, SOLDR_NO_CACHE, --no-cache on the compile step, cache: false for the matching family. Runtime confirmation: the setup log lists the layer in cache-policy layers=… but the job logs final zccache session stats: missing or zero compilations. Static detection is from YAML; runtime detection is from logs. |
CACHE-011 |
A successful default-branch run, the writer flow, ends with a declared layer unsaved or saved below its min size. Log signals: save skipped for any reason other than exact-hit or PR policy; no matching successful … marker; did not complete successfully while the job conclusion is success; saved id=-1. Reported per family with the job URL. CACHE-005 refuses the tiny save; CACHE-011 names why the writer produced nothing. |
CACHE-012 |
A family's newest default-branch entry has been restored as unusable payload / treated-as-miss on N consecutive runs (proposed N=2) and no writer run has replaced it. This is the steady-state detector for (c): it pairs with #6's heal and flags repositories where heal never fires because the writer itself is broken (CACHE-011). |
Checker behavior:
-
Report cold-vs-warm evidence per the existing timing rules in
policy-general.md: compilation counts and the dependency/workspace time split for Dylint passes, not only job duration. Label a run "warm" only when the log shows restored units that were actually used. -
Fleet sweep: for every repository whose workflows call
setup-soldrwithdylint: trueor zccache layers:- read the workflow YAML for kill-switches;
- read the latest successful default-branch job log for each declared layer's save outcome;
- list the family's cache entries with sizes.
Do not rely on GitHub code search: it did not return clud's
_dylint.ymlforZCCACHE_DISABLE.
Acceptance criteria
CACHE-010–012are added topolicy-general.md(or the feat: make template-python-rust-cmd the lean, machine-checked rust-pypi-app reference (ci.toml v3, ci-lint, tag flows, self-healing cache) #6 cache section), each with its mechanical signal and fixture.- RED fixture: clud
mainrun36463271709(green Dylint, both Dylint saves skipped, 253-byte build-cache entry) together with PR run36467947289(the 22-byte restore treated as a miss, and 405/336/278 cold dependency units). The checker must emitCACHE-010(staticZCCACHE_DISABLEplussession stats: missing),CACHE-011andCACHE-012with those run and job URLs. - GREEN fixture: a default-branch run that saves a non-trivial Dylint/zccache dependency payload (above the family
min), followed by a run that restores it with measurable dependency-unit hits. The cold-vs-warm report must show the dependency portion of each pass collapsing. - The fleet sweep produces one finding per repository and family; repositories without these layers are
pass, and inaccessible logs areinsufficient evidence.
Decisions
- P1. The failure is silent by construction: required checks stay green, the setup summary claims the layers are active, and feat: make template-python-rust-cmd the lean, machine-checked rust-pypi-app reference (ci.toml v3, ci-lint, tag flows, self-healing cache) #6's poison guard treats the symptom without naming the cause. clud pays about 4–6 extra minutes on every PR's required Dylint job.
- This extends fix: detect Dylint recompilation and cache identity drift in fast CI #1 (marker mechanics) and feat: make template-python-rust-cmd the lean, machine-checked rust-pypi-app reference (ci.toml v3, ci-lint, tag flows, self-healing cache) #6 (
CACHE-005size floor,heal) without replacing them. The dependency-layer design (per-unit zccache versussoldr dylint cook, Linux-only, default-branch saves) follows policy(rust): make coverage-per-compile-minute the objective; require cheap cross-target checks, a cache budget, and a toolchain sourcing order #5. - The fix in each repository belongs in that repository. The checker's job is detection with log evidence.
Open questions
- Should
CACHE-011treat a marker mismatch differently from a missing marker? The fix owner differs: a soldr path that never writes the marker, versus a setup-soldr/soldr identity contract (fix: detect Dylint recompilation and cache identity drift in fast CI #1). - Should the kill-switch list be owned by setup-soldr as a machine-readable manifest of env and inputs that disable each layer, so the checker does not hard-code names?
- Does zccache's per-unit layer cache nightly check-mode (
.rmeta) dependency units well enough for Dylint, or issoldr dylint cookthe required layer? This needs a measured cold/warm pair on clud afterZCCACHE_DISABLEis removed.
Related
ci.yml#1, #5, #6; clud#487 (f31368f7), clud#1541; runs 36463271709 (job 109067163056) and 36467947289; soldr#2349 (Dylint cache and skip), soldr#3042 (soldr dylint cook).
Activity
feat(ci-lint): CACHE-010/011/012 -- kill-switched layers, green runs …
feat(ci-lint): CACHE-010/011/012 -- kill-switched layers, green runs …
zackees commentedon Sep 29, 2026
Implemented in #62 (merged):
CACHE-010(static): a workflow/job/step env sets a kill-switch var (ZCCACHE_DISABLE,SOLDR_NO_CACHE,SOLDR_CACHE_DISABLE,SOLDR_DYLINT_NO_CACHE) truthy whileci.tomlstill declares a[cache.family]via a matchingsetup-soldr:*/zccache*backend.CACHE-011/CACHE-012(runtime,ci-lint cache save-check --log <file>... --conclusion <c>, pure filesystem): a successful run's log showing a declared layer's save skipped for a non-exact-hit reason (orsaved id=-1), and a layer whose restore reports an unusable 0-extracted payload on >= 2 consecutive logs with no writer save in between.RED/GREEN fixtures reproduce this issue's own evidence verbatim (clud
mainrun 36463271709 job 109067163056, PR run 36467947289).selftestpasses with and without PyYAML (419 tests). Verified read-only against zackees/template-python-rust-cmd:CACHE-010does not false-positive there.Not done (documented as open in the PR): a live fleet-wide sweep across repositories (no central scanner exists yet -- out of ci_lint's current scope per AGENTS.md), and a machine-readable kill-switch manifest owned by setup-soldr (issue's own open question) -- ci-lint's list is its own reserved allowlist for now.