Files
zeekay 9a4124c6a1 The re-pin pushes with the credential a protected branch accepts
The nightly re-pin regenerated correctly and landed nowhere. `make verify`
said "no drift", the commit was made, and the push died on `remote: error:
Not allowed to push to protected branch main`, `pre-receive hook declined`.
The forge knows the actions token as User -2 and refuses it at a protected
branch however the rule is written; FORGE_TOKEN, which every client lane in
hanzoai/ci already pushes with, is accepted.

Setting that credential is not enough on its own. `http.extraHeader` is
multi-valued and a url-specific entry does not displace a broader one, so git
sends every header that matches and the forge honours the first Authorization
it reads — actions/checkout leaves one behind, and adding a second only puts
the refused token in front. The step now clears what checkout wrote before
setting its own, and pushes to GITHUB_SERVER_URL, the host the run belongs to
and the one that takes the token.

FORGE_TOKEN also reads hanzoai/cloud, so the step carries one credential
rather than two.
2026-08-12 20:35:49 -07:00

164 lines
9.5 KiB
YAML

name: Drift Gate
# THE QUESTION THIS JOB ASKS IS NOT THE ONE `cargo test` ASKS, and that is the
# whole reason it still exists.
#
# The gate itself — the command tree and the server held against each other in
# BOTH directions — is now HERMETIC: it rules on `spec/live.json`, the checked-in
# evidence, and runs inside `cargo test` on every push with no network at all.
# That was the fix for the oldest problem this pipeline had: a gate that needs the
# network in CI gets switched off, and a switched-off gate is worse than none.
#
# What no committed file can answer is whether PRODUCTION STILL SAYS SO. This
# drift is almost never introduced by a commit here — the route table moves when
# hanzoai/cloud deploys — so a repo that only checks itself when it changes learns
# about it at the next release, from a user. `make live-check` re-asks the running
# server, with a control for every probe, and fails when a VERDICT moved: not when
# a 401 became a 403 (the same fact about the same route, and a gate that goes red
# for that is a gate people learn to switch off), but when something present
# became absent, or absent became present, or a product appeared or vanished.
#
# The remedy it prints is one command: `make live`, commit `spec/live.json`, and
# read what the hermetic gate then says.
#
# WHY IT IS ITS OWN JOB. hanzo.yml's `test:` block runs inside hanzoai/ci's one
# lane, AFTER the client step (fetch the release document, refuse a moved ref),
# the KMS login and the toolchain provisioning. A job that runs last in a shared
# lane does not report on the server — it reports on everything ahead of it, and a
# failure anywhere upstream means the question is never asked at all. It was never
# asked once that way: at the moment this file was first rewritten, `drift.yml`
# had zero runs and the last three CI/CD runs on main were red.
#
# PUSH AND PULL_REQUEST ARE NOT TRIGGERS ANY MORE. On those, `cargo test` already
# rules on the evidence, hermetically, in hanzoai/ci's lane — so re-asking
# production on every push would add nothing except the power for somebody else's
# deploy to redden this repo's builds. The SCHEDULE is the half that matters, and
# `workflow_dispatch` is here so the answer is one click away when somebody asks
# "is the tree still true?" without pushing anything to find out.
#
# `release-matrix.yml` runs `make verify` — the hermetic gate — at the one moment
# the claim reaches users, before a single artifact is minted. That is not a
# duplicate of this job; it is the other question, at the moment it counts.
#
# `.hanzo/`, not `.github/`: the runners belong to git.hanzo.ai and github.com has
# none, so a pipeline under `.github/` queues forever instead of building.
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
jobs:
live:
name: The live server still says what the committed evidence records
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/checkout@v4
# `toolchain: stable` is STATED, not inferred from the `@stable` ref.
# dtolnay/rust-toolchain reads the branch name out of `github.action_ref`
# to decide the toolchain — that is GitHub-runner behaviour, and act_runner
# on our own forge does not reproduce it, so the action received an empty
# input and failed with "'toolchain' is a required input".
#
# It only ever broke ONE target, which is why it looked like a linux-amd64
# problem: linux-amd64 is the sole `host: true` entry, so it alone runs
# `make test` -> the drift gate, and the gate's own workflow carried the
# same step. Four targets shipped, the fifth did not, and the release
# looked healthy while missing the only binary our own amd64 sandbox can
# run.
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- run: make live-check
# THE PIN MOVES BY PULLING, and this is the only thing that moves it.
#
# It used to be a push: hanzoai/cloud's release was meant to send
# `repository_dispatch: spec-update` and hand this repo the document at the
# sha it deployed. Nothing ever arrived. The CLI has no `.github/workflows`,
# so GitHub recorded the event and ran nothing; the forge has no
# repository_dispatch endpoint to send it to; and a push into this repo puts
# hanzoai/ci's client lane in check-only at the LOCKED ref, so even a
# delivered event could not have written a new one. Across 104 runs the pin
# moved exactly once, by hand.
#
# So the side WITH runners asks. This job already has the network and the
# toolchain and already runs nightly; reading one ref and regenerating costs
# it one more step and needs no dispatch, no new secret and no second
# mechanism. It runs even when `live-check` failed — production having moved
# under the committed evidence is precisely when the pin wants moving.
#
# IT READS main, NOT THE NEWEST TAG. The document is emitted from cloud's
# source, so what it says is a fact about that source and the ref that names
# it is main. A tag names a SERVICE RELEASE, which is a different thing and
# lags: measured the day this changed, cloud's newest tag was four commits and
# two hours behind a main carrying 256 newly typed operations, and re-pinning
# to it would have moved this repo's pin BACKWARDS. Two notions of "the
# document" is the same defect as two documents.
#
# A sha, never `main` itself, because a branch name is not a value: two runs
# of one commit have to read the same bytes for the digest below to mean
# anything.
#
# A RE-PIN IS NOT ALWAYS SAFE TO TAKE, and `make verify` is what decides.
# Cloud's source states a route the moment it is written and production
# deploys it some time later, so the document can name a route the running
# server does not serve; the gate calls that AHEAD and refuses once it runs
# further ahead than the ceiling allows. So verify runs on the regenerated
# tree BEFORE the commit, and the push happens only behind it. When it
# refuses, nothing is written and the red nightly carries the gate's own
# reasons — which is what a person needs to read.
#
# It COMMITS TO MAIN. This used to push a `repin` branch and open a pull
# request; the request was never openable, because this repo has the pull
# unit turned off — the forge answers 500 to `POST /pulls` and 404 to `GET
# /pulls` when it is off — so the step failed every night it had anything to
# do, after pushing a branch nobody could turn into a patch. Two ways to
# land a re-pin, one of which does not exist, is one too many.
#
# FORGE_TOKEN READS THE DOCUMENT AND PUSHES THE RESULT — one credential
# for the whole step. The actions token cannot do the second half: the
# forge knows it as User -2, and a protected branch refuses it however the
# rule is written. It is the same credential, spelled the same way, that
# hanzoai/ci's client lane uses to land a regenerated client.
- name: Re-pin to hanzoai/cloud's main
if: always()
env:
FORGE: https://git.hanzo.ai/v1
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
[ -n "${FORGE_TOKEN:-}" ] || { echo "::error::the re-pin reads hanzoai/cloud and pushes the result here, and needs FORGE_TOKEN"; exit 1; }
api() { curl -fsSL -H "Authorization: token $FORGE_TOKEN" "$@"; }
head=$(api "$FORGE/repos/hanzoai/cloud/branches/main" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["commit"]["id"])')
pinned=$(sed -n 's/^ref=//p' .spec-lock)
if [ "$head" = "$pinned" ]; then
echo "the pin is hanzoai/cloud@$pinned, and that is main"
exit 0
fi
echo "hanzoai/cloud@$pinned -> @$head"
api "$FORGE/repos/hanzoai/cloud/raw/openapi.yaml?ref=$head" -o /tmp/openapi.yaml
printf 'ref=%s\nsha256=%s\nrepo=hanzoai/cloud\npath=openapi.yaml\n' \
"$head" "$(sha256sum /tmp/openapi.yaml | cut -d' ' -f1)" > .spec-lock
# `make spec` fetches that ref AGAIN and refuses if the bytes moved under
# the digest just written — the lock checking itself — then regenerates
# the surface and re-captures the evidence in the same act.
SPEC_TOKEN="$FORGE_TOKEN" make spec
make verify
git config user.name hanzo-dev
git config user.email [email protected]
git commit -am "spec: re-pin to $head"
# ONE CREDENTIAL ON THE WIRE. `http.extraHeader` is multi-valued, and a
# url-specific entry does not displace a broader one: git sends every
# header that matches and the forge honours the first Authorization it
# reads. actions/checkout leaves its own behind, so merely adding ours
# puts the actions token in front — and this step died on `Not allowed
# to push to protected branch main` with the credential that IS allowed
# sitting unused in the environment. Clear first, then set one.
git config --local --name-only --get-regexp '^http\..*extraheader$' 2>/dev/null \
| while read -r k; do git config --local --unset-all "$k"; done || true
git config --local http.extraheader "Authorization: token $FORGE_TOKEN"
git push "${GITHUB_SERVER_URL:-https://git.hanzo.ai}/${{ github.repository }}" HEAD:main
echo "main is pinned to hanzoai/cloud@$head"