Files
zeekay 8db9930b58 gate the ingest key in the Dockerfile, where every builder passes
A guard in deploy.yml protects one of six lanes. The image live right now is
tagged ae51c3fc — a SHA that exists only on the forge, whose forge run FAILED —
so something else built it, the fail-closed KMS step never ran, and the bundle
shipped with no key. Proven by streaming the 2.7GB layer: zero occurrences.

This is the shape the export gate already solved by living in the Dockerfile
rather than in a workflow. Same treatment: an empty or non-pk- key now refuses
the artifact instead of producing a hollow one, for every builder, without any
of them knowing this repo is special.

Both lanes are wired to supply it, or the gate would simply break docs:
- forge lane already fetches from KMS; its --build-arg is renamed to
  EVENT_INGEST_KEY to match the ARG.
- ci lane now declares build_secrets: [EVENT_INGEST_KEY]; the reusable fetches
  that name from KMS and passes it as a build-arg of the same name, and since
  hanzoai/ci 5750426 it fails closed when a declared secret cannot be read.

The prefix stays a property of this build: EVENT_INGEST_KEY is the one name in
KMS, NEXT_PUBLIC_ is added here because that is what makes Next inline it.

Gate verified on all three branches: pk- exits 0, empty exits 1, wrong prefix
exits 1.
2026-08-04 01:33:54 -07:00

82 lines
4.7 KiB
YAML

# Canonical CI config for hanzo-docs/docs. THREE programs read this file:
# hanzo/platform (the webhook lane that builds what is serving), hanzoai/ci (the
# GitHub lane, .github/workflows/cicd.yml), and hanzoai/cloud's forge push
# orchestrator. All three read `images:`; only ci reads `test:`. They do not agree
# on the tag — see below, because that disagreement is the whole contract.
#
# The repo builds ONE image: the Fumadocs monorepo exported static and served by
# hanzoai/static. The recipe lives in the Dockerfile and nowhere else — APP
# selects the workspace, so every static site here is built the same way. This
# file only declares WHAT is built; it does not restate the recipe.
#
# THE TAG CONTRACT. hanzoai/universe pins this image in
# charts/app/values/hanzo/docs.yaml, and the six builders that can produce it do
# not agree on what to call it:
#
# platform webhook, THIS file <sha40>-amd64-docs ← what universe pins
# POST /v1/runner whatever the caller names
# .hanzo/workflows/deploy.yml <sha40>
# hanzoai/ci (buildx) sha-<sha7>-amd64 + latest
# hanzoai/ci (mode: delegate) sha-<sha7>-amd64
# forge push orchestrator sha-<sha7>-amd64-docs (dormant)
#
# Three of those are three programs reading THIS file and disagreeing twice over:
# whether an absent `tag-suffix` falls back to `name` (platform: yes; cloud's
# apps/git/build_on_push.go: yes; hanzoai/ci: no, it reads `."tag-suffix" // ""`),
# and whether the sha is abbreviated (platform: no; the other two: yes). Adding
# `tag-suffix: docs` below would close the first disagreement and not the second
# — deliberately NOT done, because it changes the tag shape of lanes that have
# never run. RELEASE.md has the table and the reasoning.
#
# Whichever one publishes, the pin has to be told its name — and the line that
# actually decides what runs is `digest:`, not `tag:`, because the chart renders
# repo:tag@digest and a digest beside a tag is what gets pulled. Read the tag off
# the build you actually fired; RELEASE.md writes out the edit for each of the six.
images:
- name: docs
context: .
dockerfile: Dockerfile
repo: ghcr.io/hanzoai/docs
# The KMS name IS the build-arg name. Without this the ci lane passes no key
# and the Dockerfile gate refuses the build — which is the point: the two
# lanes now either both supply it or both fail loudly.
build_secrets: [EVENT_INGEST_KEY]
test:
- name: unit
# vitest over packages/* and apps/docs. The apps/docs project is what pins
# the four surfaces: the SDK method names the docs print are openapi-
# generator's own derivation, and the MCP tool names are the door's, both
# asserted against real generated clients and a real tools/list answer. A
# drift in either naming rule fails here rather than teaching a call that
# does not exist.
run: pnpm install --frozen-lockfile && pnpm test
# NOTE — the export gate is not declared here, and not because it was forgotten.
#
# A static-export site fails by exporting NOTHING, which no image build can
# notice: the layer is valid and the site is empty. So the check has to happen
# somewhere every builder passes through, and `test:` is not it — hanzoai/ci
# runs `test:` AFTER `docker buildx --push`, and buildx builds and pushes in one
# invocation with no step in between to hold. A gate declared here would report a
# broken export that was already in the registry.
#
# The gate is therefore inside the Dockerfile (scripts/check-export.sh, plus
# apps/docs/export.require naming the sections that must not vanish). A failing
# export fails the build, so no builder reaches a push — including this one.
#
# That placement is also what caught the one thing the GitHub lane got wrong on
# its own. apps/docs/content/docs/studio is a submodule (hanzo-docs/studio-docs),
# and hanzoai/ci checked out with actions/checkout's default, which fetches none:
# the section simply would not exist in the output while page count and nav both
# stayed green. It is named in export.require, so that lane failed loudly instead
# of shipping a hole — and the fix is upstream, where hanzoai/ci now takes a
# `submodules` input (default unchanged) that .github/workflows/cicd.yml sets to
# `recursive`. The gate stays, because it is what makes the next such hole loud.
# No `deploy:` — ON PURPOSE, and this is not the usual "a human should review it".
# docs.hanzo.ai is delivered by the hanzo/app chart with `cd.automated: true`, so
# Argo is the sole writer of those objects and self-heals anything else that
# touches them. A `deploy:` block here would have the platform patch a workload
# that gets reverted on the next sync. The rollout IS the commit to universe.