Files
zeekay d440fda72a release: platform-native build+publish via hanzo.yml (retire GHA)
ONE declarative flow for both lux release artifacts, on platform.hanzo.ai +
self-hosted arcd — NO GitHub Actions:

- hanzo.yml: node image build (matrix linux/{amd64,arm64}, native long-poll
  dispatch onto lux-build-* pools, tag-pattern {{git.branch}} -> :vX.Y.Z).
  Validated against the platform parsePlatformConfig.
- scripts/publish_plugin_set.sh: extracts the 12 baked VM plugins from the
  node image + SHA256SUMS, uploads to s3://lux-plugins-<env>/<pluginset>/
  (operator pluginSource), round-trip sha-verified. DRY — no second compile.
- RELEASE.md: the ONE canonical build+publish runbook; lists the .github
  workflows to retire (docker.yml, release.yml, build*.yml, ci.yml, ...).
- LLM.md: points at RELEASE.md as the canonical release path.

Proof (no GitHub): node:v1.30.40 provenance == 44b67b99a0 (reused, not
rebuilt); evm@v1.99.37 + dexvm@v1.5.15 rebuilt natively on the spark fleet
(go1.26.4, CGO=0) and the full 12-plugin set published to
s3://lux-plugins-staging/ (round-trip verified). neo's lux-plugins-devnet
prefix untouched.
2026-06-23 09:01:48 -07:00

8.8 KiB

Lux release — build + publish via platform.hanzo.ai (the ONE canonical way)

This is the single, repeatable way to build and publish the Lux release artifacts. It runs entirely on our own infrastructure — the PaaS (platform.hanzo.ai) + self-hosted arcd runners + DOKS/fleet. There is no GitHub Actions build path (the .github/workflows/* build/release workflows are retired — see §Retire).

What a release produces

ONE Dockerfile multi-stage build (this repo) is the single source of truth. It compiles luxd + all 12 VM plugins (CGO_ENABLED=0) and yields TWO distribution surfaces:

# Artifact Destination Consumed by
1 node image (luxd + 12 plugins baked at /luxd/build/plugins/) ghcr.io/luxfi/node:vX.Y.Z operator pod image; startup.sh cp /luxd/build/plugins/*
2 plugin set (the 12 VM-ID binaries + SHA256SUMS) s3://lux-plugins-<env>/<pluginset>/ operator plugin-fetch init container (LuxNetwork CR pluginSource)

Artifact 2 is extracted from artifact 1 — the plugins are never compiled twice. One build, two surfaces (DRY, orthogonal).

The plugin versions are pinned as Dockerfile build-args, kept in lockstep with this repo's go.mod:

  • EVM_VERSION (luxfi/evm — C-Chain EVM, the 0x9999 settlement surface)
  • CHAINS_REF (luxfi/chains — the 10 non-DEX VMs incl. bridgevm)
  • DEX_REF (luxfi/dex cmd/dchain — the native D-Chain DEX VM)

The machinery

git tag vX.Y.Z (push)
        │  GitHub App webhook ─▶ https://platform.hanzo.ai/v1/github-webhook
        ▼
  platform BuildScheduler  ── reads hanzo.yml @ tag, validates, enqueues
        │                      one build_job per matrix entry
        ▼
  native long-poll fabric (build-queue.ts)        NO GitHub Actions hop
        │  arcd runner POST /v1/arcd/poll  (HMAC)
        ▼
  arcd runner on pool lux-build-linux-<arch>
        │  git checkout @ tag → docker build -f Dockerfile . → docker push
        ▼
  ghcr.io/luxfi/node:vX.Y.Z         (artifact 1)
        │  POST /v1/arcd/complete (status, image_digest)
        ▼
  build_job (DB, system-of-record)
  • PaaS: platform.hanzo.ai (~/work/hanzo/platform, pkg/platform/src/services/ci/). Owns the schema, the scheduler, the durable build_job record, and the native long-poll dispatch.
  • Build muscle: self-hosted arcd runner pools, lux-build-linux-amd64 and lux-build-linux-arm64 (one daemon per fleet host; spark = linux/arm64, amd64 via buildx). NO GitHub-hosted runners; NO GitHub Actions orchestration on the native path.
  • Contract: ~/work/hanzo/platform/docs/PLATFORM_CI.md.

Build — the one command

A release is a semver tag push. The declarative entrypoint is this repo's hanzo.yml; the trigger is one of:

(a) Tag push (normal release). Cutting the tag IS the release:

git tag v1.30.41 && git push origin v1.30.41

The webhook maps refs/tags/v1.30.41branch=v1.30.41; hanzo.yml's tag-pattern: "{{git.branch}}" yields the image tag v1.30.41. Platform schedules the amd64 + arm64 builds onto the live lux-build-* arcd pools and pushes the multi-arch image to GHCR.

(b) On-demand (re-release / backfill). The platform buildJob.trigger tRPC mutation schedules the same build for an explicit ref, no push required:

buildJob.trigger({
  installationId: "<luxfi GitHub App installation id>",
  repo: "luxfi/node",
  sha:  "<commit at the tag>",
  ref:  "refs/tags/v1.30.41",
  branch: "v1.30.41"            // → image tag via {{git.branch}}
})

Track it: buildJob.list / buildJob.one / buildJob.logs (org-scoped).

A pool goes native the moment an arcd runner self-registers for it (arcd_runner.lastSeen within 90s); until then platform transparently falls back to workflow_dispatch so a build is never stranded. To run a release fully GitHub-free, ensure a lux-build-linux-{amd64,arm64} runner is live (tRPC arcd / the arcd_runner table). Set platform env WORKFLOW_DISPATCH_FALLBACK=false to forbid the legacy hop.

What the runner runs (identical on a fleet host, for manual/DR builds)

The native path runs exactly the repo's Dockerfile. To reproduce on a fleet host directly (e.g. spark), with no platform and no GitHub:

# on spark (linux/arm64; amd64 via buildx)
git clone --branch v1.30.41 git@github.com:luxfi/node.git && cd node
docker buildx build --platform linux/amd64 \
  --build-arg CGO_ENABLED=0 \
  -t ghcr.io/luxfi/node:v1.30.41 -f Dockerfile --push .

Publish the plugin set — step 2

After the image exists, publish artifact 2 from it (one command, idempotent, no second compile). Run on any fleet host or a DOKS Job that has crane/docker

  • mc; typically the same arcd runner that just built the image:
scripts/publish_plugin_set.sh \
  ghcr.io/luxfi/node:v1.30.41 \
  lux-plugins-<env>/<pluginset> \
  lux                          # mc alias for the target MinIO/S3
# e.g. lux-plugins-testnet/v1.3.5

It extracts the 12 plugin binaries from the image, writes SHA256SUMS, uploads all to s3://lux-plugins-<env>/<pluginset>/, and verifies remote==local sha.

S3 is the in-cluster MinIO (s3.lux-system.svc.cluster.local:9000, external s3.lux.network). Configure the mc alias once with the hanzo-s3-secret credentials:

mc alias set lux <endpoint> hanzo "$(kubectl -n lux-system get secret \
  hanzo-s3-secret -o jsonpath='{.data.password}' | base64 -d)" --api s3v4

A pluginset prefix is immutable — bump <pluginset> for a new release, never overwrite a prefix a live network points at.

Deploy — step 3 (operator, not this repo)

luxd rollout is owned by the lux operator (~/work/lux/operator, LuxNetwork CR). Update the CR's image.tag (artifact 1) and, when the network fetches plugins from S3, the pluginSource.bucket + per-plugin sha256 (artifact 2, from the SHA256SUMS you just published). The operator's plugin-fetch init container verifies each sha256 fail-closed. This is deliberately decoupled from build: hanzo.yml has no deploy: block.

Reproducibility

  • The build is functionally reproducible: same source tags + same toolchain (Go 1.26.4) + CGO_ENABLED=0 ⇒ functionally identical plugins, provable by a fleet rebuild (verified: spark rebuilt evm@v1.99.37 + dexvm@v1.5.15 from the same tags). It is not bit-identical by construction: the Dockerfile plugin stages omit -trimpath and use -mod=mod with a first-party go.sum strip (re-resolves luxfi/* deps), so embedded paths + re-tagged module content can shift the bytes (Go BuildID differs; binary ~16 KB larger). The published image is the canonical artifact; verify against ITS baked sha (what publish_plugin_set.sh records), not a separate fleet build.
  • To make releases bit-reproducible (future hardening, patch-only): add -trimpath to every plugin go build and pin go.sum (drop the strip + -mod=mod). Tracked as a follow-up; not required for correctness.

Retire the GitHub Actions build workflows

These .github/workflows/* build/release/CI workflows are superseded by this flow and must be removed/disabled (platform owns build; the native long-poll owns dispatch). Delete them once a lux-build-* arcd runner is live:

Workflow Replaced by
docker.yml (built ghcr.io/luxfi/node on the lux-build ARC pool) hanzo.yml (artifact 1) — native long-poll, NO GitHub Actions
release.yml the tag-push trigger above + scripts/publish_plugin_set.sh
build.yml, ci.yml platform CI test step (runner runs go test pre-build)
build-linux-binaries.yml Dockerfile builder stage (luxd binary)
build-ubuntu-amd64-release.yml, build-ubuntu-arm64-release.yml Dockerfile + buildx multi-arch
build-macos-release.yml, build-win-release.yml, build-and-test-mac-windows.yml arcd lux-build-{macos,windows}-* pools (matrix in hanzo.yml when desired)
build-deb-pkg.sh, build-tgz-pkg.sh (under .github/workflows/) packaging step on the arcd runner (post-build), not GitHub Actions
codeql-analysis.yml, fuzz.yml, fuzz_merkledb.yml, test-database-replay.yml scheduled jobs on arcd / DOKS (not a build dependency)
buf-lint.yml, buf-push.yml, labels.yml, stale.yml repo-hygiene; migrate to arcd cron or drop

The same retirement applies to the equivalent build/release workflows in the plugin-source repos (luxfi/evm, luxfi/chains, luxfi/dex): their artifacts are built from source by THIS repo's Dockerfile at the pinned refs, so those repos need no independent image/release CI — only their tags. Migrate each by adding a hanzo.yml (if it ships its own image) or deleting its build CI (if it is consumed only as a Go module / plugin source here).