mirror of
https://github.com/hanzo-apps/tabs.git
synced 2026-09-15 19:32:29 +00:00
Release publishes tabs.hanzo.ai to the Sites plane from the forge
Both workflows live in .hanzo/workflows, which git.hanzo.ai runs. Release installs with pnpm, typechecks, tests, exports, checks the sheet is in the export, and publishes `out/` through hanzoai/ci's site action as project `tabs`, the prefix the tabs.hanzo.ai route serves. The deploy key comes from KMS; no bucket credential is held. hanzo.yml's gates install with pnpm, since `catalog:` specs only resolve there, and package-lock.json goes with npm. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
name: Release
|
||||
# Publishes tabs.hanzo.ai.
|
||||
#
|
||||
# Tabs is a STATIC EXPORT, so a release is a bucket sync, not an image build. The
|
||||
# server in front of it is hanzoai/static, pinned in the operator values file and
|
||||
# released on its own cadence — so building a container here produced an image
|
||||
# that nothing deployed, and the only way a change ever reached the site was by
|
||||
# hand. That is what this replaces.
|
||||
#
|
||||
# The endpoint is the PUBLIC s3.hanzo.ai, not the in-cluster service address the
|
||||
# serving pod uses: these runners are not in the cluster. Same bucket, same
|
||||
# bytes, different address.
|
||||
#
|
||||
# Runs on Hanzo Git (git.hanzo.ai) native runners — see hanzoai/app/release.yml.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**.md', '.github/workflows/**']
|
||||
workflow_dispatch:
|
||||
permissions: { contents: write }
|
||||
concurrency: { group: release-tabs, cancel-in-progress: false }
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: [hanzo-build-linux-amd64]
|
||||
env:
|
||||
BUCKET: tabs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '22' }
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
- name: The layout engine and the bindings
|
||||
run: npm test
|
||||
- name: Export
|
||||
run: npm run build
|
||||
|
||||
# A build that emitted no app page is a green run that publishes a blank
|
||||
# site. Caught here rather than in a browser.
|
||||
- name: The export is complete
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for f in index.html app/index.html auth/callback/index.html; do
|
||||
[ -s "out/$f" ] || { echo "::error::missing or empty: out/$f"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Publish to the tabs bucket
|
||||
# The credential comes from KMS, not from repo secrets. `SITES_S3_*` —
|
||||
# the names this used to read — are in no org and in no KMS project, so
|
||||
# the step failed closed on every run since it was written and tabs could
|
||||
# only be updated by hand. Same pair the serving pod reads (k8s secret
|
||||
# `s3-credentials`), so CI and the server agree on one identity.
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The org rides the TOKEN, so the secret path is flat. An org-segmented
|
||||
# /v1/kms/orgs/<org>/secrets/... answers 404, and `curl -fsS` reports
|
||||
# that as a bare exit 22 that `set -e` turns into a step death with no
|
||||
# message naming which call failed.
|
||||
KMS_TOKEN=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken // empty')
|
||||
[ -n "$KMS_TOKEN" ] || { echo "::error::KMS login failed at ${KMS_ENDPOINT} — check KMS_CLIENT_ID/KMS_CLIENT_SECRET on this org"; exit 1; }
|
||||
kms() { curl -fsS "${KMS_ENDPOINT}/v1/kms/secrets/s3-credentials/$1?env=${KMS_SECRET_ENV}" -H "Authorization: Bearer ${KMS_TOKEN}" | jq -r '.value // empty'; }
|
||||
AWS_ACCESS_KEY_ID=$(kms access-key)
|
||||
AWS_SECRET_ACCESS_KEY=$(kms secret-key)
|
||||
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
|
||||
echo "::error::s3-credentials/{access-key,secret-key} missing in KMS (env ${KMS_SECRET_ENV}) — cannot sync."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${AWS_ACCESS_KEY_ID}"
|
||||
echo "::add-mask::${AWS_SECRET_ACCESS_KEY}"
|
||||
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
|
||||
# The runner image ships no aws CLI and nothing in the reusable
|
||||
# provisions one, so install it here or the step dies `aws: command
|
||||
# not found` (exit 127) with a perfectly good credential in hand.
|
||||
#
|
||||
# NOT apt: noble keeps awscli in universe, which is not enabled on
|
||||
# these runners — `apt-get install awscli` answers "Package 'awscli'
|
||||
# has no installation candidate", which reads like a typo rather than a
|
||||
# disabled component. AWS's own installer needs no repo and no sudo
|
||||
# when pointed at a user prefix, and gives v2, which is what the sync
|
||||
# flags below assume.
|
||||
if ! command -v aws > /dev/null 2>&1; then
|
||||
curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o /tmp/awscliv2.zip
|
||||
unzip -q -o /tmp/awscliv2.zip -d /tmp
|
||||
/tmp/aws/install -i "$HOME/.awscli" -b "$HOME/.local/bin" --update
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
aws --version
|
||||
# Assets first, HTML second, and the order is the point: the hashed
|
||||
# _next/ files are immutable and safe to cache forever, while the HTML
|
||||
# that names them must never be cached. Publishing the HTML first would
|
||||
# briefly serve a page pointing at chunks that are not up yet.
|
||||
aws s3 sync out/ "s3://${BUCKET}/" \
|
||||
--endpoint-url https://s3.hanzo.ai --only-show-errors \
|
||||
--exclude '*.html' --exclude '*.txt' \
|
||||
--cache-control 'public,max-age=31536000,immutable'
|
||||
# --delete so a page removed from the export stops being served;
|
||||
# without it the bucket only ever grows.
|
||||
aws s3 sync out/ "s3://${BUCKET}/" \
|
||||
--endpoint-url https://s3.hanzo.ai --only-show-errors \
|
||||
--delete --cache-control 'no-cache'
|
||||
|
||||
- name: Tag the release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force --quiet
|
||||
max="$(git tag -l 'v0.1.*' | sed 's/^v//' | sort -V | tail -1 || true)"
|
||||
[ -n "$max" ] || max="0.1.0"
|
||||
v="v0.1.$(( ${max##*.} + 1 ))"
|
||||
git tag "$v"
|
||||
git push origin "$v"
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Release
|
||||
# Publishes tabs.hanzo.ai.
|
||||
#
|
||||
# Tabs is a STATIC EXPORT, so a release is a site publish, not an image build.
|
||||
# hanzoai/ci's site action uploads `out/` to the Sites plane as project `tabs`,
|
||||
# which lands in s3://hanzo-sites/hanzo/tabs, the prefix the tabs.hanzo.ai route
|
||||
# serves (universe charts/app/values/hanzo/static-sites.yaml). The action reads
|
||||
# the deploy key from KMS and uploads under a prefix-scoped presigned grant, so
|
||||
# no bucket credential belongs here.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**.md', '.hanzo/workflows/**']
|
||||
workflow_dispatch:
|
||||
concurrency: { group: release-tabs, cancel-in-progress: false }
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: [linux-amd64]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# package.json's `packageManager` is the one place pnpm is pinned.
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: The layout engine and the bindings
|
||||
run: pnpm test
|
||||
|
||||
- name: Export
|
||||
run: pnpm build
|
||||
|
||||
# A build that emitted no app page is a green run that publishes a blank
|
||||
# site. Caught here rather than in a browser.
|
||||
- name: The export is complete
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for f in index.html app/index.html auth/callback/index.html; do
|
||||
[ -s "out/$f" ] || { echo "::error::missing or empty: out/$f"; exit 1; }
|
||||
done
|
||||
# The component sheet is what makes the page drawn rather than bare.
|
||||
grep -rlq -- '--hanzo-ui-styles' out/_next/static/css \
|
||||
|| { echo "::error::@hanzo/ui/styles.css is not in the export"; exit 1; }
|
||||
|
||||
- name: Publish to the Sites plane
|
||||
uses: hanzoai/ci/.github/actions/site@v1
|
||||
with:
|
||||
slug: tabs
|
||||
dir: out
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
@@ -63,10 +63,10 @@ by the machine, not by this site.
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npm test # the layout engine and bindings
|
||||
npm run typecheck
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:3000
|
||||
pnpm test # the layout engine and bindings
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
MIT.
|
||||
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
// Serves the export the way the bucket does, so the guard tests what ships.
|
||||
//
|
||||
// hanzoai/static fronts the `tabs` bucket and resolves a directory to its
|
||||
// index.html; trailingSlash: true in next.config.mjs is what makes every route a
|
||||
// directory. This does the same two things and nothing else — a test server that
|
||||
// resolves paths differently from production would pass on pages that 404 live.
|
||||
// The ingress serves the export from s3://hanzo-sites/hanzo/tabs and resolves a
|
||||
// directory to its index.html; trailingSlash: true in next.config.mjs is what
|
||||
// makes every route a directory. This does the same two things and nothing
|
||||
// else — a test server that resolves paths differently from production would
|
||||
// pass on pages that 404 live.
|
||||
import { createServer } from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, extname } from 'node:path';
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# Canonical CI/CD config for hanzoai/tabs — the one file both the hanzoai/ci
|
||||
# reusable (.hanzo/workflows/cicd.yml) and platform.hanzo.ai read.
|
||||
#
|
||||
# Tabs IS deployed from this repo — `.hanzo/workflows/release.yml` syncs the
|
||||
# static export to the `tabs` bucket on every push to main, and that is what
|
||||
# tabs.hanzo.ai serves. There is no `images:` and no `deploy:` here because
|
||||
# neither describes that: the server in front of the bucket is hanzoai/static,
|
||||
# pinned in universe and released on its own cadence, so this repo builds no
|
||||
# image and rolls out no workload. Read the absence as "nothing to build", not
|
||||
# as "nothing ships" — it said the latter once, and cost an afternoon spent
|
||||
# looking for a lane that was already there.
|
||||
# Tabs IS deployed from this repo — `.hanzo/workflows/release.yml` publishes the
|
||||
# static export to the Sites plane (project `tabs`) on every push to main, and
|
||||
# that is what tabs.hanzo.ai serves. There is no `images:` and no `deploy:` here
|
||||
# because neither describes that: the ingress serves the export straight from
|
||||
# s3://hanzo-sites/hanzo/tabs, so this repo builds no image and rolls out no
|
||||
# workload. Read the absence as "nothing to build", not as "nothing ships".
|
||||
#
|
||||
# The gate is typecheck, the jest suite, and the surface guard. Jest is
|
||||
# deliberately narrow and says so in its own config — "pure logic only: the
|
||||
@@ -30,11 +28,14 @@ test:
|
||||
- name: build
|
||||
run: |
|
||||
set -e
|
||||
npm ci
|
||||
npm run typecheck
|
||||
npm test
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
- name: surface
|
||||
run: |
|
||||
set -e
|
||||
npx playwright install --with-deps chromium
|
||||
npm run test:surface
|
||||
# The runner image bakes browsers under /opt/playwright as root, so the
|
||||
# job's own copy goes where the job may write.
|
||||
export PLAYWRIGHT_BROWSERS_PATH="$PWD/.cache/ms-playwright"
|
||||
pnpm exec playwright install chromium
|
||||
pnpm test:surface
|
||||
|
||||
Generated
-10267
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user