mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-24 20:09:02 +00:00
178 lines
8.3 KiB
YAML
178 lines
8.3 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-hanzo-ai`, whose prefix
|
|
# (hanzo/docs-hanzo-ai) 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 runs on the amd64 runner nodes, 32Gi each. linux-arm64 is a
|
|
# 4-core, 16Gi node the sandboxes share.
|
|
runs-on: linux-amd64
|
|
# A runner pod is capped at 6 cores and 24Gi. The compile used to peak at
|
|
# 24 GB in one process, because the API reference's code examples were
|
|
# highlighted into the bundle token by token, and the last failed run died
|
|
# in it. They are highlighted as each page renders now
|
|
# (apps/docs/lib/rehype-example.ts): measured with this job's settings, the
|
|
# compile peaks near 10 GB and the build, render workers included, near
|
|
# 14 GB, and the export is 6 GB rather than 19. It compiles two tasks wide
|
|
# and renders with two workers. 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 memory 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. That figure leaves out what the pod is also charged
|
|
# for, visor's sentry (about 1.4G) and the page cache of files the job
|
|
# wrote. After it come the five largest processes by resident memory, so
|
|
# the line also says which part of the build holds it.
|
|
#
|
|
# TURBO_TASKS_AVAILABLE_PARALLELISM sets how many tasks Turbopack runs at
|
|
# once and how many Node processes it starts for loaders, each with its
|
|
# own heap. Two keeps the loaders near 1.2G together.
|
|
- 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=8192
|
|
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; }
|
|
|
|
# The slug is the host's name, as hanzo-ai is hanzo.ai's and cloud-hanzo-ai
|
|
# is cloud.hanzo.ai's. `docs` cannot be a project: it is a reserved label
|
|
# (cloud apps/sites/reserved.go), so creating it answers 400, and the first
|
|
# run to reach this step failed there with the export built and gated.
|
|
- name: publish
|
|
uses: hanzoai/ci/.github/actions/site@v1
|
|
with:
|
|
slug: docs-hanzo-ai
|
|
dir: apps/docs/out
|
|
jobs: '48'
|
|
env:
|
|
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
|
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|