mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
probe(none): a .gitmodules declaring nothing, and no test: key
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# the last submodule was removed; the file outlived it
|
||||
@@ -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-none
|
||||
secrets: inherit
|
||||
|
||||
@@ -15,296 +15,6 @@ 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
|
||||
|
||||
# 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.
|
||||
# A pipeline that both builds and rolls itself out can put an unreviewed image
|
||||
|
||||
Reference in New Issue
Block a user