mirror of
https://github.com/hanzoai/bot.git
synced 2026-10-30 17:24:26 +00:00
Both ran buildx on a runner with no Docker daemon and published nothing. Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
183 lines
9.1 KiB
YAML
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
|