Files
bot/hanzo.yml
zandGitHub 45a5512296 Delete the duplicate cloud and deploy lanes
Both ran buildx on a runner with no Docker daemon and published nothing.

Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
2026-09-24 10:07:01 -07:00

183 lines
9.1 KiB
YAML

# Canonical CI/CD config for hanzoai/bot — the one file both the hanzoai/ci
# reusable (.hanzo/workflows/cicd.yml) and platform.hanzo.ai read.
# ONE Dockerfile, FOUR tags, one chain that forks once:
#
# exec -> dev -> desktop
# dev -> admin
#
# Three of the four are a sandbox CLASS a caller can ask for. `admin` is not:
# hanzoai/cloud hands it to a SuperAdmin's `dev` sandbox and nobody else's, so
# it is a tag with no word in the request. It is built and published exactly
# like its siblings — same file, same repo, one more `tag-suffix` — because
# which identity receives an image is a decision for the consumer, not for a
# build lane.
#
# THESE FOUR, AND ONLY THESE FOUR. The npm package has release.yml. The bot and
# bot-cloud images have no lane: their buildx lanes (deploy.yml, cloud.yml) are
# gone, and the bot Dockerfile needs dist/control-ui assembled before it builds,
# which the build door does not do. The box recipes are the ones with no lane at
# all — which is why no box image has ever existed anywhere in the org — so this
# is the whole list, not a start on one.
#
# hanzoai/ci's image lane has no `target:` field — it runs
# `docker buildx build -f "$df" "$ctx"`, which builds the LAST stage. So
# Dockerfile.sandbox ends with `FROM ${STAGE} AS final`, an alias, and the class is
# selected here. BuildKit only builds the stages the target depends on, so
# STAGE=exec never installs a browser, and the local builder passes the same arg
# — one selection mechanism, not one for CI and one for a laptop.
#
# `tag-suffix` is what makes three entries share one repo: the tags come out as
# sandbox:sha-<short>-amd64-exec / -dev / -desktop, beside sandbox:exec-latest
# and the imgver semver, and a deployment pins the digest.
#
# THE REPO IS hanzoai/sandbox, NOT hanzoai/box, BECAUSE A CONSUMER NAMES IT.
# hanzoai/cloud apps/sandbox/runtime.go defaults SANDBOX_IMAGE_REPO to
# `oci.hanzo.ai/hanzoai/sandbox` and builds `<repo>:<class>[-<tag>]` from it. An
# image path is only correct relative to who pulls it, and nothing pulls
# `hanzoai/box`: the daemon design that owned that name was deleted from cloud
# along with cmd/boxd. One artifact, one path, named by the thing that fetches it.
#
# `repo:` NAMES THE REGISTRY DIRECTLY rather than ghcr.io. Every other repo in
# the fleet pushes to ghcr and lets the lane's crane step mirror across, but that
# mirror rewrites the org — ghcr.io/hanzoai/x lands at <registry>/hanzo/x — and
# `hanzo/sandbox` is not the path cloud pulls. The lane logs into the host named
# here before it builds, so this is a direct push, not a special case.
#
# BOTH ARCHES, BECAUSE THE FLEET NOW HAS BOTH. This entry read amd64-only, and
# the reason it gave was arithmetic rather than anything in the file: DOKS had no
# arm64 node, so arm64 bought nothing that runs. It also promised its own remedy —
# "an arm64 node makes this one word here, not a rebuild" — and that is what this
# is. The fleet's first arm64 node (spark, GB10, 20 cores) is registered and
# carries the `linux-arm64` label hanzoai/ci's build lane names.
#
# Nothing in the image ever blocked it. Every toolchain here RESOLVES its own
# arch and downloads a prebuilt binary — node, go, bun, deno, zig, gh each carry
# an `arm64)` case with its own pinned sha256 — so the old note's "unpacks and
# compiles a Go + Rust + JDK + Zig + Bun + Deno toolchain" overstated the cost of
# emulating it: what QEMU actually runs here is apt, curl and tar, not a compiler.
# A stage that genuinely could not cross is `android`, and it says so on its own
# entry rather than holding the other three back.
#
# The consumer's tag does not move. A multi-arch entry emits ONE manifest list
# per tag and containerd picks the native manifest per node, so `<sfx>-latest`,
# the semver and every pinned digest mean what they meant — with the digest now
# naming an index rather than one image, which is the whole point: spark could
# pull the old digest and then answer `exec format error`, a pod that starts and
# cannot run its own shell.
#
# QEMU is the FLOOR, not the ceiling. The lane installs binfmt and runs one
# `buildx build --platform`, so today's arm64 half is emulated on the amd64
# runner. Native is a buildx builder with an arm64 NODE appended — ci's own
# comment names that as the next step and its precondition is already met.
# THE IMAGE'S OWN VERSION, AND THE REASON IT NEEDS ONE.
#
# hanzoai/ci derives every published version with bin/imgver, which reads
# `$IMGVER_VERSION` — this key — and falls back to the build context's manifest.
# The context is the repo root, so the fallback was package.json: BOT's version,
# 2026.6.7, last bumped 2026-06-07. Today's rebuild therefore republished
# `2026.6.7-desktop` from source two months newer, and `2026.6.7-desktop` and
# `sha-d190451-amd64-desktop` are now literally one digest (sha256:7a41f69f…).
# A tag that gets rewritten is not a pin. One that LOOKS pinned is worse than
# `latest`, because `latest` at least admits what it is — which is exactly why
# hanzoai/cloud had to fall back to pinning sandbox images by digest.
#
# So the number comes from docker/sandbox.version: a file whose only subject is
# this image, changed in the same commit that changes the image.
# `<file>:<command printing it>` is imgver's own shape — hanzoai/cli's hanzo.yml
# reads Cargo.toml the same way. Dockerfile.sandbox COPYs that same file to
# /etc/sandbox-version, so a running pod can be asked what it is and cannot
# disagree with its own tag.
#
# It starts at 1.0.0 rather than continuing 2026.6.x, because 2026.6.7 was never
# this artifact's number — it was another package's, borrowed. The old tags stay
# in the registry; nothing that pins a digest notices either way.
version: "docker/sandbox.version:cat docker/sandbox.version"
images:
- name: sandbox-exec
context: .
dockerfile: Dockerfile.sandbox
repo: oci.hanzo.ai/hanzoai/sandbox
tag-suffix: exec
platforms: [linux/amd64, linux/arm64]
args: { STAGE: exec }
- name: sandbox-dev
context: .
dockerfile: Dockerfile.sandbox
repo: oci.hanzo.ai/hanzoai/sandbox
tag-suffix: dev
platforms: [linux/amd64, linux/arm64]
args: { STAGE: dev }
- name: sandbox-desktop
context: .
dockerfile: Dockerfile.sandbox
repo: oci.hanzo.ai/hanzoai/sandbox
tag-suffix: desktop
platforms: [linux/amd64, linux/arm64]
args: { STAGE: desktop }
# android is the one entry whose `platforms:` is the IMAGE's limit rather than
# the fleet's — Google publishes no linux-aarch64 emulator, so this stage could
# not be built for arm64 even on a cluster that wanted it.
- name: sandbox-android
context: .
dockerfile: Dockerfile.sandbox
repo: oci.hanzo.ai/hanzoai/sandbox
tag-suffix: android
platforms: [linux/amd64]
args: { STAGE: android }
# The BOUNDARY image is the estate's other half: the sandbox images above are
# what runs INSIDE a boundary, this is what a NODE needs to offer one. It ships
# here rather than in a repo of its own because the two are one artifact set —
# a node that cannot hold a boundary cannot run any of them — and because this
# lane already builds both arches.
#
# It therefore SHARES `version:` with them, which is the sandbox's number. That
# is a deliberate estate-wide release number and not the borrowed one this file
# warns about above: docker/sandbox.version changes when any of these images
# change, and a deployment pins a digest regardless. Give it a file of its own
# the day the two need to move independently.
- name: boundary
context: .
dockerfile: Dockerfile.boundary
repo: oci.hanzo.ai/hanzoai/boundary
platforms: [linux/amd64, linux/arm64]
- name: sandbox-admin
context: .
dockerfile: Dockerfile.sandbox
repo: oci.hanzo.ai/hanzoai/sandbox
tag-suffix: admin
platforms: [linux/amd64, linux/arm64]
args: { STAGE: admin }
# The gate for THIS lane, and deliberately not a second copy of ci.yml.
#
# `.hanzo/workflows/ci.yml` already runs `pnpm check`, `pnpm build:strict-smoke`,
# the docs check and the full vitest matrix on every push to main and every PR —
# with the HANZO_BOT_TEST_WORKERS and 6 GB old-space settings that suite needs to
# not OOM. Repeating any of it here would double the wall time of every commit to
# buy a second opinion from the same command, and it would put the image behind a
# check it has nothing to do with: `pnpm check` is red right now on 991 files the
# pinned oxfmt 0.35.0 reorders imports in, which is a sweep somebody owes the
# repo and no reason for a box to be unbuildable.
#
# What ci.yml does NOT know about is the image this lane exists to publish, so
# the gate is exactly the assertion that guards it: every base Dockerfile.sandbox
# builds on is pinned by digest, and every FROM in it resolves to a stage or an
# ARG declared in the same file. That property is what makes two builds of one
# commit the same bytes, and this lane pushes on pull_request too — so a floating
# tag here is a PR quietly redefining what main publishes.
test:
- name: box image invariants
run: |
set -e
corepack enable
pnpm install --frozen-lockfile
pnpm exec vitest run src/docker-image-digests.test.ts