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/cisite: 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:
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.
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>
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A CSS fix currently costs a ~22 minute
hanzoai/cloudbuild and a measured 2m15s outage of api.hanzo.ai, because the console ships welded into the cloud binary:console-embedimage ->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/cisite:laneI tried that first (see the first commit). It cannot express an IAM-only publish: it uploads with
mc mirrorand fails closed withoutS3_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 prodholds 11 names, none of them S3). Rather than bend the lane or seal a second kind of credential, the lane is dropped andsite:comes back out ofhanzo.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>andX-Org-Id: hanzo. Publishing ispush: mainonly, 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/loginis not a second identity — it is a broker that performs the IAMclient_credentialsexchange and returns IAM's JWT verbatim. Decoded from the live endpoint:1186 characters — the same token that minting directly at
hanzo.id/v1/iam/oauth/tokenproduces. 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:
The script's failure path was measured against a real outage — while KMS was briefly 503, it printed
::error::KMS login -> HTTP 503and exited 1 rather than a parser stack trace, which is why the login response is status-checked before it is parsed.console-embedstays declared andDockerfile.embedstays on disk —hanzoai/cloudstill doesFROM ...console-embed AS console, so removing either breaks cloud's build. Both go when the cloud-side PR lands. Theconsoleimage is untouched.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.