You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The shared CI image family is effectively monolithic. ci-base bakes in the full Tauri desktop stack (GTK, webkit2gtk, appindicator, rsvg, patchelf, xdg-utils, libssl-dev), and ci-toolchain piles Node 24 + pnpm + Rust + cargo-nextest + tauri-cli into one stage. Every consumer inherits everything: a pure-Rust codec library pulls roughly 1.5 GB of GUI and Node tooling it never uses, and there is no image a Rust-only repo can adopt without that overhead.
A toolchain survey of all active Liminal HQ repos plus ScottMorris/city-sim-1000 (2026-07-22) shows the org's real toolchain clusters. This issue proposes re-layering the shared Dockerfile into granular tiers where each layer adds exactly one concern, publishing two new CI images (ci-rust for pure-Rust repos and ci-web for JavaScript/TypeScript repos), mirroring the same tiers in the dev image family (dev-rust, dev-web), and adding Bun to the Tauri images — while preserving the existing published image names and contracts.
The dev family is a first-class deliverable here, not a byproduct: the tauri-dev-* images are actively used on desktops and laptops as portable toolboxes for running tools that are not installed on the host, and today they are built from the same Dockerfile and workflow as the CI images. The granular tiers need to exist on both sides.
What the survey found
Five repos consume the shared CI images today:threshold and foyer use tauri-ci-mobile; spindle, emoji-nook, and tauri-plugins-workspace use tauri-ci-desktop. Everything else runs on plain runners with setup actions.
Tauri + Bun is a real, unserved combination.foyer (Tauri desktop + Android, bun.lock) curl-installs Bun into tauri-ci-mobile on every CI run because the image only ships pnpm. city-sim-1000 (Rust + Bun, Tauri plugin crate) apt-installs webkit2gtk/appindicator/rsvg on plain runners every run. Bun is a house standard beyond that: smdu and coherence-chat-exporter both use bun build --compile for release binaries.
JavaScript-only repos run on bare runners today, but only for historical reasons.smdu, coherence-chat-exporter, the-lab, and liminal-hq.github.io all use setup-node/setup-bun/pnpm actions because the shared images were originally Tauri-focused, not because containers do not fit. They are intended adopters of a JS tier. Their runtime usage is mixed (pnpm, npm, and Bun in combination — smdu uses pnpm for deps and Bun to compile release binaries; coherence-chat-exporter uses npm plus Bun compile), which argues for one combined JS image rather than per-runtime forks.
Five pure-Rust repos have no suitable image:flow, flicker, libhdmv, libvc1, and mudroom need only a pinned Rust toolchain, clippy/rustfmt, cargo-nextest, and gh — none of the GUI or Node stack.
threshold desktop jobs cannot use the CI desktop image. They apt-install the full GTK stack plus GStreamer (base + bad), fonts-noto-color-emoji, and xvfb on host runners each run; the CI desktop image lacks those last three (the dev desktop image already has them).
cargo-nextest is reinstalled per run via taiki-e/install-action in threshold, flow, and city-sim-1000 even though the shared images already bundle it.
Rust pins are drifting: shared images pin 1.93.0, while threshold pins 1.96.1 and city-sim-1000/flow pin 1.94.
ARM matters at the Rust tier:threshold, spindle, emoji-nook, flow, smdu, coherence-chat-exporter, and city-sim-1000 all run ubuntu-*-arm jobs.
The dev images are used as host-side toolboxes, not just devcontainers. The tauri-dev-* images see regular interactive use on desktops/laptops to run tools that are not installed on the host. libvc1 demonstrates the granularity gap on this side too: its local docker/Dockerfile.toolchain bases on tauri-dev-desktop (plus ffmpeg) just to get a Rust toolchain, inheriting the entire GUI/Node/Tauri stack it never uses.
apt webkit stack every run, nextest, wasm-pack, arm64
smdu
pnpm + Bun compile
—
—
none
Bun cross-target binaries
coherence-chat-exporter
npm + Bun compile
—
—
none
Bun cross-target binaries
flow
—
1.94
—
none
nextest, arm64 releases
flicker
—
stable (MSRV 1.87)
—
none
—
libhdmv
—
stable
—
none
—
libvc1
—
stable
—
none (dev: tauri-dev-desktop + ffmpeg)
ffmpeg wanted locally
mudroom
—
edition 2024
—
no CI
—
the-lab, liminal-hq.github.io
pnpm
—
—
none
Pages deploys
skills, liminal-hq-report
— (Python)
—
—
none
setup-python
prismatic, haptics-lab-app
docs only
—
—
none
future Godot / Tauri Android
Proposed image tree
Each layer adds exactly one concern; single inheritance throughout; every layer inherits the pinned Rust toolchain from one ARG RUST_TOOLCHAIN source of truth.
ci-base universal CLI only: ca-certificates, curl, wget, git, gh,
│ file, unzip, build-essential, pkg-config
│ (GTK/webkit/appindicator/rsvg/patchelf/xdg-utils/libssl-dev
│ move OUT of base, DOWN into the Tauri desktop tier)
│
├── ci-web + Node 24 + pnpm + Bun (one combined JS image)
│ NEW published image: ghcr.io/liminal-hq/ci-web
│ linux/amd64 + linux/arm64
│ ← smdu, coherence-chat-exporter, the-lab,
│ liminal-hq.github.io
│
└── ci-rust + rustup (pinned) + clippy + rustfmt + cargo-nextest
│ NEW published image: ghcr.io/liminal-hq/ci-rust
│ linux/amd64 + linux/arm64
│ ← flow, flicker, libhdmv, libvc1, mudroom
│
└── tauri-ci-desktop + Node 24 + pnpm + Bun (same runtime block as ci-web)
│ + GTK/webkit2gtk/appindicator/rsvg/libssl-dev
│ + patchelf/xdg-utils + tauri-cli
│ existing name and contract, now with Bun
│ ← spindle, emoji-nook, tauri-plugins-workspace,
│ threshold desktop (pending open question 1),
│ city-sim-1000
│
└── tauri-ci-mobile + Java 17 + Android SDK/NDK + Android Rust targets
existing name and contract, inherits Bun
← threshold (Android/Wear), foyer
Single inheritance means the JS runtime block appears twice (in ci-web off base, and again in the Tauri desktop tier off ci-rust). That duplication is deliberate: both sites install from the same ARG NODE_MAJOR / PNPM_VERSION / BUN_VERSION pins, so versions cannot drift, and it avoids COPY --from gymnastics or forcing pure-Rust images to carry Node.
The dev family mirrors the same tiers on the devcontainer base, keeping its existing contract (non-root vscode user, tool paths under $HOME, interactive QoL packages):
dev-base devcontainers base + universal CLI + interactive QoL
│ (vim, editors, etc. — GUI/Tauri system libs move DOWN
│ into the Tauri desktop tier, same eviction as CI)
│
├── dev-web + Node 24 + pnpm + Bun (user-home paths)
│ NEW published image: ghcr.io/liminal-hq/dev-web
│
└── dev-rust + rustup (pinned) + clippy + rustfmt + cargo-nextest
│ (user-home paths)
│ NEW published image: ghcr.io/liminal-hq/dev-rust
│ ← libvc1 local toolchain (adds ffmpeg on top),
│ pure-Rust repo devcontainers and host-side toolbox use
│
└── tauri-dev-desktop + JS runtimes + GUI libs + GStreamer/xvfb/emoji fonts
│ + tauri-cli — existing name and contract, now with Bun
│
└── tauri-dev-mobile + Java 17 + Android SDK/NDK under $HOME
existing name and contract, inherits Bun
Why this shape and not deeper splits
No ci-rust-bun / ci-rust-pnpm fork. The survey shows the Rust+JS combos are all Tauri repos, and both runtimes together cost ~200–300 MB — trivial next to the GTK stack (~1.5 GB) and Android SDK (~4 GB). Carrying both runtimes in the Tauri tier removes the fork entirely and fixes foyer's per-run Bun install. It also avoids the strawman's structural bug: with a Tauri+Bun consumer (foyer), a Tauri image anchored to a pnpm-only branch cannot serve it.
One ci-web, not ci-bun / ci-node-pnpm. The JS-only repos mix runtimes within a single repo (pnpm or npm for deps, Bun for compiled release binaries), so per-runtime images would force most of them to pick wrong. One combined image carries Node + pnpm + Bun for a few hundred MB total and serves all four intended adopters. Note the containers apply to the Linux legs only — smdu's and coherence's macOS/Windows release jobs stay on native runners, and Pages deploy actions keep working inside container jobs.
Existing names stay.tauri-ci-desktop, tauri-ci-mobile, tauri-dev-desktop, and tauri-dev-mobile keep their names, env models, tags, and smoke contracts, so current consumers migrate by doing nothing (foyer can simply delete its Bun install step). Renaming to a uniform scheme can be a separate follow-up if wanted.
Dev tiers mirror CI tiers. Both families already build from one Dockerfile with shared ARG pins and one workflow; keeping the tier structure symmetric preserves that alignment and gives host-side toolbox use right-sized images — a Rust toolbox without 1.5 GB of GUI/Node, a JS toolbox without rustup. New dev images follow the existing dev platform contract (linux/amd64 today; ARM remains the documented follow-up).
Consumer migration map
Repo
Moves to
Wins
flow, flicker, libhdmv, libvc1, mudroom
ci-rust (new adoption)
pinned toolchain + nextest + gh preinstalled; no GUI/Node dead weight; arm64 variant for flow's ARM releases
pinned Node/pnpm/Bun without per-run setup actions; arm64 variant covers smdu's ubuntu-24.04-arm binary job; macOS/Windows release legs stay on native runners
city-sim-1000
tauri-ci-desktop
drops per-run apt webkit stack; Bun and nextest preinstalled
foyer
tauri-ci-mobile (stays)
deletes the per-run Bun curl-install
threshold (desktop jobs)
tauri-ci-desktop (pending open question 1)
drops the large per-run host apt install
spindle, emoji-nook, tauri-plugins-workspace
tauri-ci-desktop (stay)
no change required; smaller effective delta on rebuilds
threshold, flow, city-sim-1000 workflows
n/a
can drop taiki-e/install-action@nextest steps
libvc1 local toolchain container
dev-rust (+ repo-local ffmpeg layer)
drops the full GUI/Node/Tauri stack it inherits from tauri-dev-desktop today
pure-Rust and JS-only repo devcontainers / host toolbox use
dev-rust / dev-web
right-sized interactive images with the established non-root, $HOME-path ergonomics
Open questions
GStreamer + xvfb + emoji fonts in tauri-ci-desktop? threshold is the only consumer that needs them, but it is the org's most complex repo and today its desktop jobs cannot use the image at all. The dev desktop image already carries them, so adding them to the CI desktop tier aligns the families. Recommendation: add them; revisit if image size becomes a concern.
Rust version policy. Images pin 1.93.0 while consumers pin 1.94 and 1.96.1. Options: (a) bump the single org pin and have repos align; (b) publish version-suffixed tags (e.g. ci-rust:1.96) alongside latest; (c) rely on rustup honouring each repo's rust-toolchain.toml at runtime (costs a per-run toolchain download, defeating part of the purpose). Recommendation: (a) + keep the existing sha-/date tags for pinning; consider (b) only if repos genuinely need to straddle versions.
wasm tooling (wasm32-unknown-unknown, wasm-pack) — only city-sim-1000 needs it. Recommendation: leave per-repo (rustup target add is cheap on top of ci-rust-based images).
Publish workflow scale. Mirroring the tiers takes the publish matrix from 4 images to 8, each with its own buildcache ref and smoke profile. Options: keep one matrix (simplest, more parallel jobs per publish) or split CI and dev families into separate workflows sharing the Dockerfile. Recommendation: keep one matrix until publish duration or cache behaviour becomes a problem; the bi-weekly cadence keeps total cost modest.
Interactive QoL placement in the dev tiers. Which of dev-base's extras (vim, xvfb, GStreamer, emoji fonts) are universal QoL versus Tauri-desktop concerns that should move down with the GUI libs? Improve the shared Tauri desktop dev image for local validation and Linux desktop debugging #10's wishlist (ripgrep, fd, jq, X11 inspection tools) lands naturally in dev-base if it is truly universal. Recommendation: xvfb/GStreamer/fonts move to the Tauri desktop tier; editor/search/JSON tools stay in dev-base.
Acceptance criteria
docker/ci/Dockerfile re-layered for both families: ci-base and dev-base contain universal tooling only; GUI/Tauri system libs live in the desktop tiers; each stage adds one concern.
ghcr.io/liminal-hq/ci-rust published (linux/amd64 + linux/arm64) with smoke checks (cargo, rustup, clippy, rustfmt, cargo-nextest, gh) and its own buildcache ref.
ghcr.io/liminal-hq/ci-web published (linux/amd64 + linux/arm64) with smoke checks (node, pnpm, bun, gh) and its own buildcache ref, with Node/pnpm/Bun pins shared with the Tauri tier via common ARGs.
ghcr.io/liminal-hq/dev-rust and ghcr.io/liminal-hq/dev-web published (linux/amd64, matching the dev family platform contract) with dev-style smoke checks: non-root user, tool availability, $HOME tool paths set and writable.
Dev family env model preserved (HOME=/home/vscode, CARGO_HOME=$HOME/.cargo, PNPM_HOME=$HOME/.local/share/pnpm, Android under $HOME/Android/Sdk), and version pins stay aligned between CI and dev tiers via shared ARGs.
Bun added to tauri-ci-desktop and tauri-ci-mobile, pinned like Node/pnpm, covered by smoke checks.
Existing published image names, env model (CARGO_HOME=/usr/local/cargo etc.), tag policy, and platform coverage preserved for current consumers.
Open questions 1–2 and 5 decided and recorded in the reference docs.
docs/tracking/cross-repo-ci-alignment.md and Track cross-repo CI image and quality-gate normalisation #3 updated with the new adoption targets (pure-Rust repos, JS-only repos onto ci-web, city-sim-1000, foyer cleanup, threshold desktop migration).
Non-goals
Per-runtime JS images (ci-bun, ci-node-pnpm) — the combined ci-web covers all current JS consumers; split only if a real conflict appears.
Baking ffmpeg into shared images (libvc1's local validation want — keep repo-local).
Python/Godot lanes (skills, prismatic) — bare runners serve them fine.
Emulator tooling, automatic downstream rollout, per-repo cache mounts (unchanged from the existing spec's out-of-scope list).
Summary
The shared CI image family is effectively monolithic.
ci-basebakes in the full Tauri desktop stack (GTK, webkit2gtk, appindicator, rsvg, patchelf, xdg-utils, libssl-dev), andci-toolchainpiles Node 24 + pnpm + Rust + cargo-nextest + tauri-cli into one stage. Every consumer inherits everything: a pure-Rust codec library pulls roughly 1.5 GB of GUI and Node tooling it never uses, and there is no image a Rust-only repo can adopt without that overhead.A toolchain survey of all active Liminal HQ repos plus
ScottMorris/city-sim-1000(2026-07-22) shows the org's real toolchain clusters. This issue proposes re-layering the shared Dockerfile into granular tiers where each layer adds exactly one concern, publishing two new CI images (ci-rustfor pure-Rust repos andci-webfor JavaScript/TypeScript repos), mirroring the same tiers in the dev image family (dev-rust,dev-web), and adding Bun to the Tauri images — while preserving the existing published image names and contracts.The dev family is a first-class deliverable here, not a byproduct: the
tauri-dev-*images are actively used on desktops and laptops as portable toolboxes for running tools that are not installed on the host, and today they are built from the same Dockerfile and workflow as the CI images. The granular tiers need to exist on both sides.What the survey found
thresholdandfoyerusetauri-ci-mobile;spindle,emoji-nook, andtauri-plugins-workspaceusetauri-ci-desktop. Everything else runs on plain runners with setup actions.foyer(Tauri desktop + Android,bun.lock) curl-installs Bun intotauri-ci-mobileon every CI run because the image only ships pnpm.city-sim-1000(Rust + Bun, Tauri plugin crate) apt-installs webkit2gtk/appindicator/rsvg on plain runners every run. Bun is a house standard beyond that:smduandcoherence-chat-exporterboth usebun build --compilefor release binaries.smdu,coherence-chat-exporter,the-lab, andliminal-hq.github.ioall usesetup-node/setup-bun/pnpm actions because the shared images were originally Tauri-focused, not because containers do not fit. They are intended adopters of a JS tier. Their runtime usage is mixed (pnpm, npm, and Bun in combination —smduuses pnpm for deps and Bun to compile release binaries;coherence-chat-exporteruses npm plus Bun compile), which argues for one combined JS image rather than per-runtime forks.flow,flicker,libhdmv,libvc1, andmudroomneed only a pinned Rust toolchain, clippy/rustfmt, cargo-nextest, andgh— none of the GUI or Node stack.thresholddesktop jobs cannot use the CI desktop image. They apt-install the full GTK stack plus GStreamer (base + bad),fonts-noto-color-emoji, andxvfbon host runners each run; the CI desktop image lacks those last three (the dev desktop image already has them).taiki-e/install-actioninthreshold,flow, andcity-sim-1000even though the shared images already bundle it.thresholdpins 1.96.1 andcity-sim-1000/flowpin 1.94.threshold,spindle,emoji-nook,flow,smdu,coherence-chat-exporter, andcity-sim-1000all runubuntu-*-armjobs.tauri-dev-*images see regular interactive use on desktops/laptops to run tools that are not installed on the host.libvc1demonstrates the granularity gap on this side too: its localdocker/Dockerfile.toolchainbases ontauri-dev-desktop(plus ffmpeg) just to get a Rust toolchain, inheriting the entire GUI/Node/Tauri stack it never uses.Condensed toolchain matrix (survey, 2026-07-22)
tauri-ci-mobile(Android/Wear only)tauri-ci-desktoptauri-ci-desktoptauri-ci-mobiletauri-ci-desktoptauri-dev-desktop+ ffmpeg)Proposed image tree
Each layer adds exactly one concern; single inheritance throughout; every layer inherits the pinned Rust toolchain from one
ARG RUST_TOOLCHAINsource of truth.Single inheritance means the JS runtime block appears twice (in
ci-weboff base, and again in the Tauri desktop tier offci-rust). That duplication is deliberate: both sites install from the sameARG NODE_MAJOR/PNPM_VERSION/BUN_VERSIONpins, so versions cannot drift, and it avoidsCOPY --fromgymnastics or forcing pure-Rust images to carry Node.The dev family mirrors the same tiers on the devcontainer base, keeping its existing contract (non-root
vscodeuser, tool paths under$HOME, interactive QoL packages):Why this shape and not deeper splits
ci-rust-bun/ci-rust-pnpmfork. The survey shows the Rust+JS combos are all Tauri repos, and both runtimes together cost ~200–300 MB — trivial next to the GTK stack (~1.5 GB) and Android SDK (~4 GB). Carrying both runtimes in the Tauri tier removes the fork entirely and fixes foyer's per-run Bun install. It also avoids the strawman's structural bug: with a Tauri+Bun consumer (foyer), a Tauri image anchored to a pnpm-only branch cannot serve it.ci-web, notci-bun/ci-node-pnpm. The JS-only repos mix runtimes within a single repo (pnpm or npm for deps, Bun for compiled release binaries), so per-runtime images would force most of them to pick wrong. One combined image carries Node + pnpm + Bun for a few hundred MB total and serves all four intended adopters. Note the containers apply to the Linux legs only — smdu's and coherence's macOS/Windows release jobs stay on native runners, and Pages deploy actions keep working inside container jobs.tauri-ci-desktop,tauri-ci-mobile,tauri-dev-desktop, andtauri-dev-mobilekeep their names, env models, tags, and smoke contracts, so current consumers migrate by doing nothing (foyer can simply delete its Bun install step). Renaming to a uniform scheme can be a separate follow-up if wanted.ARGpins and one workflow; keeping the tier structure symmetric preserves that alignment and gives host-side toolbox use right-sized images — a Rust toolbox without 1.5 GB of GUI/Node, a JS toolbox without rustup. New dev images follow the existing dev platform contract (linux/amd64today; ARM remains the documented follow-up).Consumer migration map
ci-rust(new adoption)ci-web(new adoption, Linux jobs)ubuntu-24.04-armbinary job; macOS/Windows release legs stay on native runnerstauri-ci-desktoptauri-ci-mobile(stays)tauri-ci-desktop(pending open question 1)tauri-ci-desktop(stay)taiki-e/install-action@nexteststepsdev-rust(+ repo-local ffmpeg layer)tauri-dev-desktoptodaydev-rust/dev-web$HOME-path ergonomicsOpen questions
tauri-ci-desktop? threshold is the only consumer that needs them, but it is the org's most complex repo and today its desktop jobs cannot use the image at all. The dev desktop image already carries them, so adding them to the CI desktop tier aligns the families. Recommendation: add them; revisit if image size becomes a concern.ci-rust:1.96) alongsidelatest; (c) rely on rustup honouring each repo'srust-toolchain.tomlat runtime (costs a per-run toolchain download, defeating part of the purpose). Recommendation: (a) + keep the existingsha-/date tags for pinning; consider (b) only if repos genuinely need to straddle versions.wasm32-unknown-unknown, wasm-pack) — only city-sim-1000 needs it. Recommendation: leave per-repo (rustup target addis cheap on top ofci-rust-based images).dev-baseif it is truly universal. Recommendation: xvfb/GStreamer/fonts move to the Tauri desktop tier; editor/search/JSON tools stay indev-base.Acceptance criteria
docker/ci/Dockerfilere-layered for both families:ci-baseanddev-basecontain universal tooling only; GUI/Tauri system libs live in the desktop tiers; each stage adds one concern.ghcr.io/liminal-hq/ci-rustpublished (linux/amd64+linux/arm64) with smoke checks (cargo,rustup,clippy,rustfmt,cargo-nextest,gh) and its own buildcache ref.ghcr.io/liminal-hq/ci-webpublished (linux/amd64+linux/arm64) with smoke checks (node,pnpm,bun,gh) and its own buildcache ref, with Node/pnpm/Bun pins shared with the Tauri tier via commonARGs.ghcr.io/liminal-hq/dev-rustandghcr.io/liminal-hq/dev-webpublished (linux/amd64, matching the dev family platform contract) with dev-style smoke checks: non-root user, tool availability,$HOMEtool paths set and writable.HOME=/home/vscode,CARGO_HOME=$HOME/.cargo,PNPM_HOME=$HOME/.local/share/pnpm, Android under$HOME/Android/Sdk), and version pins stay aligned between CI and dev tiers via sharedARGs.tauri-ci-desktopandtauri-ci-mobile, pinned like Node/pnpm, covered by smoke checks.CARGO_HOME=/usr/local/cargoetc.), tag policy, and platform coverage preserved for current consumers.README.md,docs/reference/shared-image-layout.md,docs/reference/shared-image-implementation-spec.md,docs/runbooks/image-publish-and-rollback.md.docs/tracking/cross-repo-ci-alignment.mdand Track cross-repo CI image and quality-gate normalisation #3 updated with the new adoption targets (pure-Rust repos, JS-only repos ontoci-web, city-sim-1000, foyer cleanup, threshold desktop migration).Non-goals
ci-bun,ci-node-pnpm) — the combinedci-webcovers all current JS consumers; split only if a real conflict appears.skills,prismatic) — bare runners serve them fine.Related
docs/reference/shared-image-implementation-spec.md,docs/reference/shared-image-layout.md