# Canonical CI config for hanzoai/openapi — read by the hanzoai/ci reusable # (.github/workflows/cicd.yml) and platform.hanzo.ai. # # Publishes the agent-skills catalog (/.well-known/agent-skills, skills.py # --no-services) as a versioned immutable image whose rootfs is /catalog. # hanzo-inc/cloud consumes it via `FROM ... AS skills` + `COPY --from=skills # /catalog/`, so it never re-runs skills.py inside a cloud release. hanzoai/ci # pushes to `repo:` (GHCR) and auto-mirrors to oci.hanzo.ai. images: - name: agent-skills context: . dockerfile: Dockerfile.skills repo: ghcr.io/hanzoai/agent-skills test: - name: spec # hanzo.yaml is the single input to SEVEN language SDKs, the doc site and the # agent-skills catalog, so a defect merged here is a defect merged into all # of them at once, discovered separately in each. # # test_publish.py is the offline half of the artifact gate: it reads the # COMMITTED hanzo.yaml and asserts the properties publish.py promises — # every operation generatable, operationIds unique to a GENERATOR and not # merely as strings, a security scheme a client can authenticate with, every # capability grouped exactly once, and no `/openapi.yaml` back on disk. # It needs no network and no hanzo-inc/cloud checkout. # # The half that needs a hanzo-inc/cloud checkout is `publish.py --check` in the # `spec sync` workflow, which re-derives from the pinned ref and diffs. That # is what catches a hand edit that happens to be well-formed — the one this # repo exists to make impossible now that hanzo.yaml is output rather than # input. The `served` gate below asks the weaker version of that question # with no checkout at all, which is why it is HERE and runs on every build. # # test_flows.py asserts every operationId flows.yaml names still exists in # hanzo.yaml, so a route that stops being served breaks one gate here instead # of six SDKs' examples quietly and separately. # # uv rather than a bare python3 — the runner image promises no interpreter, # let alone one with PyYAML. run: | set -e if command -v uv >/dev/null 2>&1; then UV=uv; else curl -fsSL https://astral.sh/uv/install.sh | sh UV="$HOME/.local/bin/uv" fi "$UV" run --no-project --python 3.12 --with pyyaml --with pytest \ python -m pytest test_publish.py test_flows.py test_skills.py test_tools.py -q - name: served # Every operation this repo PUBLISHES must be one api.hanzo.ai answers for. # # The gate that was supposed to hold that line — `publish.py --check` — # needs a hanzo-inc/cloud checkout, so it can only run where a credential # for a private repo in another org exists, and HANZO_GIT_TOKEN is not # sealed here yet. So it never ran, and while it did # not: the pin went 81 hanzo-inc/cloud commits stale, hanzo.yaml was hand- # edited to delete a route (which is the one thing a generated file must # never receive), and it shipped 19 operations to every SDK that nothing # serves — including `POST /v1/admin/credits`, a money mint hanzo-inc/cloud # had deleted. Measured, not supposed: those 19 are what this gate printed # the first time it was pointed at the committed artifact. # # So this asks the weaker question that needs NOTHING — no checkout, no # token, no ref — and therefore actually runs. Cloud serves its own emission # unauthenticated at /v1/openapi.json, from the binary that is running, and # a route absent from it is refuted by the deployment itself. One direction # only: served-and-unpublished means the pin is behind, which is not a lie # about the API and is `--current`'s question. # # NEVER FAILS ON AN UNREACHABLE API — same rule as hanzo.ai's # scripts/audit-catalog.mjs, which gates the same document the same way. A # gate that goes red for somebody else's outage gets switched off, and then # it gates nothing. run: | set -e if command -v uv >/dev/null 2>&1; then UV=uv; else curl -fsSL https://astral.sh/uv/install.sh | sh UV="$HOME/.local/bin/uv" fi "$UV" run --no-project --python 3.12 --with pyyaml python publish.py --served