Lithair is a memory-first Rust web framework I build; cidx is the declarative CI runner I build, and it runs Lithair's CI. Both are open source. This post stands on its own.
A brand-new container in Lithair's CI, declared in four lines of cidx.toml: a Rust image pinned by digest, a script that installs PostgreSQL 17 inside it, generates a test-only PKI and runs the suite with LITHAIR_REQUIRE_POSTGRES=1, and privileged = true, because installing a package means writing to /var/lib/apt.
And it falls over:
E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)
As if the flag did not exist. privileged = true is right there, read three times. The container still runs as the host user.
What was happening
cidx reuses containers from one run to the next. To decide whether the existing container is still the right one, it hashes the configuration: image, command, working directory, entrypoint, volumes, environment variables. And a comment in the code, an honest one, says that PullPolicy, Privileged and Timeout are deliberately left out of the hash because they "affect execution behavior, not the container".
True for the timeout. False for privileged. In cidx an unprivileged container runs with the host user's id; a privileged one runs as root. The flag therefore changes User in the Docker config, which is the container itself. Flipping it from false to true, cidx looked at the hash, found it unchanged, and picked up the container created earlier, with its earlier user.
docker rm -f on the container clears it. That is not a fix, it is a diagnosis: issue cidx#531 goes up with the cause and a proposal, include in the hash everything that changes the created container's configuration.
Eighteen hours
2026-10-02 14:55Z lithair cidx#531 opened: "privileged reuses the previous container"
2026-10-03 08:04Z cidx #532 recreate the container when the user it runs as changes
08:27Z cidx #533 compare the environment the container was created with, not the declared one
08:39Z cidx #534 v3.8.0
09:07Z lithair #302 "ci: upgrade cidx to 3.8.0" — the only diff is the number
Eighteen hours and twelve minutes from the issue to the consumed release. The second fix was not in the issue: looking at the hash surfaced a neighbouring defect. cidx compared the environment declared in the config, not the one the container had been created with. Two paths to the same question, "is this container still the right one?", and one of them could say yes wrongly.
The Lithair PR that consumes the release has a one-sentence description: "3.8.0 recreates a container when privileged changes, found while adding the postgres-test container". The GitHub workflow is regenerated by cidx generate github, and the only diff is the version pin.
Not the first time that week
It was the fourth time in five days that Lithair's CI wrote cidx's requirements.
- No cache in the generated workflow (cidx#503): v3.5.0 adds a per-phase cache, Lithair consumes it as 3.6.0 on September 29th.
- No cancellation of previous runs on a PR (cidx#504): same release.
- The cache restores from the previous key when the key changes (cidx#515). Measured on Lithair's test phase: cargo never prunes
target/, so every key change stacked a generation of artifacts on the previous one, 3.3 → 5.4 GiB after a single version bump. Fixed in 3.7.0 withcache_restore_fallback = false; the first run after a key change compiles cold, which is the honest price of a cache that does not lie about its size. -
privilegedignored (cidx#531): 3.8.0.
Three cidx bumps in Lithair in five days, 3.6.0, 3.7.0, 3.8.0, each with its reason written in the PR. Seven issues opened by the same author and closed the same week on the cidx side. It is the same mechanism that produced seven Lithair releases in August: the consumer dictates, the tool follows. This time Lithair is the consumer and cidx is the tool.
Meanwhile, Lithair
The Postgres container existed because Lithair had just gained its third storage tier. RFC 296 sets the vocabulary before the code:
tier where today
─────────────────────────────────────────────────────────────────
L1 process memory native models (SCC2), always resident
L2 local durable storage native log + snapshots; embedded Turso
L3 external database new: PostgreSQL
a model's authority lives in exactly one tier;
"durable" is declared, and requires an L3 authority
lithair-postgres 0.1 lands in v1.17.0 with the same surface as the Turso adapter: #[storage(postgres)], the same generated routes, migrations and application commands. The choices are those of someone who has already lost data: SERIALIZABLE everywhere with a bounded number of retries, a lithair schema the framework owns and migrates under an advisory lock so it happens exactly once across nodes, TLS verified whatever sslmode asks for, Unix sockets refused. The core still depends on no driver.
The rest of the week, in order: v1.13.0 wires declarative models onto the native consensus and fixes the frontend journal that had bloated this very site to 1.3 GB; v1.14.0 and v1.15.0 add atomic application commands, native then Turso; v1.16.0 brings OpenID Connect login for custom handlers, with PKCE and single-use login attempts; v1.18.0 puts an L1 cache on external models, with #[retention(memory, max_mb, ttl)] and invalidation through pg_notify. Three RFCs merged before their code, every time.
The limit, named
Seven releases in seven days is a sprint, not a cadence. I wrote the same sentence two weeks ago about five releases in two days; it still holds.
And a mistake I caught this morning in my own tooling: session replication in the native cluster was merged five hours after the v1.18.0 tag. It is on main, it is in no release, and the automated tracking I maintain had filed it under 1.18. The next release will make it true; the map was wrong until this morning.
Finally, OIDC login pulls rsa 0.9 through openidconnect, and that crate carries RUSTSEC-2023-0071. It is written in the release notes, not hidden in an exceptions file. And this is where the cidx week meets the Lithair week: since 3.7.0, cidx's audit gate gives a finding seven days from first sight before it fails the build, counted from when the scanner first saw it rather than from the advisory's publication date, with no grace for a known-exploited vulnerability or an expired exception. The same week, three CVEs published on the day got their answer on the day. A gate that leaves seven days to decide and then breaks is the only way two repositories can ship on the same morning without lying about a same-day CVE.
A loop that closes in eighteen hours is not speed. It is an ecosystem that exists.
French original on arcker.org.
Top comments (0)