Recommended rollout
Last updated: June 7, 2026
Adding any static analyzer to an existing codebase has the same failure mode: you turn it on, it finds 137 things, the team looks at the wall of red, you turn it off. We watched it happen to ourselves dogfooding Nark on our own SaaS this week — first run, 137 violations. We didn't turn it off, because we've been here before with other scanners. This page is the playbook.
The three phases
Three phases over roughly two weeks. The point of the structure isn't to be slow — it's to make sure the team understands the gate before it starts blocking PRs. Skipping straight to Phase 3 is how scanners get ripped out.
Shadow mode — week 1
Add Nark to CI with continue-on-error: true. The scanner runs on every PR + main push, uploads its audit JSON as an artifact, posts a step summary — but does not block merges. The goal of this phase is the receipt, not the gate: prove to the team that the scan runs reliably, and capture the first baseline.
What to expect: your first run will typically surface 50–250 findings on a codebase that wasn't built with Nark in mind. The bulk of them are almost always concentrated in a small number of packages — framework error-handling is implicit, and Nark's profiles don't know that yet for your framework.
Triage — week 1 → week 2
Walk the baseline. Every violation has one of three outcomes:
- True positive (fix it) — the call really is missing error handling. Wrap in try/catch, expose the error to the caller, add the onError handler the profile says is required.
- False positive (suppress it) — the framework / your wrapper / the call site context already handles the failure mode. Add an entry to
.nark/suppressions.jsonwith areasonfield explaining why. The reason field is mandatory and lives in the commit so it doesn't rot. - Profile is wrong (file it) — if a profile flags a pattern that's definitively not an error path for that package, file a corpus issue at github.com/nark-sh/nark-corpus. Profile tightening is how the scanner gets less noisy for everyone, not just you.
Realistic target: ~80% of the baseline gets suppressed (with reasons), ~20% gets fixed. The 20% that gets fixed is your launch narrative.
Block — week 2+
Drop continue-on-error: true from the Nark step in your workflow. New violations now block PR merges. The suppressions you committed in Phase 2 cover the pre-existing baseline, so the blocker is strictly net-new error-handling gaps introduced by the current PR.
Pair this with nark --diff origin/main..HEAD in PR-mode jobs so the gate is line-level: pre-existing violations on unchanged lines never fail a PR; only the lines the PR actually touched do. The README of the nark repo has the full PR-mode snippet.
Phase 1: the workflow YAML
Drop this into .github/workflows/nark.yml. Three lines matter; the rest is boilerplate.
name: Nark
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
nark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: pnpm install
- run: pnpm prisma generate # if you use prisma
- name: Run nark
id: nark
continue-on-error: true # ← Phase 1: shadow mode
run: |
npx -y nark@latest \
--tsconfig apps/web/tsconfig.json \
--output nark-audit.json
env:
NODE_OPTIONS: '--max-old-space-size=8192' # ← prevents OOM
NARK_TELEMETRY: 'false'
- name: Verify nark produced an audit
if: steps.nark.outcome == 'success' || steps.nark.outcome == 'failure'
run: |
test -f nark-audit.json || {
echo "::error::Nark did not produce nark-audit.json"
exit 1
}
- uses: actions/upload-artifact@v4
if: always()
with:
name: nark-audit
path: nark-audit.json
retention-days: 30The three non-boilerplate lines:
continue-on-error: trueon the scan step — this is what makes it Phase 1 instead of Phase 3. Remove it to flip the switch later.NODE_OPTIONS: --max-old-space-size=8192— GitHub-hosted runners default Node old-gen heap to ~2 GB, which is too small for mid-sized TypeScript projects. The symptom isFATAL ERROR: Reached heap limitand exit code 134. We hit it on saas; you will too. Skip this only on a small project.- The
Verify nark produced an auditstep — without it, an OOM or crash leavesnark-audit.jsonmissing,upload-artifactemits a warning instead of failing, andcontinue-on-error: truereports the whole job as green. We hit this pattern 18 times in a row before noticing. The verify step makes silent-success impossible.
What "baseline" looks like
Our first Nark run on the nark.sh SaaS — a Next.js + Prisma + Stripe + Clerk codebase, ~500 TS files — surfaced 137 violations: 94 errors, 43 warnings.
| Package | Findings | Notes |
|---|---|---|
| next | 70 | Framework error-handling is implicit (try/catch at the route layer). Mostly suppressible with a single pattern entry. |
| undici | 39 | Server-side fetch() is backed by undici under the hood. Same framework-handling story as next. |
| stripe | 9 | Real targets. Stripe errors aren't optional — these get fixed, not suppressed. |
| date-fns | 9 | Likely profile tightening needed. Filed upstream. |
| others | 10 | Long tail across 6 packages. Mixed TP/FP. |
The distribution above is typical for a Next.js + Prisma codebase. The shape will be different for an Express + Postgres stack, but the principle holds: two packages dominate, and those two are usually the framework layer where errors are handled implicitly. Triage the dominant packages first with pattern-level suppression; the long tail is faster to review per-instance.
Why this beats "just turn it on and block"
Two failure modes we've watched teams hit when they skip Phase 1:
- The wall-of-red revert. Day-one merge gets blocked by 137 unfixable-in-the-PR findings. The team rips the scanner out within 48 hours. Net effect: no scanner at all.
- The blanket suppression. Team patches around the gate by
nark-ignore-all-style suppressions just to land the next PR. Net effect: scanner present but lying — looks like coverage, actually ignores everything.
Phase 1 gives the team a week to look at the findings without pressure. Phase 2 produces a justified baseline (every suppression has a reason field someone wrote). Phase 3 is then enforceable because the baseline is real.
See also
- /our-setup — the full scanner stack we run alongside Nark on our own SaaS.
- nark README — Recommended GitHub Actions workflow — the same YAML as above, plus PR-mode (
--diff) and triage commands. - Suppressions reference — the
.nark/suppressions.jsonschema, including the requiredreasonfield and how it integrates with the bot's PR comment.