Files
chat/hanzo.yml
zeekay e3ee8643ca say Hanzo Git, which is what it is
"Forge" named nothing in this estate. The product is Hanzo Git, the address is
git.hanzo.ai, and the in-cluster service is hanzo-git.hanzo.svc. A generic word
invented for it sends a reader looking for a system that does not exist.

Comments only. ("unforgeable" stays — different word.)
2026-08-19 22:27:47 -07:00

156 lines
8.5 KiB
YAML

# Hanzo CI/CD — chat. All real config lives here; .hanzo/workflows/cicd.yml is
# triggers and an import of hanzoai/ci.
#
# NO `images:`, and this file used to declare one. Two lanes were already aimed
# at ghcr.io/hanzoai/chat — this block (platform.hanzo.ai builds every `images:`
# entry on a push to the default branch) and .hanzo/workflows/deploy.yml.
# Importing the reusable would have made a third. Two builders racing one tag is
# how hanzoai/app moved v1.42.225 off the bytes that had just been verified:
# nothing errors, and the pod reports one version while serving another.
#
# deploy.yml is the lane that stays, because it is the lane that has been
# building. Hanzo Git runs #90-#97 are green and #97 is this exact commit. It also
# carries the two things the image cannot ship without: PUBLISHABLE_KEY read
# from KMS (Vite inlines it at BUILD, so a runtime value cannot reach a compiled
# bundle, and the Dockerfile refuses a blank one) and the post-push check that
# the tag resolves. Folding it in here is mechanical later — `build_secrets:
# [PUBLISHABLE_KEY]` reads the same KMS path deploy.yml curls — but changing
# which lane ships hanzo.chat does not belong in the commit that gives this repo
# its first tests.
#
# What was missing was never a builder. It was a gate: deploy.yml runs no test,
# no lint, no typecheck, so hanzo.chat has shipped ungated for 97 builds.
test:
# The build the image itself runs, run before there is an image. `pnpm run
# frontend` is verbatim the Dockerfile's own build line — four workspace
# packages, then the client bundle — so a break here is a break there, reached
# in ~2 minutes instead of after a full layer stack. On a pull request it is
# the only signal that exists: deploy.yml fires on main pushes only.
- name: build
run: |
set -e
corepack enable pnpm
pnpm install --frozen-lockfile
NODE_OPTIONS=--max-old-space-size=8192 pnpm run frontend
# A vite build that emits nothing still exits 0.
test -f client/dist/index.html
# scripts/check-barrel.cjs, against the dist the gate above just produced. Two
# defects that are invisible until the process boots: an import of a package
# @hanzochat/api never declared (rollup inlines it, and an inlined native
# require throws at load), and a name api/server/** destructures from the
# barrel that the barrel does not export (`undefined` until its first call).
- name: barrel
run: |
set -e
node scripts/check-barrel.cjs
# What a user document is allowed to carry out of this process, and who is
# allowed to ask — and whether erasure erases. Forty-one tests over the two
# hops where a credential escapes: the projection every user-returning route sends through
# (services/publicUser — an allow-list, because a deny-list only covers the
# fields somebody thought of), and the schema's own default projection under
# it. Around them, the controller that does the sending, the strategy that
# decides who verifies a request, the org header Commerce authorizes on, the
# standing refusal to enable MCP while nothing filters its tools, and every
# filter in the account-deletion chain naming a field its schema actually
# declares — one of them did not, so deleting an account kept its API keys.
#
# The second factor is not here because it is not ours: IAM owns enrolment and
# verification, and chat verifies the caller's IAM token rather than issuing a
# credential of its own.
#
# These files were each written against a real defect and each mutation-tested
# — every assertion here has been made to fail on purpose. That is what earns
# them the lane; the note below on why no whole suite is gated still stands
# unchanged, and this does not weaken it. A new spec joins by being listed,
# which is a maintenance cost taken deliberately: the alternative is a
# convention in filenames, which would braid what a test proves together with
# when it runs.
#
# The names are checked before they are run. Jest reads its arguments as
# PATTERNS, so a listed file that is not there contributes nothing and the run
# is green on what is left: this lane named ten files and ran six for a while,
# reporting 29 tests where its own note claimed seventy-five. Three of the
# missing four were right to go — 70196c49a1 handed two-factor, verification,
# reset and session issuance to IAM and deleted those subjects — but the lane
# never said so, and the fourth (UserController) was still live and had simply
# lost its cover. A gate that shrinks in silence is the failure this whole
# section exists to prevent.
- name: credentials
run: |
set -e
cd api
SPECS="
server/services/publicUser.spec.js
server/services/CommerceClient.spec.js
server/controllers/UserController.spec.js
server/controllers/__tests__/deleteUserFilters.spec.js
server/controllers/agents/mcpAuthorizationGuard.spec.js
server/middleware/__tests__/requireJwtAuth.spec.js
server/services/iamUser.spec.js
"
for spec in $SPECS; do
test -f "$spec" || { echo "::error::credentials lane names $spec, which is not there"; exit 1; }
done
npx jest $SPECS
cd ../packages/data-schemas
npx jest src/schema/user.spec.ts
# One row, one box. The app's top row is one row to the eye and eight files to
# the code, and each had grown its own answer: measured on production, 44s
# beside a 40, glyphs of 24 and 18, four opaque plates beside four transparent
# glyphs, and a mark rounder than any of them. `components/chrome.ts` is the
# single answer; `chrome.spec.ts` is what stops a second one growing back, by
# reading the source of every file that renders a control in that row.
#
# `Header.spec.tsx` rides along because it is the same lesson in a worse form:
# it holds which controls belong to the header and which belong to the
# sidebar, and it had been dying at IMPORT — `Tests: 0 total`, which reads in
# CI exactly like a file with nothing to say. It sat that way long enough for
# the contract it describes to reverse underneath it. A suite that cannot run
# is not a weaker gate than one that fails; it is a green light.
#
# Both were mutation-tested — every assertion here has been made to fail on
# purpose — which is what earns a lane, same as above. ~2s, no downtime.
- name: chrome
run: |
set -e
cd client
npx jest --coverage=false \
src/components/chrome.spec.ts \
src/components/Chat/Header.spec.tsx
# NO WHOLE jest suite is gated — the `credentials` gate above names eight files,
# one at a time — and that is a measurement rather than an oversight. All five
# suites are red on this commit — cb653548, node 22, pnpm 10.27.0, one full
# local run each:
# client 515 failed / 2356 packages/api 711 / 4644
# api 403 failed / 2737 data-schemas 246 / 1489
# packages/data-provider 227 failed / 985
# The client column is exactly the baseline CLAUDE.md already records (77 suites
# failed / 125 passed), so this is the long-standing state of the repo and not
# something a run introduced. `pnpm lint` does not lint a single file either:
# ESLint 9 loads @typescript-eslint/typescript-estree, which dies against the TS7
# native compiler with "Cannot read properties of undefined (reading 'Cjs')".
#
# Gating any of that ships a lane that is red on arrival and red on every commit
# after, which is read as noise within a day and then ignored — worse than no
# lane, because it looks like coverage. Turn a suite green, then add its line.
#
# Most of the red is one shape, not one bug per failure: tests importing symbols
# the source never exported — and a second shape this file used to hide inside
# the same numbers: suites that die during IMPORT and run nothing at all, which a
# failed/total count cannot distinguish from a suite that ran and disagreed.
# Twelve of the api column were that; eleven run now.
#
# The sharpest instance of the first shape was never merely a test failure. Merge
# 49dc4f7bf6 (LibreChat sync) dropped PERMISSION_TYPE_INTERFACE_FIELDS,
# INTERFACE_PERMISSION_FIELDS and PERMISSION_SUB_KEYS out of
# packages/data-provider/src/permissions.ts while packages/api/src/admin/config.ts
# and packages/data-schemas/src/app/resolution.ts kept importing them, so the
# admin config surface called `.has()` on undefined and threw on every override
# write. FIXED: restored from e77b03ab6d, which took resolution.spec.ts to 26/26
# and admin/config.handler.spec.ts to 54/54, exactly as this note predicted.