The twenty sampled assets were asked one after another, each a round trip
through the edge to the origin: 10.4 s from dgx, seconds of every publish
from a runner. They go out together now, and one that does not answer still
sends everything. Hashing runs one batch per core into a file of its own,
sorted after, so the record is the same bytes in any order. A last line,
`timing:`, names each phase's seconds.
A client preview answers 404 to everyone until its client signs off, so the
public read-back cannot reach its bytes. On a 404, and only a 404, the
release is read back from cloud with the credential that published it: the
deployment this run completed must be live, hold the files and bytes this
run sent, and be the one the project serves. Any other answer from the host
still fails the deploy. The hanzo.id redirect a forward-auth gave is no
longer accepted.
Co-authored-by: Hanzo Dev <[email protected]>
curl -L keeps every hop's headers, and the check took the last
content-length it saw. A root that redirected once before a
chunked page was measured against the redirect's 17 bytes, which
failed a healthy publish of dev-hanzo-ai three times over.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
The read-back demands HTTP 200 from the live URL. A client preview cannot give
one: hanzo-apps hosts are guarded, so they answer an API caller 401 and a browser
a redirect to hanzo.id. The same workflow's "every host is gated" step REFUSES to
publish any host that answers otherwise, so the two steps asked one deploy to be
two contradictory things.
It cost 27 of 29 client sites. The first slug of each repo uploaded and flipped
live, the read-back called it unreadable, and `set -e` took the remaining sites
down with it -- leaving hosts that resolve and gate with nothing behind them.
A browser-shaped probe that does not follow tells a gate from a failure: a gate
redirects to hanzo.id naming that host as its own callback, which proves the host
is routed and guarded. Its bytes still are not read back, so the run says so
rather than counting it verified. Every other failure path is unchanged: an empty
body, a short read and a plain non-200 all still fail.
Co-authored-by: Hanzo Dev <[email protected]>
bin/site typed every .md as application/octet-stream, and the Sites
plane serves the type stored at upload when its own table has none.
So the markdown twins on hanzoskills.com and hanzo.ai reached agents
with no usable Content-Type.
Co-authored-by: Hanzo Dev <[email protected]>
The lane creates the project before it publishes, so a slug that has never
deployed needs nothing out of band. It reads a refusal of that create as a
refusal of the whole publish, and those are not the same thing: a key can be
allowed to publish to a project it may not open, which is the ordinary shape of
a deploy credential.
A refusal now asks the question it actually cares about — is the project there —
and one GET answers it. Present means publish; absent means the create really
was needed and the error stands.
Measured on luxfi/cdn: the project answers 200 and every publish had been
failing at the create above it.
Co-authored-by: Hanzo Dev <[email protected]>
The three API calls in this script retry five times each, with a comment
explaining that both are idempotent and that a curl which cannot speak at
all reports 000. The seven thousand object uploads between them retried
zero times, and an object is the MOST idempotent thing here — the same key
with the same bytes can only replace itself.
The cost of that asymmetry is total: one dropped connection out of eight
thousand fails the release, and the deployment is marked `error` having
uploaded 99% of the site. Measured on a laptop publish: 72 of 7980 died,
the other 7908 were already in the bucket, and nothing shipped.
Three attempts with a widening pause, then the caller's error path as
before.
This does not fix the flake it was written for. macOS curl links LibreSSL
and drops connections to s3.hanzo.ai with `sslv3 alert bad record mac`
(curl 56) — at 24 parallel, at 8, and at 1, so it is the TLS stack rather
than concurrency, and a laptop is not where this should run. What the retry
fixes is the shape of the failure everywhere else: a transient drop on a
runner no longer discards a whole publish.
Co-authored-by: Hanzo Dev <[email protected]>
The bytes are live where they land — this publisher writes into the prefix the
edge already serves, so there is no release to flip and every object is public
the moment S3 accepts it. One flat upload pass therefore ordered a document and
the hashed assets it names arbitrarily, and a document is only servable once all
of them are in the bucket.
Splitting the manifest on `.html` supplies that ordering with no coordination.
Asset names carry a content hash, so phase one only ever adds objects and what is
already published keeps serving through it; the document is the switch, thrown
once everything it can name is present. It also makes cancelling a superseded run
safe: the run dies in phase one having added unreferenced objects only.
Verified against a 6,734-file export: 5,786 assets then 948 documents, and all 41
assets index.html names land in phase one.
A comment written as a changelog — "this used to sit fourth", "the lane it
replaced was dead", "until this file existed" — asks the reader to hold two
versions of the code in their head and only describes one of them. The version
it describes is the one that is gone.
Each of these now states the rule the code enforces today and the mechanism
that makes it necessary, which is the part that was doing the work: a
`while read` at the end of a pipeline is a child shell, so `set -e` cannot
reach the step; /deploy reads any body as bytes, so JSON sent there is sniffed
as a tar; /v1/keys answers identically for a bogus and a valid machine key, so
it cannot gate a publish.
All 15 gates in hanzo.yml pass.
This repo is public. A comment that recounts how a specific service broke —
the date, the duration, the named blast radius, the steps that reproduce it —
publishes a map of our failure modes to anyone who clones it, and the reader
learns nothing they could not learn from the mechanism alone.
Every such comment now states the engineering constraint in the present tense:
why Hanzo Git resolves one `uses:` hop and not two, why a `type: boolean`
workflow_call input is the constant `false`, why GOTOOLCHAIN=local turns a
go.mod bump into a mid-build death, why an ignore rule never untracks what the
index already holds, why an object store can answer 200 with zero body bytes.
The facts that make each gate worth having are unchanged; the war stories are
gone.
All 15 gates in hanzo.yml pass.
"Forge" was a borrowed word for the thing that hosts the repos, schedules the
builds and issues the tokens. That thing is Hanzo Git. The word named nothing a
reader could go look at, and it had already drifted: the same paragraph could
say "the forge" and "git.hanzo.ai" and leave you to work out they were one
thing.
forge.go becomes git.go for the same reason — the file reads what Hanzo Git
knows that the run list does not.
Where a second mention would just repeat the name, it says "the server"
instead. Left alone: "unforgeable" and "cannot be forged", which are the other
word entirely and mean what they say.
Co-authored-by: Hanzo Dev <[email protected]>
bin/site downloads the published URL and compares what arrived against the
promised content-length — a HEAD is blind to a 200 that serves nothing. That
check had no case here, so the curl shim never learned its address, and the
shim refuses what it does not know. Every successful publish then came back as
a failed test: four assertions on a zero exit, red since the check landed.
Teach the shim the address, and cover the two things the read-back exists to
catch: a 200 with an empty body, and a body shorter than the content-length it
promised. Fake sleep beside curl while there — the delays reachable from a
shimmed run are backoffs whose length nothing asserts, only their count, and
they cost 72 of the suite's 79 seconds.
The credential refusal was separate: it printed and then exited 0. ${VAR:?...}
reports through the shell, and the EXIT trap set at the top resets the status
on its way out, so a deploy that never started was reported as a success. A
brace in the mint example closed the expansion early and truncated the message
too. sitepublish is unaffected — it installs its trap after the guard.
Co-authored-by: Hanzo Dev <[email protected]>
Upload and flip-to-live both succeeding proves nothing about whether the
bytes come back. hanzo.ai's apex served 200 with a correct content-length,
a valid etag and an empty body for 35 minutes; StatObject knew a size
GetObject could not deliver.
A HEAD cannot see this — it never asks for a body — so every status-code
check reported healthy. Download the page and compare what arrived against
what was promised; curl exits 18 on the short read. Three attempts, so
propagation is not mistaken for an unreadable object.
The tool said what it did twice. `sitedeploy` is a site and a deploy welded
together, and it sat next to `sitepublish` — two compounds sharing one noun,
where the noun alone is the whole meaning. `site hanzo-ai out` reads as what it
is; `sitedeploy hanzo-ai out` reads as a category and a verb fighting for the
same word.
The env vars follow: SITEDEPLOY_JOBS is SITE_JOBS. The composite action is
`hanzoai/ci/.github/actions/site@v1`.
Nothing about the implementation moves. 61 assertions pass before and after,
including the one that was already failing, which is the point of renaming
separately from changing.
Co-authored-by: Hanzo Dev <[email protected]>