fix(ci): sniff-test bunyip-web/Cargo.toml for axum before publishing :latest #306
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/bunyip!306
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/BUNYIP-299-build-web-axum-guard"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On 2026-07-01T04:58:56Z,
dev.a8n.run/psa-systems-private/bunyip-web:latestwas overwritten with a build of commit2630bf8("Release v0.1.1", 2026-05-28). That commit predates20c6c62(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 =2630bf8on 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
axumis 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 hasaxum = ...in bunyip-web/Cargo.toml; every pre-pivot Dioxus tree hasdioxus = ...and no axum. Verified against v0.2.0 through v0.5.0 (all pass) and against2630bf8(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 runsdocker compose pull app && docker compose up --detach --force-recreate appin docker/server/c-01/bunyip-web to swap the mispushed image out.#BUNYIP-299
`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-301BUNYIP-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