probe(empty): induce the shape and let the gate answer
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run

This commit is contained in:
zooqueen
2026-08-16 22:09:55 -07:00
parent 14753264cf
commit f7d9df650f
2 changed files with 3 additions and 291 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ name: CI/CD
# build that would have caught it — the tag moves only when a release is cut.
on:
push:
branches: [main]
branches: [main, 'ci/**']
# A v* tag is what produces a published, immutable image tag (branch pushes
# only ever yield sha-<sha7>). Without this trigger a release tag builds
# nothing at all and the CR has no version to pin.
@@ -23,5 +23,5 @@ concurrency:
cancel-in-progress: true
jobs:
gate:
uses: hanzoai/ci/.hanzo/workflows/build.yml@v1
uses: hanzoai/ci/.hanzo/workflows/build.yml@ci/probe-empty
secrets: inherit
+1 -289
View File
@@ -15,295 +15,7 @@ images:
dockerfile: Dockerfile
repo: ghcr.io/hanzoai/ci
test:
- name: go-vet
run: |
set -e
export GOWORK=off
go vet ./...
- name: go-unit
# The whole tree, not a named list — an allowlist stops covering whatever is
# added after it is written. Small repo; ./... costs nothing.
#
# scope_test.go is the load-bearing one: it asserts the surface refuses a
# request with no X-Org-Id and that `?org=` can only narrow. This service
# shipped once with those properties absent and disclosed every org's build
# metadata to the internet, so a red gate here must block the image.
#
# render_test.go is the other one that has to stay green: it pins the
# vendored @hanzo/brand sheet to the hash of the version it claims to be and
# rejects any colour the page names for itself. Both gates are offline —
# checking that we use one design system costs this pipeline no npm, no
# registry and no network.
run: |
set -e
export GOWORK=off
go test -count=1 ./...
- name: one-definition
# The pipeline has ONE address: .hanzo/workflows/build.yml. Nothing runs on
# github.com, so the constraint that used to split this in two — github.com
# resolves a local reusable only out of `.github/workflows` — no longer buys
# anything, and the split it forced was costing the forge instead.
#
# The forwarder is what broke. A caller naming the forwarder made the forge
# take a second hop it cannot take: the run expanded into jobs with no labels,
# no steps and no runner, and reported `failure` with nothing to read. Every
# cicd lane in the fleet had been red that way for a day — insights, cloud,
# universe, and this repo — while the deploy lanes, which name their runner
# directly, stayed green and hid it.
#
# So: one file, no hop, and nothing to keep in sync. This asserts the
# definition is here and holds steps, and that no workflow has reappeared
# under `.github/workflows` to become a second address.
run: |
set -e
def=.hanzo/workflows/build.yml
grep -q '^ - name: ' "$def" || {
echo "::error::$def holds no steps — it is the definition, not a forwarder"; exit 1; }
grep -q '^ uses: ' "$def" && {
echo "::error::$def forwards somewhere — the forge cannot take a second hop, that is what this layout removed"; exit 1; } || true
[ -d .github/workflows ] && {
echo "::error::.github/workflows is back — nothing runs on github.com and a second address is how the fleet went red"; exit 1; } || true
echo "OK: one definition ($(wc -l < "$def") lines), no forwarder, no second address"
- name: gate-runs-before-delegate
# `mode: delegate` chooses WHERE the image is built. It must never choose
# WHETHER the commit was tested. It did: the delegate step was evaluated
# fourth, ahead of every toolchain and ahead of `test:`, and every other step
# carried `if: inputs.mode != 'delegate'` — so a delegated run skipped the
# gate along with the build and finished green in seconds having proven
# nothing. The seam was right and the ORDER was wrong, which is the kind of
# defect that reads as correct in every diff that touches one step at a time.
#
# Two assertions, because either one alone can be satisfied while the gate
# still does not run: the delegate POST must come AFTER the test gate, and
# the test gate must not be delegate-guarded.
run: |
set -e
f=.hanzo/workflows/build.yml
t=$(grep -n '^ - name: Test (per hanzo.yml)$' "$f" | cut -d: -f1)
d=$(grep -n '^ - name: Delegate build to platform (mode=delegate)$' "$f" | cut -d: -f1)
[ -n "$t" ] && [ -n "$d" ] || { echo "::error::cannot find the test gate ($t) or the delegate step ($d) — a rename broke this assertion, fix the assertion"; exit 1; }
[ "$d" -gt "$t" ] || { echo "::error::the delegate step (line $d) runs BEFORE the test gate (line $t) — a delegated build would ship an ungated commit"; exit 1; }
if sed -n "${t}p;$((t+1)),$((t+80))p" "$f" | grep -m1 '^ if: ' | grep -q "mode != 'delegate'"; then
echo "::error::the test gate is guarded by \`inputs.mode != 'delegate'\` — delegate would skip it"; exit 1
fi
echo "OK: test gate at line $t, delegate POST at line $d, gate not delegate-guarded"
- name: inputs-are-strings
# EVERY workflow_call input is `type: string`, and carries a default.
#
# git.hanzo.ai hands a workflow_call input its DECLARED TYPE'S ZERO VALUE
# rather than the value the caller passed. Measured in isolation
# (z/inputprobe, runs 27365 and 27382): a `type: boolean` input read `false`
# when omitted, `false` when the caller passed `true`, and `false` when the
# polarity was inverted and `true` was passed to a `skip_` name. String
# inputs in the SAME probe resolved correctly — `mode` read `buildx` and
# `runner` read its whole JSON array.
#
# So a boolean here is not a type, it is a constant. `tests` was one, and for
# as long as it was, `if: … && inputs.tests` was false on every run of every
# caller and the gate never executed. Reading a number would fail the same
# way at 0. A string's zero value is the empty string, which no comparison in
# this pipeline treats as an instruction — that is why the guards are written
# as `!= 'false'` rather than `== 'true'`, so an empty value runs the gate
# instead of skipping it.
#
# A default is required for the same reason from the other side: an input
# with no default and no value passed is the zero value again, arriving as if
# someone had chosen it.
run: |
set -e
awk '
/^ inputs:$/ { ins = 1; next }
ins && /^[^ ]/ { ins = 0 }
ins && /^ [a-z][a-z0-9_-]*:$/ { name = $1; sub(/:$/, "", name); n++; order[n] = name }
ins && /^ type: / { type[name] = $2 }
ins && /^ default:/ { dflt[name] = 1 }
END {
for (i = 1; i <= n; i++) {
k = order[i]
if (type[k] != "string") { printf "::error::input `%s` is `type: %s` — this forge substitutes the zero value of a declared type, so anything but a string is a constant the caller cannot set\n", k, (type[k] ? type[k] : "<none>"); bad = 1 }
else if (!dflt[k]) { printf "::error::input `%s` declares no default — an omitted one then arrives as the empty string as though it had been chosen\n", k; bad = 1 }
else printf " %-12s string, with a default\n", k
}
printf "OK: %d input(s) checked\n", n
exit bad
}
' .hanzo/workflows/build.yml
- name: verdict-is-unconditional
# The test phase may be skipped. The VERDICT on it may not.
#
# `Test verdict` carries no `if:` at all, so its condition names no input and
# no input value can silence it — and a skipped step is not a failed one, so
# for as long as the only refusal lived inside a step guarded by `inputs.tests`
# the whole gate could vanish and the run still reported success. Skipping the
# tests and passing the tests produced the same signal.
#
# Four assertions, because each one alone can hold while the phase still
# reports green over nothing: the verdict exists, it is unguarded, it sits
# after both steps that can write a receipt, and it sits before anything is
# built or published — so no artifact can be produced from a commit whose
# gate was never adjudicated.
#
# The fifth is the polarity. `!= 'false'` runs the gate for every value except
# the one explicit opt-out, INCLUDING the empty string a substitution failure
# produces; `== 'true'` would skip it for all of them.
run: |
set -e
f=.hanzo/workflows/build.yml
line() { grep -n "^ - name: $1\$" "$f" | cut -d: -f1; }
t=$(line 'Test (per hanzo.yml)'); o=$(line 'Tests not run (declared by caller)')
v=$(line 'Test verdict'); d=$(line 'Delegate build to platform (mode=delegate)')
b=$(line 'Build & push images (per hanzo.yml)')
for pair in "t:Test (per hanzo.yml)" "o:Tests not run (declared by caller)" "v:Test verdict" \
"d:Delegate build to platform (mode=delegate)" "b:Build & push images (per hanzo.yml)"; do
eval "x=\$${pair%%:*}"
[ -n "$x" ] || { echo "::error::the step named '${pair#*:}' is gone from $f — a rename broke this assertion, fix the assertion"; exit 1; }
done
# the `if:` of a step, or nothing: read from its name down to its `run:`
cond() { awk -v s="$1" 'NR>=s && /^ run: /{exit} NR>=s && /^ if: /{sub(/^ if: /,""); print}' "$f"; }
[ -z "$(cond "$v")" ] || {
echo "::error::Test verdict carries \`if: $(cond "$v")\` — the one step that adjudicates the gate must name no input, or an input can switch off the thing that notices"; exit 1; }
[ "$(cond "$t")" = "inputs.tests != 'false'" ] || {
echo "::error::the test gate is guarded by \`$(cond "$t")\` — it must be \`inputs.tests != 'false'\`, so every value except the explicit opt-out RUNS it"; exit 1; }
[ "$(cond "$o")" = "inputs.tests == 'false'" ] || {
echo "::error::the opt-out is guarded by \`$(cond "$o")\` — it must be the exact complement of the gate, so one of the two always writes a receipt"; exit 1; }
[ "$v" -gt "$t" ] && [ "$v" -gt "$o" ] || {
echo "::error::Test verdict (line $v) runs before the steps that write the receipt it reads ($t, $o)"; exit 1; }
[ "$v" -lt "$d" ] && [ "$v" -lt "$b" ] || {
echo "::error::Test verdict (line $v) runs after a build step ($d, $b) — an image would exist before its commit was adjudicated"; exit 1; }
echo "OK: verdict at line $v is unguarded, follows the gate ($t) and its opt-out ($o), and precedes both build lanes ($d, $b)"
- name: tally
# The tally decides whether a gate reported a test at all, and every caller in
# three ecosystems inherits it. Its suite lifts the function OUT of build.yml
# and runs that text, so what is tested is what executes — no second spelling
# to keep in step. Weighted toward what must NOT count: a linter, a build, an
# assertion gate printing `OK:` and a go package that says `[no tests to run]`
# all read as zero, because over-counting would manufacture exactly the
# confidence the verdict exists to refuse. Offline: strings in, a number out.
run: bash bin/tally_test.sh
- name: publishable
# bin/publishable refuses a `build_secrets` name that has not declared itself
# public. That matters because a build_secret is baked in as a --build-arg,
# which `docker history` prints to anyone who can pull — so the key is a
# publishing decision wearing the word "secret". Until this, the only check
# in the fleet lived inside hanzoai/ui's own Dockerfile, for one image.
run: bash bin/publishable_test.sh
- name: publishable-rule-is-one-rule
# The rule is spelled twice on purpose, and this is what keeps it one rule.
#
# build.yml cannot call bin/publishable, because the tools checkout and the
# workflow resolve from DIFFERENT places: a caller pins the workflow at a ref
# its own forge resolves, while the tools step derives its ref from
# GITHUB_WORKFLOW_REF — which git.hanzo.ai does not set, so it falls back to
# `v1` and clones github.com. Measured on run 36473: `derived ref=v1`,
# `cloned https://github.com/hanzoai/ci@v1`. So a step calling a NEW file
# under $CI_HOME is broken on every forge run until a tag moves on a host the
# caller never named. The pipeline has to carry its own rule.
#
# Which leaves the pattern in two files, which is the same shape as the two
# copies of build.yml and gets the same treatment: demand they are identical
# and let the gate, not a reader, be the thing that notices.
run: |
set -e
a=$(grep -oE '^[[:space:]]*PUBLISHABLE_\*\|[^)]*\)' .hanzo/workflows/build.yml | tr -d '[:space:]')
b=$(grep -oE '^[[:space:]]*PUBLISHABLE_\*\|[^)]*\)' bin/publishable | tr -d '[:space:]')
[ -n "$a" ] && [ -n "$b" ] || { echo "::error::publishable rule not found in build.yml ($a) or bin/publishable ($b) — an edit renamed it, fix this assertion"; exit 1; }
[ "$a" = "$b" ] || { echo "::error::the publishable rule differs between build.yml and bin/publishable — build.yml is what runs, bin/publishable is what is tested, so a difference means the tested rule is not the enforced one:
build.yml: $a
bin/publishable: $b"; exit 1; }
echo "OK: one publishable rule, two spellings — $a"
- name: imgver
# bin/imgver decides the version EVERY image in the fleet publishes — this
# workflow's build lane calls it, and so does the imgver composite action the
# repos that hand-roll their own deploy.yml use. A wrong number here is one
# tag covering two digests, which a node running imagePullPolicy:
# IfNotPresent never picks up and no reader can see. Offline and
# deterministic: the registry floor is injected, so it needs no network.
run: bash bin/imgver_test.sh
- name: vendormark
# bin/vendormark is the gate the build lane runs against every checkout: it
# refuses a site-generator vendor's branding in a tracked path, in tracked
# content, in a commit message, in an author identity or in a Co-authored-by
# trailer. Five shipped sites carried it — one serving the vendor's upload
# folder as its favicon — and clearing them took a history rewrite and a
# force-push of every branch.
#
# The suite pins the refusals AND the allowances, and the allowances are the
# larger half on purpose. The vendor's name is also an ordinary English
# adjective: across the 1,065 repos swept it appears innocently in tokenizer
# vocabularies, word lists, ML corpora, packed public-suffix data and an
# upstream Clerk bundle's list of other people's dev hosts. A gate that reds
# a fifth of the estate is a gate someone switches off, so the content rules
# are only strings that cannot occur in prose, and two candidate rules were
# measured and DROPPED for redding that Clerk bundle. Offline and
# deterministic: temp repos, no network.
run: bash bin/vendormark_test.sh
- name: gover
# bin/gover is the gate the build lane runs against every Dockerfile before
# it builds: it refuses a Go builder image older than the go.mod it
# compiles. That failure is not hypothetical — the golang images set
# GOTOOLCHAIN=local, so the mismatch is a hard mid-build death, and
# hanzoai/visor v1.108.16 shipped it. A sweep of the orgs found 54 more
# Dockerfiles already below their own go.mod.
#
# The gate is only worth having if it is exact in BOTH directions: a false
# negative lets the next visor through, and a false positive blocks a build
# that would have worked, which is how gates get skipped. So the suite pins
# the refusals AND the allowances — a newer image than the floor is fine, an
# alpine suffix is not a Go patch, a floating tag warns instead of failing,
# and a multi-module repo is judged by its NEAREST go.mod. Offline and
# deterministic: temp dirs, no registry, no network.
run: bash bin/gover_test.sh
- name: sitepublish
# bin/sitepublish is the whole of the `site:` lane. The lane it replaced was
# dead: it staged with `mc mirror` behind S3_ADMIN_ACCESS_KEY/SECRET_KEY,
# names KMS holds for no org, so it failed closed for every caller — and of
# the 155 hanzo.yml files across the three orgs, not one declared a `site:`.
#
# The suite is weighted toward REFUSALS because the failure that matters here
# is a green publish that verified nothing — indistinguishable from a working
# one until a site quietly stops updating. Three defects of exactly that
# shape are pinned by name: an object-shaped read of a body that is not an
# object, `.releases[]` against the BARE ARRAY the list route returns (which
# resolves nothing and so can never fail for its stated reason), and
# `[ "$a" = "$b" ]` on two empty strings, which is TRUE. Reintroducing them
# turns 3, 9 and 1 of these tests red respectively. Offline and
# deterministic: a curl shim answers the routes from fixtures, no network.
run: bash bin/sitepublish_test.sh
- name: site
# bin/site is the other half of the same plane — the per-file presigned
# route the exports too large for sitepublish's 16 MiB body take — and it is
# what all 15 static repos reach through the composite action. Its suite was
# written and then never wired here, so the one tool in bin/ with the widest
# blast radius was the one tool nothing tested on the way in.
#
# What it pins is the part cloud does NOT do for us. On the archive route the
# server stamps Content-Type and Cache-Control itself; under a presigned POST
# it stamps neither, because the policy carries no condition for either — so
# whatever this script sends is what the object keeps, and the cache-control
# table here is load-bearing rather than advisory. That is why the strings are
# asserted literally: they are a second spelling of apps/sites.CacheControlFor,
# and this suite is the only thing that can notice when the two disagree.
#
# It also pins the edge body limit from BOTH sides, which is the only way a
# size check earns its place: over it, the completion manifest is refused
# before a single request goes out; at exactly it, the publish still happens.
# A guard that can only refuse is a guard that eventually blocks a build that
# would have worked, and those are the ones people learn to route around.
# Offline and deterministic: SITE_PLAN prints the manifest, no network.
run: bash bin/site_test.sh
- name: conflictmarkers
# bin/conflictmarkers runs against every repo in the fleet, so a false
# positive is as expensive as a miss: it blocks a build that would have
# worked, on a file the author cannot legally change. hanzoai/code is that
# case — VOID_CODEBASE_GUIDE.md documents Void's Search/Replace format,
# which deliberately spells itself the way git writes a conflict, and the
# escape used everywhere else here (indent it) would change what the page
# renders. So the suite pins BOTH directions: the .d.ts and unfenced-
# markdown conflicts still fail, and a fenced sample, a tilde fence,
# hanzoai/app's same-line fixture and an eight-bracket heredoc still pass.
# Offline and deterministic: git repos in temp dirs, no network.
run: bash bin/conflictmarkers_test.sh
test: []
# No `deploy:` key exists any more — for any repo. A build ends at a published
# image; what RUNS is a reviewed pin in hanzo/universe, which cd.hanzo.ai reads.