Repository navigation
ci-pre.yml: rename from ci-precheck, job-level concurrency (janitor only), cache janitor on every push, PR-numbered cache keys #23
Description
Summary
This records the owner decisions from 2026-09-28 that turn #6's proto "precheck" into a concrete, fleet-wide ci-pre.yml. It is also where the Actions-cache janitor now lives. The first implementation is in zackees/soldr (in progress; linked below when the PR is up).
1. Rename: ci-pre.yml, never "precheck"
The fast first workflow is ci-pre.yml. Nothing is called "precheck": not the file, workflow name (CI Pre), job IDs, concurrency groups, step names or docs.
To do in this repo:
- rename
ci-precheck.yml→ci-pre.ymlin 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 design text andexamples/; - rename it in the
ci_lintfixturesCT-004,GEN-008,CACHE-009and_e2e; - rename it in any rule that names the file;
- have
ci_lintreport aci-precheck.ymlin a repo as a finding: wrong name, useci-pre.yml.
2. Shape
on: workflow_callonly. It is the first job ofci.yml, so it runs on everyci.ymltrigger: PR, push to any branch, schedule, dispatch.- Fast: ≤ 30 s when there is nothing to delete, with
timeout-minutes: 2. - No installs: stdlib
python3only; no setup-uv,uv runor pip. - The calling job grants
actions: write+contents: read. On fork PRs GitHub downgrades the token to read-only, and ci-pre copes with that.
3. Concurrency: per job, not per workflow
# ci-pre.yml has NO workflow-level `concurrency:`
jobs:
cache-janitor:
concurrency: { group: cache-janitor, cancel-in-progress: false } # repo-wide, serialized, never cancelled mid-delete
cache-budget: # no concurrency: runs immediately, in parallel
# every other ci-pre check: no concurrency- Only the janitor is serialized. Two sweeps must not act on stale listings in parallel. GitHub keeps one pending run per group, so a burst of pushes collapses to the latest sweep.
- Check jobs have no lock and must not
needs: cache-janitor, so a queued sweep never delays a check. - The proto fixture's workflow-level
concurrencywithcancel-in-progress: trueis removed: it would cancel sweeps mid-delete. - The caller
ci.ymlmay keep its own per-ref cancel group.
4. Cache janitor (cache-janitor job)
It runs on every push (all branches), plus schedule and dispatch, whenever the token can write.
- Merged or closed PR caches are deleted.
- A cache belongs to PR N if its key contains a delimited
pr-<N>component, or its ref isrefs/pull/<N>/merge. - Entries are grouped by N and each PR's state is looked up. Everything of a
closedPR (merged or not) is deleted. - A failed lookup keeps the entries (fail safe).
- Open PRs keep their caches, so repeat pushes stay warm.
- A cache belongs to PR N if its key contains a delimited
- Family budgets converge.
- Evictable families (
evict: lru, e.g. experiment lanes) are LRU-evicted down to their budget. - Pinned-download families keep the newest entry per key prefix.
- A non-evictable family whose declared steady state can't fit its budget is a static finding (row 8), never a permanently red cron.
- Evictable families (
- Safety. The janitor never deletes:
- the newest entry that a required job restores;
- any entry younger than a grace window (a named constant, e.g. 10 min), so it can't race a producer and its consumers.
A pull_request: closed trigger in the calling workflow deletes that PR's entries immediately, including on merge. A workflow-call-only file can't take that trigger. If the token can't delete, the job logs and exits 0, and the next push sweep catches it.
5. PR number in cache keys (new rule)
- Every cache saved in a PR context embeds
pr-<N>in its key, set in one place (e.g. setup-soldrcache-key-suffix). Main, nightly and non-PR keys carry no PR tag. - The ref alone isn't enough: pushes to a PR's branch save under
refs/heads/<branch>. - Proposed lint: a PR-context save without the
pr-key component is a finding (CACHE-*).
6. Budget verdict (cache-budget job)
- It evaluates live state independently of the janitor.
- Warn-only (exit 0) in PR context and on non-main pushes, for state the PR didn't create. Fails only for entries saved under the PR's own
pr-<N>/ ref, or a forecast breach from its own additions. - On
mainand schedule it is a hard failure if still over budget. A transient race with an in-flight sweep resolves on the next run.
Evidence (see the #6 comments):
- soldr's
Repository Actions cache budgetblocked ci: save setup-soldr caches fully under act (local bosn runs) soldr#3431 and #3438 on families neither touched, and needed manual pruning; - its 6-hourly sweep ended
STILL OVER BUDGET … pruning alone cannot fix thisevery run.
7. Related fleet work done the same day
- Local act/bosn runs save caches fully. Merged in ci: save setup-soldr caches fully under act (local bosn runs) clud#1553, ci: save setup-soldr caches fully under act (local bosn runs) mimalloc-pprof#567, ci: save caches fully under act (local bosn runs), keep main-push-only on GitHub zccache#1784, ci: save setup-soldr caches fully under act (local bosn runs) soldr#3431 and ci: save zccache caches fully under act, and repair CI (ruff, nested build, zig, pins, pyo3) python-rust-build-chain#9, as a stopgap until fix(cache): save-cache=auto should save fully on local runners (act/bosn) — PR-event skip leaves every local run cold setup-soldr#537 and feat(action): runner-aware save-cache=auto — save fully under act/bosn, skip PR saves only on GitHub (fleet parity with setup-soldr#537) zccache#1783 make it the default. Tracked under meta: correct stdout/stderr streaming for act tasks (pass-through + HTTP, token masking) and migrate clud onto it bosn#309.
- Policy issues filed here:
- policy(python): never plain
uv runin CI steps of a repo with a [build-system] — it silently rebuilds the package per step #16: no plainuv runin CI; - policy(rust-pypi): build wheels with
soldr wheel(abi3-py310); ban maturin CLI and zig (--zig, maturin[zig], ziglang, cargo-zigbuild) in CI #17:soldr wheel, no maturin CLI or zig; - policy(rust): avoid pinning soldr — float by default (soldr>=floor, setup-soldr@v0), exact pins only as recorded exceptions #18: avoid pinning soldr;
- policy(rust-pypi): release Linux wheels are manylinux_2_17, enforced by soldr wheel --release and verified — never the host's glibc floor #19: release Linux wheels are
manylinux_2_17, announced by a green info line with a--host-glibcoverride (soldr wheel: no runner can produce a manylinux_2_17 x86_64-unknown-linux-gnu wheel (host build is untagged-floor, aarch64 cross refused) soldr#3432, PR fix(wheel): release linux-gnu wheels always build against glibc 2.17 (#3432) soldr#3438 in progress).
- policy(python): never plain
Acceptance criteria
- 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 design text,
examples/and allci_lintfixtures useci-pre.yml, andci_lintflagsci-precheck.yml. - RED → GREEN
ci_lintfixtures:- a workflow-level
concurrencyinci-pre.yml→ finding; - a job other than the janitor with
concurrencyorneeds: cache-janitor→ finding; - a janitor without the repo-wide group → finding;
- a PR-context cache save without
pr-<N>→ finding; - a
ci-pre.ymlwith an install step → finding.
- a workflow-level
- 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 live-cache row (row 9) and rules
CACHE-005/006/008/GEN-009are updated to the janitor rules above: merged/closed-PR deletion, LRU convergence, grace window, warn-only PR verdict. - The zackees/soldr reference implementation is merged and linked here, with a measured ci-pre wall time of ≤ 30 s on a no-op run.
Activity
feat(ci-lint): ci-pre.yml rename + shape rules GEN-013..017, PR-keyed…
feat(ci-lint): ci-pre.yml rename + shape rules GEN-013..017, PR-keyed…
feat(ci-lint): ci-pre.yml rename + shape rules GEN-014..018, PR-keyed…
feat(ci-lint): ci-pre.yml rename + shape rules GEN-014..018, PR-keyed…
feat(ci-lint): ci-pre.yml rename + shape rules GEN-014..018, PR-keyed…
zackees commentedon Sep 29, 2026
Implemented by #60, squash-merged as af6d22d.
ci-lint side of #23:
ci-pre.ymlis the canonical name.GEN-014flags aci-precheck.ymlfile, auses:of one, or an[allow].workflowskey naming it. The example, every fixture (CT-004, GEN-008, CACHE-009, _e2e and the rest) and the docs are renamed.GEN-015flags a workflow-levelconcurrencyinci-pre.yml.WF-002now exempts that file.GEN-016flags a job other thancache-janitorthat hasconcurrencyorneeds: cache-janitor.GEN-017flags a janitor that lacks{ group: cache-janitor, cancel-in-progress: false }.GEN-018flags an install step inci-pre.yml.CACHE-013flags a PR-reachable cache save without a delimitedpr-<N>key component. The planner has a new--github-outputkey,cache_key_pr, which holdspr-<N>on a PR and is empty elsewhere.pr-<N>anywhere in a key, still accepts the legacydelta-v1-pr<N>form, and treats arefs/pull/<N>/mergeref as belonging to PR N.evict = "lru"key.ci-lint cache budget:pr-<N>entries.Rule IDs are GEN-014..018 and CACHE-013 because GEN-012, GEN-013 and CACHE-010..012 were taken by #57, #61 and #62 while this was in flight.
Validation: there is no CI in ci.yml, so the selftest pair is the validation (435 tests):
uv run --no-project --with pyyaml python3 -m ci_lint selftest: OKpython3 -S -m ci_lint selftest(PyYAML not importable): OK (skipped=63)The RED/GREEN output for every rule is in the PR body.
Not done here:
precheckcaller job id and theci_lint prechecksubcommand.fix(ci-lint): delta keys use delimited pr-<N>; dylint-output family i…