Skip to content

fix(#60): surface db:push failures instead of swallowing the exit code#200

Open
agjs wants to merge 1 commit into
mainfrom
fix/surface-db-push-failure
Open

fix(#60): surface db:push failures instead of swallowing the exit code#200
agjs wants to merge 1 commit into
mainfrom
fix/surface-db-push-failure

Conversation

@agjs

@agjs agjs commented Jul 25, 2026

Copy link
Copy Markdown
Owner

boringstackCommandStage ran bun run db:push -- --force every cycle but discarded its result — a failed schema push (bad Drizzle schema, or the DB unreachable) was silent, and the gate then ran against a stale/out-of-sync DB, surfacing confusing downstream errors instead of the real cause.

Now a non-zero push short-circuits to a gate failure carrying the raw output, without running the gate.

It does not auto-classify schema-vs-infra from the text — extended review proved that unwinnable (Postgres echoes arbitrary user identifiers and unquoted data in error detail: column "epipe", DETAIL: Key (name)=(socket hang up), Failing row contains (1, EPIPE), so any matcher mis-steers real schema failures). Instead the error carries a decision rule naming both levers (fix app.schema.ts for a column/table/type/constraint/data problem; dev.sh up for a connection/auth/TLS/does-not-exist/too-many-clients problem) and lets the model decide, with no guessed file.

The error key hashes JSON.stringify([stdout, stderr]) — an unforgeable serialization (escapes every byte incl. NUL, encodes the array boundary) with no cwd stripping — so distinct failures get distinct keys (stuck-detection isn't fooled) while the same deterministic failure stays stable.

Panel: PASS (clean). Full suite green; typecheck + lint clean. Infra HARD-abort at the settleGate seam is a separate task (#47).

boringstackCommandStage ran `bun run db:push -- --force` every cycle but DISCARDED its
result. A failed push — a broken Drizzle schema, or the DB not reachable — was silent, and
the gate then ran against a stale/out-of-sync DB, surfacing confusing downstream errors
instead of the real cause. Now a non-zero push short-circuits to a gate failure carrying the
raw output, WITHOUT running the gate.

It does NOT auto-classify schema-vs-infra from the text. Extended review proved that
unwinnable: Postgres echoes ARBITRARY user identifiers AND unquoted data in error detail
(`column "epipe"`, `DETAIL: Key (name)=(socket hang up) …`, `Failing row contains (1, EPIPE)`),
so any token matcher — even quote-blanked or word-bounded — can match a genuine schema/data
failure and mis-steer the model to "bring the stack up / do NOT edit the schema" (the
stuck-inducing direction). Instead the error carries the raw output + a decision rule naming
BOTH levers (fix apps/api/.../app.schema.ts for a column/table/type/constraint/data problem;
dev.sh up for a connection/auth/TLS/does-not-exist/too-many-clients problem) and lets the
model decide. No `file` is set — guessing app.schema.ts would be wrong for a real infra
failure. (A clean infra HARD-abort belongs to the settleGate seam — #47.)

The error KEY hashes the NUL-joined stdout/stderr verbatim (no lowercasing/whitespace/digit
normalization, which is not semantics-preserving for Postgres output), so distinct failures
get distinct keys while the same deterministic failure stays stable across cycles — stuck
detection isn't fooled by a changed failure, nor a changed byte sliding between streams.

Tests: db:push failure is surfaced (not swallowed) and PROVES the gate never runs afterward
(db:push is the last command); the error is NOT auto-classified (raw output + a both-levers
decision rule, no file) even for an adversarial `socket hang up` DETAIL value; distinct keys
for preamble-tail / digit-only / case-only differences and a NUL-delimiter forge attempt, with
the same failure stable. Full suite 0 fail; typecheck + lint clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant