Compare commits

...
2 Commits
Author SHA1 Message Date
hanzo-dev f5c88aac0a publish with IAM and nothing else
Hanzo CI/CD / cicd (pull_request) Successful in 6m5s
CI/CD / cicd (pull_request) Successful in 6m6s
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>
2026-08-05 18:11:08 -07:00
hanzo-dev a9079a1b17 the frontend ships on its own cadence
Hanzo CI/CD / cicd (pull_request) Successful in 4m9s
CI/CD / cicd (pull_request) Successful in 4m16s
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>
2026-08-05 17:36:49 -07:00
2 changed files with 95 additions and 5 deletions
+77
View File
@@ -0,0 +1,77 @@
# The console ships as a SITE RELEASE, and identity is IAM only.
#
# This is deliberately NOT the hanzoai/ci `site:` lane. That lane uploads with
# `mc mirror` and refuses to publish without S3_ADMIN_ACCESS_KEY/SECRET_KEY —
# shared static object-store keys, not IAM — so it cannot express an IAM-only
# publish. It is not bent into one either: the upload here is cloud's OWN zip
# route, authorized by the SAME bearer as the publish that follows it. One
# issuer, one token, no S3 credential anywhere in the path.
#
# WHERE THE BEARER COMES FROM. GitHub holds only KMS_CLIENT_ID/KMS_CLIENT_SECRET,
# as everywhere else. 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 own
# JWT verbatim. Measured — the token it hands back is RS256 with
# `iss: https://hanzo.id`, `owner: hanzo`, `tokenType: access-token`, which is the
# same token minting it directly at hanzo.id/v1/iam/oauth/token produces. So this
# IS the IAM bearer, there is no second credential path, and nothing has to be
# sealed in KMS for it to work.
#
# Publishing is `push: main` only, so a pull request can never ship a release.
name: Site
on:
workflow_dispatch:
push:
branches: [main]
jobs:
publish:
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: corepack enable && pnpm install --frozen-lockfile
# The heap bump is load-bearing, not defensive: the full @hanzo/gui export
# OOMs into a stub shell without it. The analytics id is the public per-site
# id Dockerfile.embed baked, so the release tracks exactly as the embed did.
- run: pnpm build:embed
env:
NODE_OPTIONS: --max-old-space-size=8192
NEXT_PUBLIC_ANALYTICS_WEBSITE_ID: 7dce54ee-41f6-4751-96bf-fe005067c7c7
- name: Deploy + publish
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
run: |
set -euo pipefail
# node, not jq: this job already pins Node 22 above, so parsing with it
# assumes nothing about what the runner image ships.
jv () { node -p "JSON.parse(require('fs').readFileSync('$1','utf8'))['$2'] ?? ''"; }
post () { # url content-type curl-data-flag value
local code
code=$(curl -sS -o /tmp/resp.json -w '%{http_code}' -X POST "$1" \
-H "Authorization: Bearer $TOKEN" -H 'X-Org-Id: hanzo' \
-H "Content-Type: $2" "$3" "$4")
# Each non-2xx here is a different fix (402 hosting, 403 wrong org, 404
# no such site, 409 source moved, 413 too large), so print the body.
case "$code" in 2??) ;;
*) echo "::error::POST $1 -> HTTP $code"; cat /tmp/resp.json; exit 1 ;;
esac
}
node -e 'require("fs").writeFileSync("/tmp/login.json",JSON.stringify({clientId:process.env.KMS_CLIENT_ID,clientSecret:process.env.KMS_CLIENT_SECRET}))'
code=$(curl -sS -o /tmp/tok.json -w '%{http_code}' -X POST https://api.hanzo.ai/v1/kms/auth/login \
-H 'Content-Type: application/json' --data-binary @/tmp/login.json)
# Checked before parsing: the gateway answers a down KMS with a 503 whose
# body is `no available server`, and feeding that to a JSON parser turns an
# outage into a stack trace instead of the one line that names it.
case "$code" in 2??) ;;
*) echo "::error::KMS login -> HTTP $code"; cat /tmp/tok.json; echo; exit 1 ;;
esac
TOKEN=$(jv /tmp/tok.json accessToken)
[ -n "$TOKEN" ] || { echo "::error::KMS login returned no IAM bearer"; exit 1; }
echo "::add-mask::$TOKEN"
# index.html must be at the ZIP ROOT — cloud refuses a source without one.
( cd out && zip -qr ../site.zip . )
post https://api.hanzo.ai/v1/projects/hanzo-console/deploy application/zip --data-binary @site.zip
post https://api.hanzo.ai/v1/sites/hanzo-console/publish application/json -d '{"source":"hanzo-console"}'
echo "published $(jv /tmp/resp.json releaseId) ($(jv /tmp/resp.json objects) objects)"
+18 -5
View File
@@ -1,11 +1,20 @@
# Canonical CI config for hanzoai/console — read by the hanzoai/ci reusable
# (.hanzo/workflows/cicd.yml) and platform.hanzo.ai.
#
# Publishes the console STATIC EMBED artifact (SPA static export at /dist) as a
# versioned immutable image. hanzoai/cloud consumes it via `FROM ... AS console`
# + `COPY --from=console /dist/`, so it never rebuilds npm+Next on a cloud release.
# hanzoai/ci pushes to `repo:` (GHCR) and server-side-mirrors to registry.hanzo.ai
# automatically.
# The frontend ships as a SITE RELEASE — .hanzo/workflows/site.yml. A push to main
# builds the static export and promotes it on /v1/sites with an IAM bearer, and that
# is the whole path: no image, no cloud rebuild, rollback is a pointer flip. It is
# NOT the hanzoai/ci `site:` lane — that lane uploads with `mc mirror` and refuses
# to publish without static S3 admin keys, which is a second, non-IAM credential.
#
# It used to ship welded INTO the cloud binary via the console-embed image below
# (cloud `COPY --from=console /dist/` → go:embed), which priced a CSS fix at a
# ~22-minute cloud build and a 2m15s api.hanzo.ai outage. Nobody pays that for a
# frontend change, so nobody shipped: the live console ran 13 commits behind this
# branch. The embed image stays declared until cloud stops consuming it.
#
# hanzoai/ci pushes each `images:` entry to `repo:` (GHCR) and server-side-mirrors
# to registry.hanzo.ai automatically.
#
# BOTH console images are declared here — one config, read by whichever runner
# executes it. The Next.js SERVER image (admin.hanzo.ai, operator CR
@@ -22,6 +31,10 @@
# the sha tag — that is what hanzoai/cloud does, and an immutable digest-shaped
# tag cannot be re-pushed to different bytes the way `:v8.4.118` once was.
images:
# DEPRECATED — superseded by the `site:` release below. Kept only because
# hanzoai/cloud still does `FROM ...console-embed AS console`; dropping it here
# (or deleting Dockerfile.embed) breaks cloud's build. Remove both once the
# cloud-side PR that stops consuming it merges.
- name: console-embed
context: .
dockerfile: Dockerfile.embed