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

Open
z wants to merge 2 commits from site-release into main
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