Skip to content

ci: add a schema-3 ci.toml bounding the Actions cache - #52

Merged
zackees merged 3 commits into
mainfrom
ci/cache-policy
Oct 5, 2026
Merged

zackees merged 3 commits into
mainfrom
ci/cache-policy

Conversation

@zackees

@zackees zackees commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Adds ci.toml (schema 3) so wild's GitHub Actions cache has declared family boundaries and a budget, plus the [workspace.metadata.soldr].targets table CT-006 cross-checks it against.

Nothing is deleted by this PR. No cache is touched, and no workflow is edited.

Why

Measured 2026-10-05 (paginated actions/caches, after a reclaim): 26 entries, 4.58 GiB = 46% of GitHub's 10 GiB repository limit — the last fleet repository above 40% with no ci.toml at all. With no [cache.family] table, nothing bounded how many entries a family could accumulate.

Families: measured, then declared

family via entries live total live max declared
cook setup-soldr:cook 3 1653.3 MB 666.4 MB shapes 700/660/420 MB, evict = "lru"
compile setup-soldr:build-cache 3 790.2 MB 423.7 MB shapes 140/460/290 MB, evict = "lru"
registry setup-soldr:cargo-registry 3 92.4 MB 31.0 MB max = "35MB", per = "os"
soldr-mini setup-soldr:soldr-mini 3 29.0 MB 10.3 MB max = "12MB", per = "os"

Why shapes on the two large families

ci-lint cache janitor LRU-evicts an evict = "lru" family down to family_footprint(), which is the sum of shapes when declared, else max × cardinality(per). CACHE-004's static proof uses the same number. wild's entries differ sharply within a family:

  • compile: 113.9 MB (linux-x64) / 423.7 MB (macos-arm64) / 252.6 MB (windows-x64) — a 3.7x spread
  • cook: 666.4 / 616.3 / 370.6 MB — 1.8x

One max per family would model 424 × 3 = 1271 MB for a family that actually occupies 790 MB. Per-shape ceilings give the honest sum. Every ceiling is at or above the observed live maximum for that shape; over-stating only costs headroom, while under-stating truncates live caches.

registry and soldr-mini are uniform across hosts (31.0/30.9/30.4 and 10.3/10.1/8.6 MB), so a single ceiling each is truthful.

Both shaped families also declare per = "os". That is not padding: setup-soldr's prefixes embed ${runnerOs}-${runnerArch}, so these families really are one entry per runner OS (3), and ci_lint.cache.audit's live CACHE-004 model check reads per (it does not read shapes). Without it the live check computes cardinality(per=None) × 2 = 2 against 3 live entries and fires two spurious findings — cook as a hard violation, compile as needs-review. With per = "os" both clear, and the footprint arithmetic is unchanged because family_footprint() prefers shapes.

Budget arithmetic

compile:    3 shapes [linux-x64=140MB, macos-arm64=460MB, windows-x64=290MB] =  890 MB (lockfile)
cook:       3 shapes [linux-x64=700MB, macos-arm64=660MB, windows-x64=420MB] = 1780 MB (lockfile)
registry:   35MB x 3 (os)                                                      =  105 MB (lockfile)
soldr-mini: 12MB x 3 (os)                                                      =   36 MB
  steady total            = 2811 MB
  + lockfile-change peak  = 2775 MB
  + [cache.pr].budget     =    0 MB
  = worst case            = 5586 MB = 5.34 GiB
  budget ([cache].budget) = 9.5 GB

steady 2811 MB vs budget 9.5 GB. The lockfile-change peak is counted rather than waived: no writer flow declares pre-prune = true, because wild runs no ci-lint cache preprune step today and claiming otherwise would be a false statement about the YAML. The honest arithmetic fits comfortably either way.

Live audit (ci-lint cache audit --repo .)

26 live cache entries, 4914519143B total
family                    count        bytes
compile                       3    828603699
cook                          3   1733563096
registry                      3     96886851
soldr-mini                    3     30425403
undeclared                   14   2225040094
budget: 4914519143B / 10200547328B (48.2%)

Zero CACHE-004 findings. The only remaining findings are the undeclareable family below, plus CACHE-008 (six v0-rust-* entries sitting on the merge refs of already-closed PRs #28/#31 — the janitor reclaims those safely on its next sweep, again not by this PR) and the GEN-009 rollup that cites them.

Janitor dry-run — the check that matters

ci-lint cache janitor --repo . --dry-run against the live repository:

family                    before(n)    before(B)   after(n)     after(B)
compile                           3    828603699          3    828603699
cook                              3   1733563096          3   1733563096
registry                          3     96886851          3     96886851
soldr-mini                        3     30425403          3     30425403
undeclared                       14   2225040094          0            0

Zero deletions from all four declared families — before(n) == after(n) and before(B) == after(B) for every one. The 14 the dry-run would delete are exactly the undeclared v0-rust-* entries, which is the correct signal: they are the ones nothing declares.

This check is why the numbers above are a table rather than a summary. A previous adopter's first draft put a family's ceiling below live and static CACHE-004 passed it anyway; only the live dry-run caught it.

v0-rust-* — undeclareable, and the reason

14 entries, 2122.0 MB, 45% of the repository's cache. Deliberately not declared.

ci_lint.cache.families.ALLOWED_VIA_VALUES contains ci-lint plus the setup-soldr/setup-uv shapes, and none of them resolves to a v0-rust- prefix. Verified by reading the shape table: the prefixes are cook-base-v2-, setup-soldr-buildcache-v2-, setup-soldr-cargoregistry-v1-, soldr-mini-v2-, setup-soldr-targetcache-, setup-uv-2- and the rest. v0-rust- is Swatinem/rust-cache's own default, from core.getInput("prefix-key") || "v0-rust" in its src/config.ts:74.

And CACHE-025 bans Swatinem/rust-cache outright — unwaivable, per the maintainer decision of 2026-10-02 (docs/policy-rust.md, zackees/ci.yml#209): no [[exceptions]] entry, no # ci-lint: allow comment, no benchmark-baseline carve-out. ci-lint precheck currently reports three CACHE-025 violations in .github/workflows/ci.yml (lines 62, 83, 274).

So there is no honest via to declare, and inventing one would make a number fit at the cost of a false statement in the config. The correct disposition is to delete the three Swatinem/rust-cache@v2 steps, which removes the writer and makes the 2122 MB legitimately reclaimable. That is already tracked in #19 ("stop caching Rust toolchains in wild CI") and is deliberately not done here — it is a workflow change with real cold-start consequences, out of scope for a cache-policy PR.

Until then these entries stay reported as CACHE-001, and ci-lint cache janitor treats them as deletable. That is the intended behaviour and the reason to close #19.

Other things this file does not claim

  • [cache.promote] mode = "off". setup-soldr#566 (ancestor-cache.ts, auto-key) is merged but unreleased; auto-key defaults to "false" and no fleet repository has a single live setup-soldr-ancestor-build-v1-* entry. Declaring mode = "ancestor" would trip CACHE-030 needs_review on every run and stand as an invitation to opt into a pilot this repository cannot run.
  • No [local.gate]. wild has no local gate — no local-gate.toml at the repo root, and no local-gate reference in AGENTS.md (both checked 2026-10-05). The table is deliberately absent, not empty: ci_lint.local_gate._load_gate_config only prefers ci.toml's [local.gate] when the table exists, so omitting it leaves the "no gate configured" reading intact.
  • write-on = ["main"], pr = { mode = "none", ..., budget = "0MB" }. PRs restore and never save.
  • No [publish.pypi]. wild ships a Rust binary and a Nix package, not a wheel.
  • No [rust] table. Declaring one obliges the file to also answer for public/private/ship and the full test-target inventory — a workspace-wide statement outside a cache-policy change. wild publishes every workspace crate to crates.io (RELEASING.md: "Run cargo publish for each package"), so it has no public/private split to declare truthfully. The one thing RUST-005 asks for is waived with a dated [[exceptions]] entry: wild has exactly one integration-test target (wild/tests/integration_tests.rs, harness = false) — already at the floor, nothing to consolidate.

[platforms] and the Cargo.toml addition

Ten platforms, every one a job that really exists in .github/workflows/ci.yml: linux-x64, linux-arm64, linux-riscv64, linux-loong64, linux-ppc64le, linux-x64-musl, linux-arm64-musl, linux-wasm32, macos-arm64, windows-x64. The glibc/musl pairs come from the CI container images' rustup target add lines (docker/ci/*.Dockerfile), the cross triples from the linux-cross-build matrix, and wasm32-wasip1 from the wasm-test job.

CT-006 requires [platforms]'s target set and [workspace.metadata.soldr].targets to be identical, so this adds that 22-line table to Cargo.toml. It is the single source of truth for the shipping matrix (it is what soldr prepare --target all reads) and is inert metadata otherwise.

Two smaller notes on the declarations themselves:

  • [platforms].*.runs-on uses the fleet-pinned labels (macos-15, windows-2025), not the macos-latest/windows-latest the workflows currently use. RUN-001 only resolves runs-on: ${{ matrix.<var>.runs_on }}, and ci.toml declares the target state. Pinning the workflows to match is a follow-up.
  • [allow].setup-soldr.only-in = ".github/workflows" names the location that actually exists — wild has no .github/actions/soldr wrapper and calls zackees/setup-soldr@v0 directly from eleven places. Declaring the preferred-but-absent path would fire CACHE-009/RUST-001 eleven times on a gap this PR does not close. Folding the calls into a wrapper composite action is the follow-up.

Validation

load_ci_toml                 0 findings
check_cache_004              0 findings (worst 5 857 345 536 B vs 10 200 547 328 B budget)
check_cache_029              0 findings
check_cache_030              0 findings
ci-lint cache audit (live)   0 CACHE-004 findings
ci-lint cache janitor        0 deletions from declared families

The remaining precheck findings (SEC-004 SHA pinning, TOOL-001/RUST-001 bare cargo, GEN-005, CACHE-013, WF-*, TAG-003, GATE-012) are pre-existing properties of wild's existing workflows and Cargo.toml, not introduced here. LAYOUT-001 and RUST-005 were introduced by this file's own declarations and are resolved: LAYOUT-001's selector list cannot distinguish host from target selection, so two target-selection files (libwild/src/args/elf.rs's default_emulation(), linker-diff/src/utils.rs's objdump tests) are listed in [allow].platform-code with the reasoning inline.

🤖 Generated with Claude Code

wild held 4.58 GiB across 26 live cache entries (46% of the 10 GiB
repository limit) with no ci.toml, so nothing bounded how many entries
any family could accumulate.

Declares the four families setup-soldr actually writes, with per-shape
ceilings where entries within a family differ in size:

  cook      cook-base-v2-*              3 entries  1653 MB  evict=lru
  compile   setup-soldr-buildcache-v2-* 3 entries   790 MB  evict=lru
  registry  setup-soldr-cargoregistry-v1-* 3 entries  92 MB
  soldr-mini soldr-mini-v2-*            3 entries   29 MB

compile's entries span 114 MB (linux-x64) to 424 MB (macos-arm64); a
single `max` would over-count 3.7x, so both large families declare
`shapes` and their footprint is the honest sum. Every ceiling is at or
above the observed live maximum -- `ci-lint cache janitor` LRU-evicts
down to exactly family_footprint(), so under-stating truncates live
caches.

[v0-rust-* (14 entries, 2122 MB) is deliberately NOT declared: no via
in ci_lint.cache.families.ALLOWED_VIA_VALUES resolves to that prefix.
It is Swatinem/rust-cache's own default, and CACHE-025 bans that
action outright. Removing the three steps is tracked in #19.]

`[cache.promote] mode = "off"`: setup-soldr#566 is merged but unreleased,
so there is no shipped ancestor mechanism to opt into.

Also adds [workspace.metadata.soldr].targets to Cargo.toml, which
CT-006 requires ci.toml's [platforms] to agree with.

Verified: zero schema findings; CACHE-004/029/030 clean; the live janitor
dry-run proposes zero deletions from all four declared families.
wild's `tidy_tests::check_toml_format` runs taplo over the repository's
TOML, so an unformatted ci.toml fails `check_toml_format` and with it the
six `Test Linux (...)` jobs, `minimal-versions`, `reuse-check` and
`Nix/wild` -- nine red jobs from one formatting difference.

taplo collapses the aligned `=` to single spaces, which is the whole
change; no key or value is altered.
`fsfe/reuse-action` (the `reuse-check` job) verifies every file carries
REUSE licensing metadata. The new `ci.toml` had none and was not listed
in REUSE.toml's annotations, so the compliance check failed -- a
licensing failure, not a CI one.

Inherits the repository's existing annotation: MIT OR Apache-2.0,
Copyright 2024-2026 Wild Linker Contributors.
@zackees
zackees merged commit e2e75df into main Oct 5, 2026
28 checks passed
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.

meta: stop caching Rust toolchains in wild CI (setup-soldr seal-at-install + no-scan off mode, soldr target self-heal)

1 participant