ship the frontend as a site release, not a cloud rebuild #1

Open
z wants to merge 2 commits from site-release into main
Owner

A CSS fix currently costs a ~22 minute hanzoai/cloud build and a measured 2m15s outage of api.hanzo.ai, because the console ships welded into the cloud binary: console-embed image -> COPY --from=console /dist/ -> go:embed. Nobody pays that for a frontend change, so nobody ships — the live console is 13 commits behind main.

The frontend now ships as a site release, authenticated with IAM and nothing else.

Why not the hanzoai/ci site: lane

I tried that first (see the first commit). It cannot express an IAM-only publish: it uploads with mc mirror and fails closed without S3_ADMIN_ACCESS_KEY/S3_ADMIN_SECRET_KEY — shared static object-store keys, not IAM. Those names are also genuinely absent from KMS at the path CI reads (org hanzo, path deploy, env prod holds 11 names, none of them S3). Rather than bend the lane or seal a second kind of credential, the lane is dropped and site: comes back out of hanzo.yml.

What replaces it

.hanzo/workflows/site.yml — build the export, zip it at its root, then two calls with one bearer:

  • POST /v1/projects/hanzo-console/deploy (Content-Type: application/zip)
  • POST /v1/sites/hanzo-console/publish ({"source":"hanzo-console"})

Both carry Authorization: Bearer <IAM token> and X-Org-Id: hanzo. Publishing is push: main only, so a pull request cannot ship a release.

The bearer is an IAM token, and nothing has to be sealed in KMS

GitHub still holds only KMS_CLIENT_ID/KMS_CLIENT_SECRET. cloud's /v1/kms/auth/login is not a second identity — it is a broker that performs the IAM client_credentials exchange and returns IAM's JWT verbatim. Decoded from the live endpoint:

alg: RS256
iss: https://hanzo.id
aud: ['hanzo-console']
sub: admin/hanzo-console
owner: hanzo
tokenType: access-token

1186 characters — the same token that minting directly at hanzo.id/v1/iam/oauth/token produces. One issuer, one token, no S3 credential in the path, and no new KMS entry required.

Verified

The build, the zip shape, and the workflow's own script were run locally:

[build:embed] static export ready at out/
BUILD_EXIT=0
PASS: out/index.html exists (369492 bytes)
9.1M	out / 68 files

zip: index.html at zip ROOT: yes / 68 entries

The script's failure path was measured against a real outage — while KMS was briefly 503, it printed ::error::KMS login -> HTTP 503 and exited 1 rather than a parser stack trace, which is why the login response is status-checked before it is parsed.

console-embed stays declared and Dockerfile.embed stays on disk — hanzoai/cloud still does FROM ...console-embed AS console, so removing either breaks cloud's build. Both go when the cloud-side PR lands. The console image is untouched.

A CSS fix currently costs a ~22 minute `hanzoai/cloud` build and a measured 2m15s outage of api.hanzo.ai, because the console ships welded into the cloud binary: `console-embed` image -> `COPY --from=console /dist/` -> `go:embed`. Nobody pays that for a frontend change, so nobody ships — the live console is 13 commits behind main. The frontend now ships as a **site release**, authenticated with **IAM and nothing else**. ### Why not the `hanzoai/ci` `site:` lane I tried that first (see the first commit). It cannot express an IAM-only publish: it uploads with `mc mirror` and fails closed without `S3_ADMIN_ACCESS_KEY`/`S3_ADMIN_SECRET_KEY` — shared static object-store keys, not IAM. Those names are also genuinely absent from KMS at the path CI reads (`org hanzo`, `path deploy`, `env prod` holds 11 names, none of them S3). Rather than bend the lane or seal a second kind of credential, the lane is dropped and `site:` comes back out of `hanzo.yml`. ### What replaces it `.hanzo/workflows/site.yml` — build the export, zip it at its root, then two calls with **one bearer**: - `POST /v1/projects/hanzo-console/deploy` (`Content-Type: application/zip`) - `POST /v1/sites/hanzo-console/publish` (`{"source":"hanzo-console"}`) Both carry `Authorization: Bearer <IAM token>` and `X-Org-Id: hanzo`. Publishing is `push: main` only, so a pull request cannot ship a release. ### The bearer is an IAM token, and nothing has to be sealed in KMS GitHub still holds only `KMS_CLIENT_ID`/`KMS_CLIENT_SECRET`. cloud's `/v1/kms/auth/login` is not a second identity — it is a broker that performs the IAM `client_credentials` exchange and returns IAM's JWT verbatim. Decoded from the live endpoint: ``` alg: RS256 iss: https://hanzo.id aud: ['hanzo-console'] sub: admin/hanzo-console owner: hanzo tokenType: access-token ``` 1186 characters — the same token that minting directly at `hanzo.id/v1/iam/oauth/token` produces. One issuer, one token, no S3 credential in the path, and no new KMS entry required. ### Verified The build, the zip shape, and the workflow's own script were run locally: ``` [build:embed] static export ready at out/ BUILD_EXIT=0 PASS: out/index.html exists (369492 bytes) 9.1M out / 68 files zip: index.html at zip ROOT: yes / 68 entries ``` The script's failure path was measured against a real outage — while KMS was briefly 503, it printed `::error::KMS login -> HTTP 503` and exited 1 rather than a parser stack trace, which is why the login response is status-checked before it is parsed. `console-embed` stays declared and `Dockerfile.embed` stays on disk — `hanzoai/cloud` still does `FROM ...console-embed AS console`, so removing either breaks cloud's build. Both go when the cloud-side PR lands. The `console` image is untouched.
z added 1 commit 2026-08-06 00:46:03 +00:00
the frontend ships on its own cadence
Hanzo CI/CD / cicd (pull_request) Successful in 4m9s
CI/CD / cicd (pull_request) Successful in 4m16s
a9079a1b17
A CSS fix used to cost a ~22 minute hanzoai/cloud build and 2m15s of
api.hanzo.ai, because the console shipped welded into the cloud binary:
console-embed image -> COPY --from=console /dist/ -> go:embed. Nobody pays
that for a frontend change, so nobody shipped, and the live console fell 13
commits behind main.

Declare the `site:` lane hanzoai/ci@v1 already implements. A push to main
builds the static export and POSTs /v1/sites/hanzo-console/publish: one
immutable release digested from its manifest, activated by a pointer flip,
rolled back the same way. Cloud is not in the path at all.

No new credential. Declaring `site:` is itself what makes ci fetch the
publish credential, and the bearer is the IAM JWT the workflow's single KMS
login already minted from KMS_CLIENT_ID/KMS_CLIENT_SECRET. One machine
identity, one thing to rotate, nothing added to git.

console-embed stays declared and Dockerfile.embed stays on disk: cloud still
consumes the image, so removing either here breaks cloud's build. Marked
deprecated, removed when the cloud-side PR lands. The Next.js server image
`console` is untouched -- it still serves admin.lux.cloud, admin.lux.network
and admin.zoo.cloud.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
a added 1 commit 2026-08-06 01:13:28 +00:00
publish with IAM and nothing else
Hanzo CI/CD / cicd (pull_request) Successful in 6m5s
CI/CD / cicd (pull_request) Successful in 6m6s
f5c88aac0a
Identity is IAM everywhere, so the publish path may not hold a second kind of
credential. The hanzoai/ci `site:` lane does: it uploads with `mc mirror` and
refuses to publish without S3_ADMIN_ACCESS_KEY/SECRET_KEY, shared static
object-store keys that are not IAM and are not in KMS. The lane cannot express
an IAM-only upload, so it is dropped rather than bent, and `site:` comes back
out of hanzo.yml.

In its place, .hanzo/workflows/site.yml uploads through cloud's own zip route
(POST /v1/projects/hanzo-console/deploy) and promotes it (POST
/v1/sites/hanzo-console/publish) with ONE bearer for both calls.

That bearer is an IAM token, not a second identity. GitHub still holds only
KMS_CLIENT_ID/KMS_CLIENT_SECRET; cloud's /v1/kms/auth/login is a broker that
performs the IAM client_credentials exchange and returns IAM's JWT verbatim.
Measured: RS256, iss=https://hanzo.id, owner=hanzo, tokenType=access-token —
the same token minting it directly at hanzo.id produces. So nothing has to be
sealed in KMS for this to work, and there is no S3 credential in the path.

Publishing is `push: main` only, so a pull request cannot ship a release.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
All checks were successful
Hanzo CI/CD / cicd (pull_request) Successful in 6m5s
CI/CD / cicd (pull_request) Successful in 6m6s
This pull request has changes conflicting with the target branch.
  • hanzo.yml
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin site-release:site-release
git checkout site-release
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hanzoai/console#1