feat: build clients locally on a published base #56

Merged
claudiomaradonna merged 2 commits from feat/local-client-layer into master 2026-10-26 20:50:42 +01:00
Collaborator

Why

publish-images has never published anything. On the arm64 runner, the
amd64 build of Claude Code runs claude install, a Bun executable,
under qemu-x86_64. It aborts with
ASSERTION FAILED: MemoryExhaustion (exit 134). This is a known Bun 1.4
limitation under qemu-user that its maintainers will not fix
(https://github.com/oven-sh/bun/issues/41087). OpenCode is a Bun
executable too. Meanwhile installers and launches on Linux amd64 and
arm64 pull client images that do not exist.

The base stage builds fine under emulation, so this change splits the
images: the forge publishes only the shared base, and every host builds
its client layer on it natively. Two alternatives were set aside. A
second native amd64 runner needs infrastructure. Installing Claude Code
without running it would copy Anthropic's download layout into the
Dockerfile and still leave OpenCode exposed.

What changed

The workflow builds the Dockerfile's base target from scratch for
linux/amd64 and linux/arm64 and publishes
git.unitoo.it/claudiomaradonna/azkaban-base:latest. No client is
built or run on the runner. Each nightly run produces a new base.

The client targets now start FROM ${BASE_IMAGE}. Each records the
base's ID in an azkaban.base label. On Linux amd64 and arm64:

  • Install: ./install.sh pulls the base and builds the selected
    clients on it, with their version tags, before it touches the
    installation.
  • Launch: each launch pulls the base. When the base is not the one
    the client was built on (once after each nightly build), it rebuilds
    the client from the installed release's containers/Dockerfile, now
    part of the release. That takes about a minute; otherwise the client
    starts at once. A failed pull or rebuild warns and runs the local
    image.

macOS, other architectures, and --build, --tz or --no-cache build
the base locally too. AZKABAN_AUTO_PULL=0 keeps the local image. The
docs state a new rule: the published base keeps what installed
releases' client stages use (npm, curl, a free UID/GID 1000).

How to verify

  1. Run publish-images from the Actions tab on this branch. It should
    end green and publish azkaban-base with latest, latest-amd64
    and latest-arm64.
  2. On a Linux amd64 or arm64 host with rootless Podman, run
    ./install.sh claude from this branch. Expect
    Pulling the base image git.unitoo.it/claudiomaradonna/azkaban-base:latest...,
    then Building claude image claude-code:latest....
    podman image inspect --format '{{index .Config.Labels "azkaban.base"}}' claude-code:latest
    should print the ID of localhost/azkaban-base:latest.
  3. Launch claude .. It pulls the base and starts without rebuilding.
    After the next nightly run, the first launch prints
    claude: the base changed; rebuilding claude-code:latest.

make lint && .agents/hooks/shellcheck.sh repo && make test passes on
each commit. The rewritten publish, install, launch and Dockerfile tests
fail on master. Not tried: the workflow on the runner, a real install
or launch with Podman, and make test-e2e.

Considerations

The first launch after each nightly base takes about a minute per
client, even when the client itself has not released anything. One low
review finding was set aside: an assertion label in
tests/unit/test_containerfile.sh still says "timezone argument" for a
client, where the build now passes the timezone only to the base.

## Why `publish-images` has never published anything. On the arm64 runner, the amd64 build of Claude Code runs `claude install`, a Bun executable, under qemu-x86_64. It aborts with `ASSERTION FAILED: MemoryExhaustion` (exit 134). This is a known Bun 1.4 limitation under qemu-user that its maintainers will not fix (https://github.com/oven-sh/bun/issues/41087). OpenCode is a Bun executable too. Meanwhile installers and launches on Linux amd64 and arm64 pull client images that do not exist. The base stage builds fine under emulation, so this change splits the images: the forge publishes only the shared base, and every host builds its client layer on it natively. Two alternatives were set aside. A second native amd64 runner needs infrastructure. Installing Claude Code without running it would copy Anthropic's download layout into the Dockerfile and still leave OpenCode exposed. ## What changed The workflow builds the Dockerfile's base target from scratch for linux/amd64 and linux/arm64 and publishes `git.unitoo.it/claudiomaradonna/azkaban-base:latest`. No client is built or run on the runner. Each nightly run produces a new base. The client targets now start `FROM ${BASE_IMAGE}`. Each records the base's ID in an `azkaban.base` label. On Linux amd64 and arm64: - **Install:** `./install.sh` pulls the base and builds the selected clients on it, with their version tags, before it touches the installation. - **Launch:** each launch pulls the base. When the base is not the one the client was built on (once after each nightly build), it rebuilds the client from the installed release's `containers/Dockerfile`, now part of the release. That takes about a minute; otherwise the client starts at once. A failed pull or rebuild warns and runs the local image. macOS, other architectures, and `--build`, `--tz` or `--no-cache` build the base locally too. `AZKABAN_AUTO_PULL=0` keeps the local image. The docs state a new rule: the published base keeps what installed releases' client stages use (`npm`, `curl`, a free UID/GID 1000). ## How to verify 1. Run `publish-images` from the Actions tab on this branch. It should end green and publish `azkaban-base` with `latest`, `latest-amd64` and `latest-arm64`. 2. On a Linux amd64 or arm64 host with rootless Podman, run `./install.sh claude` from this branch. Expect `Pulling the base image git.unitoo.it/claudiomaradonna/azkaban-base:latest...`, then `Building claude image claude-code:latest...`. `podman image inspect --format '{{index .Config.Labels "azkaban.base"}}' claude-code:latest` should print the ID of `localhost/azkaban-base:latest`. 3. Launch `claude .`. It pulls the base and starts without rebuilding. After the next nightly run, the first launch prints `claude: the base changed; rebuilding claude-code:latest`. `make lint && .agents/hooks/shellcheck.sh repo && make test` passes on each commit. The rewritten publish, install, launch and Dockerfile tests fail on `master`. Not tried: the workflow on the runner, a real install or launch with Podman, and `make test-e2e`. ## Considerations The first launch after each nightly base takes about a minute per client, even when the client itself has not released anything. One low review finding was set aside: an assertion label in `tests/unit/test_containerfile.sh` still says "timezone argument" for a client, where the build now passes the timezone only to the base.
Claude Code and OpenCode are Bun executables, which crash under the
runner's qemu-x86_64 emulation
(https://github.com/oven-sh/bun/issues/41087), so no client image could
be published. The workflow now publishes only the base image, rebuilt
from scratch for linux/amd64 and linux/arm64 each run, and every host
builds its clients on it natively.

The client targets start from BASE_IMAGE and record the base's ID in an
azkaban.base label. On Linux amd64 and arm64 the installer pulls the
base and builds the selected clients on it; elsewhere, and with
--build, --tz, or --no-cache, it builds the base too. The release now
holds containers/Dockerfile, from which a launch rebuilds its client
when the pulled base differs from the one the client was built on.
docs: describe the published base and local clients
All checks were successful
publish-images / publish (push) Successful in 37m44s
991904f09e
The README tells that hosts build their clients on a published base,
when a launch rebuilds a client, and how long it takes; the development
and client guides cover the base target, the base-only workflow, and
the label a client target carries.
claudiomaradonna deleted branch feat/local-client-layer 2026-10-26 20:50:47 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/azkaban!56
No description provided.