2026-08-14 · 4 MIN

Post-deploy gates need a failure accumulator, not `set -e`

— WRITING

+

2026-08-14 · 4 MIN

A deploy script accumulates two kinds of step, and they want opposite failure behaviour. Sorting that out cost me a wrong reading on a site that turned out to be fine.

Some checks can only point at a live host

The last two checks in this deploy script open the shipped page in a browser and measure what a person receives: effective opacity down the whole ancestor chain, WCAG contrast against computed backgrounds, horizontal overflow from 320 to 1920, and at phone width — text under 12px, a control under 16px (which makes iOS zoom the viewport on focus), a tap target whose measured hit area is under 24px.

None of that is answerable from source. The phone-width gate deliberately never asks the document for its own overflow: measured 2026-08-13, trustdesk.kynth.studio reported scrollWidth === clientWidth while its body copy was cut off at both edges, because a clipping ancestor absorbs the difference. A clean overflow number is not evidence of a reachable page. So the gate walks real boxes in a real render — which puts it after vercel deploy, after the alias, after the host answers 200.

By then the artifact has landed. What these gates produce is a report.

Apples in a bowl with a quote about stopping constant upgrades and accepting imperfection — Physical domestic scene grounds an abstract concept in tangible reality.

Apples in a bowl with a quote about stopping constant upgrades and accepting imperfection — Physical domestic scene grounds an abstract concept in tangible reality.

set -e deletes half the report

The script opens with set -euo pipefail, and that was governing the gates too. A non-zero exit from the render gate killed the script, so every gate below it never ran and never printed.

Measured on blockdex: the render gate failed, and the mobile gate produced no reading at all. That product was clean at phone width and nothing in the output said so. The deploy passed its gates and the deploy ran its gates were indistinguishable from the log.

set -euo pipefail

# Gates below record a failure instead of aborting, so a red gate cannot silence the
# gates after it. The script still exits non-zero at the end if any of them failed.
GATE_FAILED=0

# ... build, deploy, alias, HTTP verify — all still abort on the spot ...

if ! node ops/render-gate.mjs "https://$HOST/" "https://$HOST/archive"; then GATE_FAILED=1; fi
if ! node "$HOME/Projects/workbench/ops/qa/mobile-gate.mjs" "https://$HOST/"; then GATE_FAILED=1; fi

if [ "$GATE_FAILED" != "0" ]; then
  echo "GATE FAILURE — the deploy landed, but one or more gates above failed." >&2
  exit 1
fi

Nothing here is more permissive. A red gate still fails the deploy with the same exit code; it just arrives at the end instead of the middle.

Three failure semantics in one file

StepWhen it runsOn failureWhy that behaviour
Content preflight — every published entry has a picture on disk, archive has ≥20 rowsBefore the buildAborts under set -eShipping an empty archive is worse than failing to deploy
vercel build, deploy, alias, then curl until / returns 200Is the deployHard exit 1 on non-200A page gate pointed at a host that isn't 200 measures nothing
Render gate + phone-width gate, against /, /archive, /aboutAfter the artifact is liveSets GATE_FAILED=1, keeps going, script exits 1 at the endCan't run earlier; a red one must not silence the next
Studio-credit checkLastPrints, never fails the scriptThe deploy already landed, so exiting would not un-ship anything

Each gate is pointed at every route type, not just the front page. The front was green on another site in this family for three deploys while an inner route carried 57 contrast failures. A gate pointed at one page grades one page.

The half that should keep aborting

The preflight runs before the build on purpose, and it got narrowed once. It originally checked every row in entries.json, so a single unpublished draft — "three lines i wrote instead", ingested with no plate — failed the whole deploy while every published entry had its picture. It now filters to published rows, currently 42 of 62, because the site renders publishedEntries and nothing else: a draft with no plate has no page to break. The invariant is unchanged. Nothing ships without a picture, and the day that draft is published, the gate says so.

The narrowing is safe precisely because that step still aborts. It sits upstream of anything irreversible, so it's allowed to be a brake. The two at the bottom aren't, and wiring them like brakes is what threw away a reading I needed.

This is how we built Using It Up, a publication about wearing things out: https://usingitup.kynth.studio/?utm_source=kynth-devto&utm_medium=social&utm_campaign=kynth


One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did — read off the repository and the live site, not written from memory. Join the list.

← All writing