fix(ci): sniff-test bunyip-web/Cargo.toml for axum before publishing :latest #306

Merged
YousifShkara merged 3 commits from fix/BUNYIP-299-build-web-axum-guard into main 2026-07-01 09:38:35 +02:00
Owner

On 2026-07-01T04:58:56Z, dev.a8n.run/psa-systems-private/bunyip-web:latest was overwritten with a build of commit 2630bf8 ("Release v0.1.1", 2026-05-28). That commit predates 20c6c62 (2026-05-30, "a8n.tools parity - SSR frontend") which pivoted bunyip-web from a Dioxus WASM SPA (Caddy on :8080) to the current Axum SSR frontend (Maud + htmx on :4400). Once c-01 pulled the mispushed :latest, Traefik dialed the labelled port 4400, hit a container listening on 8080 (Caddy JSON confirmed the SPA image), and returned 502 on every path of a8n.systems until we caught it. The image label org.opencontainers.image.revision = 2630bf8 on the running container was the smoking gun.

Timing lines up almost exactly with the PSA-48 history-rewrite force-push finishing. The workflow's Resolve publish mode block (lines 91-101) picks mode = latest for any main-branch push, and there is no content sniff test between "checkout ran" and "publish :latest", so a stale ref that reaches the push code path silently overwrites the tag.

Add a nu-shell guard step that runs before every mode resolution and every image push: parse bunyip-web/Cargo.toml, verify axum is a direct dependency, and exit 1 with a pointed error if it is not. The guard is zero-assumption about the trigger's ref plumbing (works on push, tag, workflow_dispatch alike) and catches the specific class of failure that just happened. Every bunyip-web tree since the May-30 pivot has axum = ... in bunyip-web/Cargo.toml; every pre-pivot Dioxus tree has dioxus = ... and no axum. Verified against v0.2.0 through v0.5.0 (all pass) and against 2630bf8 (correctly rejected). The check completes in well under a second.

Header comment on the workflow file gets a paragraph explaining the incident and why the guard is there, so a future edit does not remove it without understanding what it defends against.

The follow-up ticket for retro-restoring :latest on the registry is the merge itself: this PR touches .forgejo/workflows/build-web.yml, which is inside the workflow's own paths: filter (line 34), so merging fires build-web.yml on push: branches: main with current Axum HEAD. That build publishes a fresh, correct :latest as a side effect. Operator then runs docker compose pull app && docker compose up --detach --force-recreate app in docker/server/c-01/bunyip-web to swap the mispushed image out.

#BUNYIP-299

On 2026-07-01T04:58:56Z, `dev.a8n.run/psa-systems-private/bunyip-web:latest` was overwritten with a build of commit 2630bf8 ("Release v0.1.1", 2026-05-28). That commit predates 20c6c62 (2026-05-30, "a8n.tools parity - SSR frontend") which pivoted bunyip-web from a Dioxus WASM SPA (Caddy on :8080) to the current Axum SSR frontend (Maud + htmx on :4400). Once c-01 pulled the mispushed :latest, Traefik dialed the labelled port 4400, hit a container listening on 8080 (Caddy JSON confirmed the SPA image), and returned 502 on every path of a8n.systems until we caught it. The image label org.opencontainers.image.revision = 2630bf8 on the running container was the smoking gun. Timing lines up almost exactly with the PSA-48 history-rewrite force-push finishing. The workflow's Resolve publish mode block (lines 91-101) picks mode = latest for any main-branch push, and there is no content sniff test between "checkout ran" and "publish :latest", so a stale ref that reaches the push code path silently overwrites the tag. Add a nu-shell guard step that runs before every mode resolution and every image push: parse bunyip-web/Cargo.toml, verify `axum` is a direct dependency, and exit 1 with a pointed error if it is not. The guard is zero-assumption about the trigger's ref plumbing (works on push, tag, workflow_dispatch alike) and catches the specific class of failure that just happened. Every bunyip-web tree since the May-30 pivot has `axum = ...` in bunyip-web/Cargo.toml; every pre-pivot Dioxus tree has `dioxus = ...` and no axum. Verified against v0.2.0 through v0.5.0 (all pass) and against 2630bf8 (correctly rejected). The check completes in well under a second. Header comment on the workflow file gets a paragraph explaining the incident and why the guard is there, so a future edit does not remove it without understanding what it defends against. The follow-up ticket for retro-restoring :latest on the registry is the merge itself: this PR touches .forgejo/workflows/build-web.yml, which is inside the workflow's own `paths:` filter (line 34), so merging fires build-web.yml on push: branches: main with current Axum HEAD. That build publishes a fresh, correct :latest as a side effect. Operator then runs `docker compose pull app && docker compose up --detach --force-recreate app` in docker/server/c-01/bunyip-web to swap the mispushed image out. #BUNYIP-299
fix(ci): sniff-test bunyip-web/Cargo.toml for axum before publishing :latest
Some checks failed
E2E / Playwright against deployment (pull_request) Failing after 5s
Check / fmt + clippy + build + tests (pull_request) Successful in 8m42s
bf63945b91
On 2026-07-01T04:58:56Z, `dev.a8n.run/psa-systems-private/bunyip-web:latest` was overwritten with a build of commit 2630bf8 ("Release v0.1.1", 2026-05-28). That commit predates 20c6c62 (2026-05-30, "a8n.tools parity - SSR frontend") which pivoted bunyip-web from a Dioxus WASM SPA (Caddy on :8080) to the current Axum SSR frontend (Maud + htmx on :4400). Once c-01 pulled the mispushed :latest, Traefik dialed the labelled port 4400, hit a container listening on 8080 (Caddy JSON confirmed the SPA image), and returned 502 on every path of a8n.systems until we caught it. The image label org.opencontainers.image.revision = 2630bf8 on the running container was the smoking gun.

Timing lines up almost exactly with the PSA-48 history-rewrite force-push finishing. The workflow's Resolve publish mode block (lines 91-101) picks mode = latest for any main-branch push, and there is no content sniff test between "checkout ran" and "publish :latest", so a stale ref that reaches the push code path silently overwrites the tag.

Add a nu-shell guard step that runs before every mode resolution and every image push: parse bunyip-web/Cargo.toml, verify `axum` is a direct dependency, and exit 1 with a pointed error if it is not. The guard is zero-assumption about the trigger's ref plumbing (works on push, tag, workflow_dispatch alike) and catches the specific class of failure that just happened. Every bunyip-web tree since the May-30 pivot has `axum = ...` in bunyip-web/Cargo.toml; every pre-pivot Dioxus tree has `dioxus = ...` and no axum. Verified against v0.2.0 through v0.5.0 (all pass) and against 2630bf8 (correctly rejected). The check completes in well under a second.

Header comment on the workflow file gets a paragraph explaining the incident and why the guard is there, so a future edit does not remove it without understanding what it defends against.

The follow-up ticket for retro-restoring :latest on the registry is the merge itself: this PR touches .forgejo/workflows/build-web.yml, which is inside the workflow's own `paths:` filter (line 34), so merging fires build-web.yml on push: branches: main with current Axum HEAD. That build publishes a fresh, correct :latest as a side effect. Operator then runs `docker compose pull app && docker compose up --detach --force-recreate app` in docker/server/c-01/bunyip-web to swap the mispushed image out.

#BUNYIP-299
fix(e2e): soften hub /healthz probe on pull_request so outage-fix PRs are not deadlocked
Some checks failed
E2E / Playwright against deployment (pull_request) Failing after 6m27s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m43s
728a7eaf85
`e2e.yml`'s "Verify deployment is reachable" step hard-fails any pull_request whose reachability probe to `hub/healthz` does not answer `{"status":"ok"}`. Reality bit us on BUNYIP-299: staging was 502-ing (mispushed Dioxus image at bunyip-web:latest), the PR that would restore staging by fixing CI + firing a fresh :latest build could not merge because its own PR check probed the currently-broken hub, got the same 502, and process.exit(1)-ed. Chicken and egg.

A PR's SHA never deploys - `wait-for-deploy.mjs` runs on push, not pull_request - so the state of the currently-deployed staging is not evidence about the quality of a PR that changes CI plumbing, docs, or infra. Soften the hub probe on pull_request events: `probeHub` already carries a `soft` mode (BUNYIP-185's prod-apex carve-out), extend it to honour `E2E_HUB_SOFT === 'true'` from the environment, and set that env var in `e2e.yml` only when `github.event_name == 'pull_request'`. Production `workflow_dispatch` still hard-checks (`E2E_HUB_SOFT` resolves to 'false' for it), and post-merge push events use `wait-for-deploy.mjs` on a different code path, so staging-broken alerting is intact everywhere else.

Verified locally against the actual 502-ing a8n.systems: with `E2E_HUB_SOFT=true` the probe logs a warning and exits 0; without it, exits 1 as before. API `/health` stays hard everywhere (line 133) since a downed API means nothing else is meaningful. Docstring on `probeHub` documents both soft-mode inputs so a future edit does not remove one without knowing about the other.

Ships alongside BUNYIP-299 on the same branch so the guard PR unblocks itself: the softened probe lets the next CI run pass, PR merges, `build-web.yml` fires (through the newly-guarded path) and publishes a fresh Axum :latest, `just app-restart` on c-01 pulls it, a8n.systems restored.

#BUNYIP-301
fix(e2e): skip Playwright suite when hub soft-probe reports hub not-live on pull_request
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 11s
Check / fmt + clippy + build + tests (pull_request) Successful in 29m57s
Create release / Create release from merged PR (pull_request) Has been skipped
bb12002285
BUNYIP-301 softened the reachability probe so an outage-fix PR is not deadlocked at "Verify deployment is reachable". Next door still open though: the Playwright suite itself talks to the same broken hub. The BUNYIP-299 PR's next CI run proved it - reachability warned and continued, and then `[setup] global.setup.ts` sat for 180 seconds waiting for `form[action="/login"] input[type="email"]` on a page that has no such form. The mispushed Dioxus SPA answers `/login` with its SPA shell HTML; the DOM node the setup waits for lives inside the Axum SSR bunyip-web that isn't running. Playwright times out, `[auth-ui] login.spec.ts` inherits the same failure, whole run fails.

Wire the reachability probe's hub-liveness verdict into the suite gate. `health-check.mjs` now returns a boolean from `probeHub` (true on live JSON body, false on soft-swallowed non-live) and writes `hub_live=<true|false>` to `$GITHUB_OUTPUT`. `e2e.yml`'s reachability step gains `id: reachability`, and the "Run E2E suite" step's `if:` conjunction adds `steps.reachability.outputs.hub_live != 'false'`. Two-line skip note (companion to the "not bootstrapped" note) fires when the gate triggers so a reviewer knows why the suite line was skipped without digging through reachability logs.

Behaviour matrix (unchanged where the ticket says unchanged):

    trigger                 | reachability     | suite gate today       | after this change
    ------------------------|------------------|------------------------|----------------------------
    pull_request, hub up    | passes (hard)    | runs                   | runs (hub_live=true)
    pull_request, hub down  | passes (soft)    | runs, times out        | SKIPPED (hub_live=false)
    push (post-merge)       | not run          | runs                   | runs (output unset -> pass)
    workflow_dispatch stage | not run          | runs                   | runs (output unset -> pass)
    workflow_dispatch prod  | passes (hard)    | runs                   | runs (hub_live=true)

Verified locally against the real 502-ing a8n.systems: `E2E_HUB_SOFT=true GITHUB_OUTPUT=<tmp> node e2e/scripts/health-check.mjs` writes `hub_live=false\n` to the temp file, script exits 0. Fresh matching JS module import + `fs.appendFileSync` guarded on `process.env.GITHUB_OUTPUT` so local invocations no-op.

Ships alongside BUNYIP-299 + BUNYIP-301 on the same branch so the CI hardening trio unblocks in one merge: sniff-test guards `:latest` from a pre-May-30 Dioxus tree; softened probe stops the reachability step from deadlocking outage-fix PRs; skipped-suite gate stops the Playwright suite from wasting three minutes on a browser session against a hub that has no login form to fill.

#BUNYIP-303
YousifShkara deleted branch fix/BUNYIP-299-build-web-axum-guard 2026-07-01 09:38:35 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!306
No description provided.