21 Commits
Author SHA1 Message Date
zeekay 28c0db22ea perf(site): ask the origin's sample all at once, hash on every core, and say where the time went
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.
2026-10-20 07:42:29 -07:00
zooqueenandHanzo Dev 805c91c7bb fix(site): a preview the edge does not serve reads back from cloud's record
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]>
2026-10-15 05:27:13 -07:00
zeekay 30af8df1e7 fix(site): the sampled asset check does not glob the brackets in a route's chunk name 2026-09-30 07:09:57 -07:00
zeekay fece5973c3 feat(site): identical objects of a sibling site are copied by the plane, not sent 2026-09-30 06:56:52 -07:00
zeekay 524eac6ff1 fix(site): read the previous manifest from the host that serves the prefix
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-30 01:12:27 -07:00
zeekay fb8ace6f61 perf(site): upload each batch through one parallel curl; retry only what was not accepted
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-30 01:12:24 -07:00
zeekay cae4ed5d93 fix(site): the action names the ref whose bin/site runs; the completion is asked again on a 5xx
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-30 01:12:22 -07:00
zeekay 84b05273ab feat(site): publish only what changed; sites publish in parallel
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-30 01:12:18 -07:00
zeekay e607b8bfc6 Read back only the answer that carried the body
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
2026-09-24 10:29:00 -07:00
zooqueenandHanzo Dev 3c1989c933 fix(site): increase upload attempts to 5 with 60s timeout to prevent socket drop failures
Hanzo CI/CD / cicd (push) Failing after 1m40s
CI/CD / gate (push) Failing after 1m41s
Runner image / build (amd64, linux-amd64) (push) Failing after 2m12s
Runner image / build (arm64, linux-arm64) (push) Failing after 3m2s
Runner image / merge (push) Skipped
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-20 02:25:15 -07:00
zeekayandHanzo Dev d8a47494cd A gated preview answers the read-back 401, and that is the gate working
Hanzo CI/CD / cicd (push) Failing after 2m14s
CI/CD / gate (push) Failing after 2m14s
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]>
2026-09-16 14:53:31 -07:00
zeekayandHanzo Dev 9e9d121347 Markdown ships as text/markdown
Hanzo CI/CD / cicd (push) Failing after 3m56s
CI/CD / gate (push) Failing after 58m35s
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]>
2026-09-11 00:21:54 -07:00
zeekayandHanzo Dev 4d1bd88ff0 Publishing to a project and opening one are separate rights
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]>
2026-09-09 13:02:08 -07:00
zooqueenandHanzo Dev 92e886af90 Retry an object the way we already retry a call
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]>
2026-09-04 16:54:44 -07:00
zeekay eee00a2ed3 site: publish assets before documents
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run
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.
2026-08-26 11:20:35 -07:00
zeekay e310c06502 comments describe present behaviour, not the edit that produced it
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.
2026-08-21 08:06:27 -07:00
zeekay cba78c079b comments state the mechanism, not the day it bit us
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.
2026-08-21 08:04:11 -07:00
zeekayandHanzo Dev 628fddb5d1 call it Hanzo Git, which is its name
"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]>
2026-08-20 19:39:04 -07:00
zeekayandHanzo Dev 9025bd4d14 the read-back had no case, so the wire refused its own address
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]>
2026-08-20 19:31:37 -07:00
zeekay 9aa35288c9 site: read the published page back before calling the deploy done
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.
2026-08-19 22:12:06 -07:00
zeekayandHanzo Dev fc8fbf8f02 site
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]>
2026-08-14 18:58:56 -07:00