This repository builds a backend-specialized MeshLLM CI image family from one shared core toolchain. Every native-runtime backend is available as a GitHub-hosted job container (public) and an image containing the GitHub Actions runner (self-hosted):
public-cpu-*/self-hosted-cpu-*public-vulkan-*/self-hosted-vulkan-*public-cuda12-*/self-hosted-cuda12-*public-cuda13-*/self-hosted-cuda13-*public-rocm70-*/self-hosted-rocm70-*public-rocm72-*/self-hosted-rocm72-*
CPU, Vulkan, and CUDA tags support linux/amd64 and linux/arm64. ROCm tags are intentionally linux/amd64 only because that is the currently supported MeshLLM ROCm CI target. The compatibility aliases preserve the existing image contract: public-* is CPU on both architectures, while self-hosted-* combines CUDA 12 on AMD64 with CPU on ARM64. The existing GHCR package name is retained to avoid a registry and credential migration.
The web backend is public-web-* only — no self-hosted-web exists, since nothing consumes a self-hosted web image today. It is linux/amd64 only for the same reason. Unlike the native-runtime backends above, web bakes a specific Chromium build for MeshLLM's Playwright-based UI end-to-end suite rather than a compiler/runtime toolchain; see "Playwright / Chromium version pin" below.
The image has four layers of configuration:
profiles/common.ymlis the shared operating-system toolchain found in MeshLLM CI and build scripts.profiles/backends/*.ymlcontains CPU, Vulkan, CUDA, ROCm, or Web SDK packages; the owning installer handles vendor repositories and compilers.web's stays an emptyapt.packageslist — Chromium's system dependencies areplaywright install-deps chromium's to own, not ours; see below.profiles/public.ymlandprofiles/self-hosted.ymlcontain environment-only additions.scripts/prepare-build-context.shchecks out the requested MeshLLM revision, discovers its Rust, Node, Python, and Go manifests, and creates one bundle per runner environment. The Docker build injects the matching bundle and warms Cargo, pnpm, npm, and Python dependencies.
The YAML profiles use a deliberately small schema (schema, profile, and apt.packages) that is parsed by portable Bash without Python or Ruby. The manifest bundle is content-addressed in manifest-index.json. Cargo target stubs retain the complete workspace graph without copying or publishing MeshLLM source code in the runner image.
The toolchain stage builds from ACTIONS_RUNNER_BASE_IMAGE, whose default is
ghcr.io/actions/actions-runner:latest pinned by digest in the Dockerfile. It
provides the GitHub Actions runner agent, the runner user (uid 1001), the
docker group (gid 123), /usr/bin/docker (Docker CLI, no daemon), and Node 20
and 24 runtimes under /home/runner/externals/node{20,24}/bin.
scripts/install-core-tools.sh wires Node from these externals into
/usr/local/bin instead of installing Node from an apt repository;
docker-ce-cli is no longer apt-installed because the base already ships a
compatible Docker client.
Trusted staging/promotion may replace only the registry/repository portion with
a Depot Registry pull-through mirror while retaining the exact upstream digest.
The opt-in gate, repository mapping, short-lived pull-token authentication, and
rollback procedure are documented in docs/OPERATIONS.md. Validation and pull
request builds always use the canonical upstream reference.
Both final stages provision the GitHub Actions root-system conventions required when the image is used as a job container: (DinD sidecar, rootless Podman, or any setup where the runner cannot rely on kubelet bind mounts):
/__e/node24→ symlinked to/home/runner/externals/node24/__e/node20→ symlinked to/home/runner/externals/node20/__w,/github/home,/github/workflow→ directories with mode 0777
The public stage bakes these as USER root (matching the public-container root convention); the self-hosted stage bakes them before restoring USER runner so the runner agent's run.sh entrypoint still executes with uid 1001. scripts/verify-runner-image.sh asserts both the /__e/node24/bin/node symlink and its canonical source under /home/runner/externals/, plus docker --version, so regressions of the /__e/node24/bin/node: no such file or directory failure mode are caught at build time.
config/playwright-pin.txt is the single declared source of truth for the
Chromium build baked into public-web. The backend-web Dockerfile stage
installs the playwright package at that exact version, runs playwright install-deps chromium (so Playwright's own dependency list drives apt, not a
hand-maintained one) and playwright install chromium into a fixed
PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright, and records what it actually
got in /etc/mesh-runner-playwright-version and /etc/mesh-runner-chromium-build
(the latter read from the installed browser directory name, not hand-written).
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 is baked as an image env var so a stray
pnpm install postinstall never re-downloads a browser into a running
container; Playwright hard-errors instead of downloading when a browser is
missing (scripts/verify-runner-image.sh's web case proves this by
launching Chromium headless, offline, and closing it).
This image is the stable side of the version pairing: it does not read
MeshLLM's pnpm-lock.yaml at build time, and MeshLLM does not re-derive its
pin from this image. Each side asserts the other's value independently —
verify-runner-image takes an optional expected Playwright version argument,
and MeshLLM's own CI checks its @playwright/test resolution against
/etc/mesh-runner-playwright-version before trusting the image. Bumping the
pin has a mandatory order (this repo promotes first, MeshLLM bumps its
lockfile second); see docs/OPERATIONS.md.
scripts/prepare-build-context.sh /Users/ndizazzo/dev/mesh/mesh-llm
docker buildx build \
--platform linux/amd64 \
--target public \
--build-arg ACTIONS_RUNNER_BASE_IMAGE=ghcr.io/actions/actions-runner:latest@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda \
--build-arg BACKEND=cpu \
--build-arg RUNNER_ENVIRONMENT=public \
--build-arg MESH_LLM_REVISION="$(git -C /Users/ndizazzo/dev/mesh/mesh-llm rev-parse HEAD)" \
--build-arg RUNNER_IMAGES_REVISION="$(git rev-parse HEAD)" \
--load \
-t mesh-llm-runner:public .
docker run --rm --entrypoint verify-runner-image mesh-llm-runner:public public cpuUse target self-hosted and RUNNER_ENVIRONMENT=self-hosted for an image that includes the GitHub Actions runner. Select BACKEND=cpu|vulkan|cuda|rocm; CUDA additionally accepts CUDA_SERIES, while ROCm accepts ROCM_VERSION and currently requires AMD64.
.github/workflows/build-and-push.yml runs on pull requests, pushes to main, a weekly schedule, and manual dispatch. It:
- validates the workflow and immutable-candidate contract before expensive work;
- checks out the requested MeshLLM ref and generates both manifest bundles;
- builds affected pull-request families plus one always-on public CPU AMD64 contract row, while trusted main and scheduled runs remain exhaustive;
- verifies each trusted staged image through its exact registry digest, then assembles and validates one immutable index descriptor per family;
- promotes timestamp, MeshLLM compatibility, and digest-derived immutable tags from those descriptors without rebuilding;
- records the complete previous/target
latestmap, then reconciles the eventuallatestcohort from that retained manifest.
Execution is explicit: validate builds test targets without registry writes, stage pushes and verifies run-scoped candidates without moving production aliases, and promote performs the same staging before versioned and eventual-latest reconciliation. Manual dispatch defaults to validate; any staging or promotion requires the exact repository, refs/heads/main, and the default-branch caller workflow. Main pushes stage candidates, while the weekly schedule performs the deliberate production promotion.
Pull requests receive no package-write permission and run only affected
families plus the public CPU AMD64 contract row. Every platform build uses
Depot remote BuildKit and its persistent project-scoped cache; no job imports
or exports type=gha cache archives. Public fork builds are isolated from the
project cache by Depot. Trusted staging and promotion push immutable platform
digests directly from the remote builder to GHCR, while GHCR remains the
canonical registry. The checked-in Depot project configuration and required
OIDC trust setup are documented in docs/OPERATIONS.md.
Supported environments, backends, architectures, compatibility aliases, and extra indexes are declared in config/runner-image-families.json; scripts/generate-workflow-matrices.sh validates that descriptor and generates both build and promotion matrices.
Pull requests always use native GitHub-hosted ubuntu-24.04 and ubuntu-24.04-arm runners. The policy job is also fixed to GitHub-hosted Ubuntu and selects the downstream provider. The called family workflow independently derives literal runner labels and permits Depot only for the exact repository, default-branch caller workflow, refs/heads/main, a trusted event, and DEPOT_RUNNERS_ENABLED=true. It accepts no caller-provided runner JSON. All mismatches fall back to GitHub-hosted labels or fail before checkout.
The weekly default-branch publication is intentionally Depot-eligible when that gate is enabled: it is a trusted, high-value full image rebuild. Scheduled runs on any other ref remain GitHub-hosted.
The runner gate is independent of container builds: GitHub-hosted pull-request runners still dispatch Docker builds to Depot. Depot isolates public fork builds from the project cache, while authorized same-repository builds share the persistent content-addressed cache.
Published tags are:
<environment>-<backend>-latest<environment>-<backend>-YYYYMMDDHHMMSSfor discovery and evaluation<environment>-<backend>-sha-<12-character MeshLLM revision>is a mutable compatibility alias and may move when runner-image inputs change<environment>-<backend>-digest-sha256-<64-hex manifest digest>is the collision-proof immutable content identity and refuses conflicting overwrite
MeshLLM and runner-images source revisions remain full OCI labels and candidate-descriptor fields. They are not used as immutable tag identity because provenance timestamps and resolved package inputs can legitimately change content for the same source pair. Production consumers resolve a selected tag and pin its immutable manifest digest; tags are not the production contract.
examples/workflows/public-github-hosted.ymlruns onubuntu-24.04with the public image through job-levelcontainer:.scripts/verify-end-to-end.shverifies the registry manifest lists and executes every supported architecture.
See docs/AUDIT.md for the source audit and docs/OPERATIONS.md for publication and registry-verification steps.