mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-24 20:09:02 +00:00
deploy / site (push) Failing after 15m17s
The build cache is where Turbopack puts a task it takes out of memory. With the cache off it has nowhere to evict to, and the compile kept every task resident. With it on and eviction set to full, each snapshot of the cache is followed by dropping every task the build can reload from it. The heartbeat prints every 15 seconds instead of 30. Co-authored-by: Hanzo Dev <[email protected]>
169 lines
7.7 KiB
YAML
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 15 seconds, 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 15
|
|
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 }}
|