Files
docs/.hanzo/workflows/deploy.yml
T
zeekayandHanzo Dev ecf058e96b
deploy / site (push) Failing after 15m7s
The docs export bounds its memory, and bun installs by version
The export build keeps no Turbopack build cache, makes no source maps,
leaves the server bundle unminified, runs two Turbopack tasks and two
page workers, and gives each Node process a 4GB heap. next dev is
unchanged apart from the build cache, which it never used.

setup-bun installs 1.3.11 from its release by name. Asking for latest
reads the tag list from api.github.com first, which refuses the runner's
shared address once its anonymous limit is spent.

The heartbeat also names the five largest processes by resident memory.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 15:32:02 -07:00

169 lines
7.7 KiB
YAML

name: deploy
# docs.hanzo.ai is a static export on the Sites plane. This job builds
# apps/docs/out and publishes it as project `docs`, whose prefix (hanzo/docs) the
# docs.hanzo.ai route serves. No image, no pod. RELEASE.md has the whole path.
#
# It runs on git.hanzo.ai (hanzoai/docs, a pull mirror of this repo), which reads
# .hanzo/workflows/. GitHub reads only .github/workflows/, so it runs none of it.
on:
push:
branches: [main]
paths:
- 'apps/docs/**'
- 'packages/**'
- 'pnpm-lock.yaml'
- 'scripts/check-export.sh'
- '.hanzo/workflows/deploy.yml'
workflow_dispatch:
# One publish at a time: a newer push waits for the running one to finish, then
# the newest waiting commit publishes. Two publishes at once would each delete
# the other's files when they complete.
concurrency:
group: deploy-docs
cancel-in-progress: false
jobs:
site:
# A static export names no architecture; the label picks the host.
# linux-amd64 is the runner node, 8 cores and 32Gi. linux-arm64 is a
# 4-core, 16Gi node the sandboxes share.
runs-on: linux-amd64
# The export alone took 47 minutes on a 10-core workstation (15 compiling,
# 23 rendering 5,907 pages). Here it compiles two tasks wide and renders
# with two workers to fit the runner's 12Gi, so it runs longer, and install,
# packages and publish come on top. The runner pod itself ends at 4 hours.
timeout-minutes: 210
steps:
- uses: actions/checkout@v4
with:
# Team sections (hanzo-docs/<team>) are submodules under
# apps/docs/content/docs/; without them whole sections are missing.
submodules: recursive
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
# build:pre and build:post run under bun. An exact version is fetched from
# its release by name; "latest" first asks api.github.com for the tag list,
# which answers the runner's shared address 403 once its anonymous rate
# limit is spent.
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
# The ingest key is public (it ships in the bundle) and lives in KMS so it
# rotates without a commit. Login and read are retried on a lost answer
# (000/429/5xx) and not on a refusal.
- name: ingest key
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
run: |
set -euo pipefail
KMS=https://api.hanzo.ai
[ -n "${KMS_CLIENT_ID:-}" ] && [ -n "${KMS_CLIENT_SECRET:-}" ] || {
echo "::error::KMS_CLIENT_ID/KMS_CLIENT_SECRET are unset on this forge org"; exit 1; }
ask() { # ask <outfile> <curl args...> -> status code
local out="$1"; shift; local code=""
for wait in 0 5 15 45; do
[ "$wait" = 0 ] || sleep "$wait"
code="$(curl -s -o "$out" -w '%{http_code}' --max-time 30 "$@")" || code=000
case "$code" in 000|429|5*) continue ;; *) break ;; esac
done
printf '%s' "$code"
}
code="$(ask /tmp/login.json "$KMS/v1/kms/auth/login" -H 'Content-Type: application/json' \
-d "{\"clientId\":\"$KMS_CLIENT_ID\",\"clientSecret\":\"$KMS_CLIENT_SECRET\"}")"
[ "$code" = 200 ] || { echo "::error::KMS login answered $code"; exit 1; }
token="$(jq -r '.accessToken // empty' /tmp/login.json)"
[ -n "$token" ] || { echo "::error::KMS login returned no accessToken"; exit 1; }
code="$(ask /tmp/key.json "$KMS/v1/kms/secrets/deploy/PUBLISHABLE_KEY?env=prod" \
-H "Authorization: Bearer $token")"
[ "$code" = 200 ] || { echo "::error::reading deploy/PUBLISHABLE_KEY answered $code (403: this client may not read it; 404: not sealed)"; exit 1; }
key="$(jq -r '.value // empty' /tmp/key.json)"
rm -f /tmp/login.json /tmp/key.json
case "$key" in
pk-*) ;;
*) echo "::error::deploy/PUBLISHABLE_KEY is empty or not a pk- key"; exit 1 ;;
esac
# A well-formed key can still name a deleted project: ask the endpoint.
code="$(curl -s -o /tmp/ingest.json -w '%{http_code}' --max-time 20 \
-X POST "https://api.hanzo.ai/v1/event?ingest_key=$key" \
-H 'content-type: application/json' -d '{"batch":[]}')" || code=000
case "$code" in
200) ;;
000|429|5*) echo "::warning::ingest endpoint answered $code; key unverified" ;;
*) echo "::error::the ingest key is refused ($code): $(cat /tmp/ingest.json)"; exit 1 ;;
esac
echo "::add-mask::$key"
echo "NEXT_PUBLIC_PUBLISHABLE_KEY=$key" >> "$GITHUB_ENV"
- run: pnpm install --frozen-lockfile
# The workspace packages first, on their own, so a failure here is not
# read as a failure of the site. `docs^...` is turbo's word for "what docs
# depends on, not docs".
- name: build the packages the app imports
run: pnpm turbo run build --filter=docs^...
# The export, run directly rather than through turbo: turbo holds a task's
# output until the task ends, and a build that dies mid-way then reports
# nothing at all — which is how the first run of this lane failed with a
# log that stopped seven minutes before the job did.
#
# The heartbeat is there for the same reason. It prints the memory in use
# every half minute, so a run that hits the runner's 12Gi limit says so in
# its last line instead of simply stopping: the cgroup's number where the
# runtime exposes one, and under visor, which does not, the sandbox's own
# /proc/meminfo. After it come the five largest processes by resident
# memory, so the line also says which part of the build holds it.
#
# The build is sized to that limit. TURBO_TASKS_AVAILABLE_PARALLELISM sets
# how many tasks Turbopack runs at once and how many Node processes it
# starts for loaders, and each loader process holds its own heap. Every
# Node process gets a 4GB heap rather than 8GB: page workers and loaders
# grow toward whatever ceiling they are given before they collect.
- name: build the export
run: |
set -euo pipefail
( while :; do
printf '[usage] %s mem=%s disk=%s%s\n' "$(date -u +%H:%M:%S)" \
"$(cat /sys/fs/cgroup/memory.current 2>/dev/null || awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print (t-a)"k"}' /proc/meminfo)" \
"$(df -Pk . | awk 'NR==2{print $4"k free"}')" \
"$(awk '/^Name:/{n=$2} /^VmRSS:/{print $2, n}' /proc/[0-9]*/status 2>/dev/null | sort -rn | head -5 | awk '{printf " %s=%dM", $2, $1/1024}')"
sleep 30
done ) &
heartbeat=$!
trap 'kill "$heartbeat" 2>/dev/null || true' EXIT
pnpm --filter docs build
env:
NEXT_EXPORT: '1'
HANZO_DOCS_SYNC: '0'
NEXT_TELEMETRY_DISABLED: '1'
NODE_OPTIONS: --max-old-space-size=4096
TURBO_TASKS_AVAILABLE_PARALLELISM: '2'
- name: export gate
run: |
set -eu
sh scripts/check-export.sh apps/docs/out
grep -rqF "$NEXT_PUBLIC_PUBLISHABLE_KEY" apps/docs/out \
|| { echo "::error::the ingest key is not in the export"; exit 1; }
- name: publish
uses: hanzoai/ci/.github/actions/site@v1
with:
slug: docs
dir: apps/docs/out
jobs: '48'
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}