docs: record the CI investigation honestly, including what is still unknown
deploy / test (push) Failing after 7s
deploy / build (push) Skipped

Establishes what is verified (the code passes on the runner host, with
cargo's real exit code), what is narrowed (493/494 die inside the Rust step
before cargo starts; 495 died before step 1), the three theories that were
wrong, and the cheapest next experiment.

Also records the two things that made this expensive: the actions-log API
returns 403 for our token, and my first reproduction piped cargo into `tail`
and reported tail's exit code.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-20 07:33:10 -07:00
co-authored by Claude Opus 5
parent c02dbe2266
commit d23f30e929
+50 -58
View File
@@ -12,80 +12,72 @@ binaries, zero build errors, frontend builds.
## ⚠ CI IS RED — READ THIS FIRST ## ⚠ CI IS RED — READ THIS FIRST
Pushing `main` auto-deploys (`.gitea/workflows/deploy.yml`: push → test → build Pushing `main` auto-deploys (`.gitea/workflows/deploy.yml`: push → test →
→ registry → gw-04's 60s timer). **The `test` job is failing, so the `build` job build → registry → gw-04's 60s timer). **The `test` job is failing, so `build`
has not run and NOTHING FROM THESE COMMITS IS DEPLOYED.** Production is still never runs and NOTHING FROM THESE COMMITS IS DEPLOYED.** Production still serves
serving the previous image. That is a safe state, not a broken one — but it the previous image. That is a safe state, not a broken one.
means the work is pushed and not live.
Two runs failed: 490 (`b653dbf`) and 491 (`128b423`). ### What is established (verified, not inferred)
**Known and fixed:** run 490 failed because `vm_tool_gate`'s shell tests execute - **The code is fine.** On gw-04 itself, in the CI container, against a
the generated hook, which parses its payload with `node`, and CI's CI-shaped Postgres, `cargo test --workspace` exits **0** — captured as an
`rust:1.96-slim` has no node. `deploy.yml` now installs `nodejs`, and the gate explicit `CARGO_RC`, not through a pipe. `npm ci`, `typecheck` and `vitest`
records an `inert` marker when node is missing rather than silently allowing. all pass on that host too.
Verified in a `rust:1.96-slim` container: without node the force-push payload - **The job dies in the "Rust tests" step**, before `cargo` starts: the
returns 0, with node it returns 2. breadcrumb `/tmp/ci-logs/STEP` read `2-rust` and no `rust.log` was ever
created. So the failure is `apt-get`, `git config`, or `docker run` itself.
- Run 490 used the **same workflow file** as the last green run (489), so the
original breakage was not a workflow change.
**Still unexplained:** run 491 carried that fix and failed anyway. The cause was ### What is NOT yet known
NOT determined. Do not guess at it — that is what produced two bad diagnoses
already this session.
To get the actual failure, reproduce the CI job locally (the Gitea logs API Which command in that step fails, and why. The Gitea actions-log API returns
returns 403 with the token in `deploy/compose/.env`, which lacks the actions **403** for the token in `deploy/compose/.env`, so the job log is unreadable;
scope): that is why the workflow now writes everything to `/tmp/ci-logs` **on the
runner host**, which outlives the container:
``` ```
docker network create cm-ci-net ssh gw-04 'cat /tmp/ci-logs/STEP; cat /tmp/ci-logs/rust-step.log'
docker run -d --name cm-ci-pg-local --network cm-ci-net \
-e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres --shm-size=1g postgres:16-alpine
docker run --rm --network cm-ci-net -v "$PWD":/w -w /w \
-e SQLX_OFFLINE=true -e CARGO_NET_GIT_FETCH_WITH_CLI=true \
-e FORGE_TOKEN=<GITEA_TOKEN from deploy/compose/.env> \
-e CM_TEST_DATABASE_URL=postgres://postgres:postgres@cm-ci-pg-local:5432/postgres \
rust:1.96-slim sh -c 'apt-get update -qq &&
apt-get install -y -qq pkg-config libssl-dev cmake git nodejs &&
git config --global url."https://oauth2:[email protected]/".insteadOf "https://git.redclaw.dev/" &&
cargo test --workspace --no-fail-fast'
``` ```
A run of exactly this was in flight when the session ended; its output was going `rust-step.log` (added in `c02dbe2`) captures the docker run's own stdout and
to `/tmp/cilocal.log`. stderr. **Read it first.**
Candidates worth checking first, none confirmed: **New and important:** run 495 (`c02dbe2`) failed **without touching
- something in the new tests that is Linux- or dash-specific beyond the node `/tmp/ci-logs/STEP` at all** — its mtime stayed at run 494's. Runs 493 and 494
issue (the gate script itself IS verified under dash); did write it. So 495 died *before step 1*, which is a different failure from
- `--shm-size` — the local test Postgres needed 1g because the suite exhausts 493/494 dying inside the Rust step. Two distinct failure modes are in play, or
Docker's 64MB default mid-run, and CI's `cm-ci-pg` sets no `--shm-size`. This 495 was cancelled by the new `concurrency: cancel-in-progress` guard (494 and
surfaces as `could not resize shared memory segment … No space left on device` 495 were pushed minutes apart) and Gitea reports a cancellation as `failure`.
**during migrations**, which reads like a schema fault and is not one. If that
is the cause, the fix is `--shm-size=1g` on CI's postgres, not on our code.
## The thing to do first **Cheapest next experiment, no code change:** trigger a run via
`workflow_dispatch` with nothing else queued, then watch
`ssh gw-04 'cat /tmp/ci-logs/STEP'`. If STEP updates, the pre-step theory is
dead and the Rust step is the target. If it does not, the failure is in
checkout/job setup and `concurrency` is the first thing to remove.
**A runtime image with the `stream-json` fix is built and NOT deployed.** Note the Gitea server is behind Cloudflare on a host not on the tailnet (it is
not web-01 and not gw-04), so reading the job log from its disk was not
possible from here. A token with the `actions` scope would make all of this
unnecessary — that is probably the single highest-value thing to obtain.
``` ### Three wrong theories, recorded so they are not re-tried
clawmates-runtime:streamjson # built 2026-08-19, never run
```
Nothing has yet confirmed end to end that real `tool.call` rows land in 1. **node missing in `rust:1.96-slim`** — real (the `vm_tool_gate` shell tests
`mission_events` from a live mission. That confirmation is the whole point of need it) and fixed, but not the cause; run 491 carried the fix and failed.
the change and it is the one step not taken. Until it runs, treat the fix as 2. **dash vs bash** — the generated gate script is verified correct under dash.
plausible rather than proven — the parser is unit-tested against a captured 3. **Two runs stomping each other** — a real defect (they shared the container
real stream, but no mission has driven it. name `cm-ci-pg`, and a new run `docker rm -fv`s it), fixed with a
concurrency group and a run-scoped name. But run 492 ran alone and failed.
To do it: deploy the image as the mission runtime, launch a mission, then Each of those was inferred from a red build without reading the failure. Do not
add a fourth. Read `rust-step.log`.
```sql ### One mistake worth not repeating
SELECT kind, target, count(*) FROM mission_events
WHERE mission_id = '<id>' GROUP BY 1,2;
```
Expect `tool.call` rows on the container tier for the first time. If they do not My first "reproduction" ran `cargo test ... | tail -80` and reported `exit=0`
appear, the next suspects are `topology_exec`'s frame parser and whether the — which was **tail's** exit code. It proved nothing and sent me down two of the
gateway forwards the provider's `tool_calls` at all — neither has been exercised theories above. The repo already documents this trap.
with a non-empty list.
## Then, in order ## Then, in order