Appearance
Preflight
The single command developers run before opening or updating a PR.
bash
npm run preflightWhat it does
Runs the same checks PR-time CI runs, locally. Faster feedback (~30s for the quick subset) than waiting for a GitHub Actions round trip (~3 min).
Three modes:
| Mode | What runs | When to use |
|---|---|---|
--quick | TypeScript strict + 3 audit scripts | Husky pre-push hook (auto). Sub-30s. |
| default | quick + Next.js build + docs build | Before opening or updating a PR. ~3-4 min. |
--full | default + HTTP smoke against --base-url | Before merging anything risky. Adds ~30s + writes synthetic test data. |
Default mode
bash
npm run preflightRuns:
npx tsc --noEmit— strict TypeScript check across the whole repo.npm run audit:atlas-env-vars— Mongo URI manifest ↔ Terraform check.npm run audit:ecs-task-def— non-Mongo secret manifest ↔ Terraform check.npm run audit:ebs-resources— code references EBS Scheduler ↔ matching Terraform exists in both staging and prod.npm run build— Next.js production build.cd docs && npm run build— VitePress docs build (catches dead links).
Quick mode (pre-push hook)
bash
npm run preflight -- --quickSkips the two builds. Sub-30s. Husky runs this automatically on git push.
Full mode (HTTP smoke + Playwright E2E)
bash
npm run preflight -- --fullAdds two checks on top of the default mode:
- HTTP smoke —
scripts/audit/post-deploy-smoke.tsagainsthttp://localhost:3004(default). Writes synthetic test rows (taha+ci-smoke-<runId>-<checkN>@askflorence.health) and cleans up. - Playwright E2E (ENG-304) — 4 stable-surface specs running in Chromium against the same
baseUrl. ~30s for the whole suite. Writes synthetic test rows (taha+playwright-<spec>-<runId>-<checkN>@askflorence.health) withafterEachcleanup + end-of-run orphan sweep.
Override the target with --base-url:
bash
npm run preflight -- --base-url=https://stage.askflorence.health --fullPlaywright requires Chromium installed locally. First run:
bash
npx playwright install chromiumFor the testing strategy + decision rationale (why Playwright vs ephemeral PR previews), see testing-strategy.md + ADR 0008.
Why this exists
Two precedents in the same week:
- ENG-272 —
RESUME_TOKEN_SECRETwas added tosrc/lib/agent-resume-token.ts(which throws on missing) but never wired intoinfra/envs/{staging,prod}/ecs.tf. The first partial save on/agent-discoverythrew, the try/catch swallowed it, and the resume email never sent for weeks. - ENG-274 —
SCHEDULER_*env vars referenced insrc/lib/agent-reminder-schedule.ts(which silently no-ops on missing) but never wired in Terraform. 15-minute reminders never fired for any agent who signed up between 2026-05-08 and 2026-05-12. - v0.29.9 — TypeScript strict error at
src/app/_home/components/LandingCalculator.tsx:541slipped through review because no PR check rantsc. BrokeDeploy prodworkflow at run 25153366332.
All three would have been caught by npm run preflight running locally before push. PR CI also runs the same checks server-side as a backstop, so the gate is enforced even if a contributor skips preflight.
How big OSS projects handle the same constraint
| Project | Pattern |
|---|---|
| Rust | bors / homu merge queue runs expensive tests once per merge group |
| Next.js | pnpm test aggregates lint + typecheck + build |
| Astro | pnpm preflight is the canonical pre-PR command |
| Kubernetes | Prow runs all required tests per PR with smart triggering |
AskFlorence's pattern: developers run npm run preflight locally; husky pre-push enforces the quick subset; PR CI re-runs the same audits server-side. No HMAC attestation or signed-commit machinery — the audits ARE the attestation, run twice.
State file
On every run, preflight writes .preflight-state.json in the repo root (gitignored) with the HEAD SHA, timestamp, mode, and per-check timings. Purely for the developer's own reference. Inspect with:
bash
jq < .preflight-state.jsonExtending preflight
When a new audit script lands, add it to scripts/preflight.ts's quick array. When a new long-running test suite lands (e.g., Phase 3a Playwright UI flow tests), add it to the --full chain. The shape is designed to grow without restructuring.
Failure handling
Preflight runs every check even after one fails — you see ALL failures in a single run, not one at a time. The exit code is non-zero if any check failed.
For each failure, the summary prints the exact npm command to re-run that check in isolation. Fix the failure, run that command to confirm, then re-run npm run preflight end-to-end before pushing.
Bypassing the pre-push hook
Husky's pre-push hook calls npm run preflight -- --quick. To skip it (emergencies only):
bash
git push --no-verifyPR CI still runs the same checks server-side, so --no-verify won't help you merge — it just lets you push the branch faster when you're confident.
Related
- Testing strategy — the four-layer overview + decision history for why we run things where
- ADR 0008 — decision narrative (Playwright + PR-CI against staging; defer ephemeral PR previews)
scripts/preflight.ts— the orchestrator (repo root).husky/pre-push— the hook that runs--quick(repo root).github/workflows/build-check.yml— PR-time mirror (repo root)infra/secrets/manifest.ts— non-Mongo secrets manifest (repo root)infra/atlas/access-matrix.ts— Mongo URI manifest (repo root)- ENG-284 — the issue that shipped this harness