Skip to content

13 — Quality & Release Policy

Status: Design Date: 2026-05-23 Depends on: master design, all 01–12 sub-specs Research: research/quality-and-testing-best-practices.md


Owns: every gate between “agent wrote some code” and “production accepts it.” Defines test coverage policy, lint/type-check stance, formatting, pre-commit hooks, GitHub Actions CI matrix, branch protection, release process, and per-sub-system skill recipes that implementation agent teams MUST use.

NOT in scope: business logic itself (each sub-system owns its tests); legal sign-off (12); platform pricing (01).

This spec is load-bearing for main branch protection — every other sub-spec ships through these gates.


#Decision
D31Test runner: Vitest + @cloudflare/vitest-pool-workers 0.16.x. Single test toolchain across Workers, DOs, Workflows, Astro.
D32Coverage policy: 100% lines/branches/functions/statements via @vitest/coverage-v8. Exemptions only via /* v8 ignore */ with an inline reason.
D33Lint stack: Biome 2.x for format + style; typescript-eslint (type-aware) for semantics; eslint --max-warnings 0. Pre-commit via husky + lint-staged.
D34TypeScript strictness: strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature, useDefineForClassFields, verbatimModuleSyntax.
D35Branch protection: main requires 7 status checks + linear history + dismiss-stale-reviews. 1 reviewer default; 2 for 04, 05, 12 paths.
D36Release flow: Conventional Commits → release-please for changelog + tag → GitHub Environments-gated production deploy with required reviewer.
D37Status badges: CI, coverage %, last deploy, license, types-strict, security-scan — visible in README + docs-site landing.

  • Vitest + @cloudflare/vitest-pool-workers (cloudflare:test).
  • SELF.fetch(...) for full handler round-trips against the actual wrangler.toml binding graph.
  • Per-worker vitest.config.ts extending a shared vitest.base.ts in packages/config.
  • Hono workers (per ADR-0001 / D40) additionally expose app.fetch(request, env, ctx) for in-process round-trips that bypass the workerd boot cycle — used for fast unit-of-routing tests. SELF.fetch remains the canonical end-to-end gate.
  • runInDurableObject(stub, async (instance, state) => { ... }) for direct invariant assertions on DO instance state.
  • runDurableObjectAlarm(stub) to fire alarm handlers deterministically — used heavily by 04-auction-engine close logic.
  • WebSocket lifecycle covered via state.acceptWebSocket/webSocketMessage invoked under runInDurableObject (used by 06-real-time-hub).
  • introspectWorkflowInstance (vitest-pool-workers ≥ 0.9.0) — first-class Workflow testing.
  • Fake clock via vi.useFakeTimers() for step.sleepUntil ranges.
  • instance.sendEvent testable in-process; waitForEvent resolves against the fake clock.
  • Used by 05-payment-saga for all 15 vertices + every compensation branch.
  • stripe-mock (Stripe’s own OpenAPI mock server) over a local port, started by globalSetup per workspace.
  • Custom narrow stubs only for invoice-payment edge cases that stripe-mock doesn’t simulate (e.g. requires_action → success transition).
  • Idempotency-key reuse assertions enforced.
  • Vitest + happy-dom + Astro Container API (experimental_AstroContainer).
  • renderToString for SSR snapshots; @testing-library/dom queries for island behavior.
  • apps/web runs both jsdom-style island tests AND cloudflare:test-pool tests for endpoint handlers.
  • Miniflare in-memory implementations via cloudflare:test.
  • D1: pair with drizzle-kit generated migrations applied to the test DB in beforeAll.
  • Queues: producer tests assert messages enqueued; consumer tests inject batches via cloudflare:test helpers.
apps/<app>/
src/
test/
unit/ *.unit.test.ts (vitest jsdom or happy-dom)
integration/ *.int.test.ts (vitest-pool-workers, SELF.fetch)
do/ *.do.test.ts (runInDurableObject)
workflow/ *.wf.test.ts (introspectWorkflowInstance)
vitest.config.ts
packages/<pkg>/
src/
test/
*.test.ts

  • Vitest config: coverage: { provider: 'v8', reporter: ['text', 'lcov', 'json-summary'], thresholds: { lines: 100, functions: 100, branches: 100, statements: 100 }, all: true }.
  • Coverage failure breaks CI.
  • Exemptions live behind /* v8 ignore next */ or /* v8 ignore start */ ... /* v8 ignore end */ with an inline reason comment on the same/preceding line. Examples allowed:
    • Branded-type assertion fail-fast for truly-impossible states.
    • Generated D1 migration SQL imports.
    • Third-party ambient types or shim files.
  • A repo-wide pnpm run coverage:audit script (in packages/config/scripts) enumerates every v8 ignore directive and posts the list as a CI comment for review.

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"useDefineForClassFields": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": false,
"lib": ["ES2022", "WebWorker"]
}
}

Per-app tsconfig.json extends this. apps/web/tsconfig.json adds "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"].

  • biome.json at root configures format + import-sort + a curated style ruleset.
  • eslint.config.js (flat config) loads @typescript-eslint with parserOptions.project: true for type-aware rules + astro-eslint-plugin for .astro files.
  • CI runs both in parallel (biome check . --error-on-warnings and eslint . --max-warnings 0).
  • Format check via biome format --check .; no separate Prettier step.
"lint-staged": {
"*.{ts,tsx,astro}": ["biome check --apply", "eslint --max-warnings 0 --no-warn-ignored"],
"*.{json,md,yaml}": ["biome format --write"]
}
  • Run on git commit. --apply auto-fixes safe issues; if lint-staged exits non-zero, commit is blocked.
  • A pre-push hook runs pnpm typecheck to catch type errors before they hit CI.

Seven workflows under .github/workflows/. All workflows use the same node + pnpm setup steps from a shared composite action (.github/actions/setup).

FileTriggersRequired for main?
ci.ymlpush, pull_requestYES
preview.ymlpull_requestYES (preview deploys must build)
deploy.ymlpush to mainn/a (it IS the prod gate)
security-scan.ymlpush, pull_request, weekly scheduleYES
lighthouse-ci.ymlpull_request (apps/web)YES (perf budget pass)
coverage-comment.ymlpull_requestNO (informational PR comment)
release.ymlpush to mainn/a (uses release-please)
compiler-pilot.ymlaffected pull requests, workflow_dispatchNO (isolated native compiler comparison; existing tsc remains authoritative)

Matrix per workspace (web, api, admin, ws, queue, cron, webhooks, docs-site + each packages/*):

  1. pnpm install --frozen-lockfile
  2. pnpm run lint → biome + eslint
  3. pnpm run typecheck → tsc --noEmit -p .
  4. pnpm run test -- --coverage → vitest with 100% thresholds
  5. pnpm run build → wrangler/astro build artifacts
  6. Upload coverage JSON to artifact (consumed by coverage-comment.yml)

Jobs run with concurrency: ${{ github.ref }} to cancel stale runs.

  • pnpm audit --audit-level=moderate
  • gitleaks detect
  • CodeQL (default JS/TS query packs + a custom query for eval/Function in worker code)
  • headers-check.yml smoke test against deployed preview URLs

Budget targets per lighthouserc.json: performance ≥ 95, accessibility ≥ 95, best-practices ≥ 95, SEO ≥ 95. Runs against the deployed preview URL.

Uses davelosert/vitest-coverage-report-action to post a per-PR coverage delta + a heatmap. Fails the action if any file in the diff is below 100%.


main:

  • Linear history enforced (squash merges only).
  • Dismiss stale approvals on push.
  • Required reviews: 1 (default).
  • 2 reviewers required when PR touches: apps/api/**/auction-*, apps/api/**/saga-*, apps/api/**/payment-*, packages/saga/**, packages/auction-engine/**, packages/legal/**, infra/terraform/** — enforced via CODEOWNERS.
  • Required status checks:
    1. ci / lint
    2. ci / typecheck
    3. ci / test (covers coverage threshold)
    4. ci / build
    5. security-scan / audit
    6. security-scan / codeql
    7. lighthouse-ci / perf-budget (only on PRs touching apps/web)
  • Force-push disabled. Branch deletion disabled.

8. Release process (“locking the release”)

Section titled “8. Release process (“locking the release”)”
  • Conventional Commits (feat:, fix:, chore:, docs:, refactor:, test:, perf:, build:, ci:, style:, revert:, plus feat!: / BREAKING CHANGE:).
  • release-please runs on every push to main. It opens (or updates) a single “release PR” with CHANGELOG entries derived from the new commits.
  • deploy.yml runs on every merge to main, but the prod deploy step (wrangler deploy --env production) is wrapped in a GitHub Environment (production) configured with:
    • Required reviewer (you / founder + admin role from 10).
    • Wait timer of 5 minutes for human cancel.
  • A deploy that fails health-check rolls back via wrangler rollback and posts a Sentry release-noted error.
  • Tags created by release-please are protected (rule under “tag protection rules”). Re-pushing the same tag is rejected.

9. Status badges (README + docs-site landing)

Section titled “9. Status badges (README + docs-site landing)”

Embed shields.io badges, ordered:

[![CI](https://github.com/ghardenlab/nowyourlink/actions/workflows/ci.yml/badge.svg)](.)
[![Coverage](https://img.shields.io/endpoint?url=https://nowyourlink.com/badges/coverage.json)](.)
[![Last deploy](https://img.shields.io/github/deployments/ghardenlab/nowyourlink/production?label=last%20deploy)](.)
[![TypeScript strict](https://img.shields.io/badge/typescript-strict-blue)](.)
[![Security scan](https://github.com/ghardenlab/nowyourlink/actions/workflows/security-scan.yml/badge.svg)](.)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-Contributor%20Covenant-blueviolet)](CODE_OF_CONDUCT.md)

Coverage badge is served by a tiny endpoint Worker at nowyourlink.com/badges/coverage.json produced by ci.yml’s coverage step.


For implementation, consult the row for the subsystem being changed. Load only the skills and references needed for that task. Skill and agent tool names vary by client; the review and testing outcomes below apply across clients.

The names below identify the preferred workflows when installed. A missing plugin is not a reason to skip its testing or review outcome: use available tools for the applicable checks and report the substitution. Never claim an unavailable tool or independent reviewer ran. Request external reviewer messages only within the user’s authorization. Local implementation and validation can continue without repeatedly asking for approval.

  • New feature: workflows:tdd-cycle (red → green → refactor enforced).
  • Bug fix: superpowers:systematic-debugging first, then workflows:tdd-cycle.
  • PR review (before merging): workflows:full-review (multi-agent comprehensive) + pr-review-toolkit:review-pr.
  • Pre-merge correctness sweep: pr-review-toolkit:code-reviewer + pr-review-toolkit:silent-failure-hunter + pr-review-toolkit:type-design-analyzer + pr-review-toolkit:pr-test-analyzer.
  • Security-sensitive change: security-review + tools:security-scan.
  • Real-app verification: verify skill (drives the actual app to confirm behavior).
  • Refactor: tools:refactor-clean + tools:tdd-refactor.

Coverage note. This table is populated per-sub-system as each recipe is authored, so it is intentionally incomplete — rows are added in the PR that lands (or substantially advances) the corresponding sub-system, not all at once. A spec that is not yet listed (e.g. 14 design-system, 16 acquisition-seo-social) simply has no canonical recipe authored yet; until a row exists, fall back to the per-task workflows in §10.1 and the default pr-review-toolkit:review-pr sweep. The “teams MUST consult this table” rule applies to the rows that exist — a missing row is a not-yet-authored recipe, not an exemption from §10.1.

SpecPrimary skillsSpecialized review agents
01 foundation-infratools:deploy-checklist, tools:k8s-manifest (n/a, but adjacent: terraform-validate)pr-review-toolkit:code-reviewer
02 identity-onboardingworkflows:tdd-cycle, tools:security-scansilent-failure-hunter, type-design-analyzer
03 ad-moderationworkflows:tdd-cycle, tools:ai-review (for Workers AI integration)code-reviewer, pr-test-analyzer
04 auction-engineworkflows:tdd-cycle (strict — DO state invariants), superpowers:test-driven-developmenttype-design-analyzer, silent-failure-hunter
05 payment-sagaworkflows:tdd-cycle + tools:api-mock (Stripe), security-reviewsilent-failure-hunter (mandatory), code-reviewer, pr-test-analyzer
06 real-time-hubworkflows:tdd-cycle, superpowers:test-driven-developmenttype-design-analyzer, code-reviewer
07 public-site-archivefrontend-design:frontend-design, tools:accessibility-auditcode-reviewer, comment-analyzer
08 advertiser-dashboardfrontend-design:frontend-design, tools:data-validation (pixel events)silent-failure-hunter, code-reviewer
09 engagement-featuresfrontend-design:frontend-design, tools:accessibility-auditcode-reviewer
10 admin-consolefrontend-design:frontend-design, workflows:security-hardeningcode-reviewer, silent-failure-hunter
11 docs-sitetools:doc-generate, frontend-design:frontend-designcomment-analyzer
12 legal-compliancetools:compliance-check, security-reviewcode-reviewer (templates), comment-analyzer
13 quality-and-release-policy (this)claude-md-management:claude-md-improver, claude-automation-recommendercode-reviewer (workflows + scripts)
15 engagement-marketingworkflows:tdd-cycle (strict — sealed-bid + consent invariants), tools:compliance-check (DOI / UWG §7 / GDPR opt-in proof), security-review (token-kind confused-deputy, RFC 8058 unsub seam), superpowers:test-driven-development (verify-the-zero probes on every type-aware rule)silent-failure-hunter (mandatory — every producer emit / ledger write / attribution flip is best-effort .catch()-swallowed), code-reviewer (sealed-bid template lint + no-amount-join schema test + legal copy), pr-test-analyzer (per-signal smuggled-amount-fails-.parse() + soft-bounce-does-not-block-3DS + capped-push-makes-zero-/push/send coverage)
16 acquisition-seo-socialworkflows:tdd-cycle (red → green → refactor, one PR per [16-x][Pn] issue), superpowers:test-driven-development (100% coverage, no live network — ship a mock double, D48), seo-audit (on-demand Unlighthouse/lychee/JSON-LD vs a preview — review-only, D47), social-post-compose (draft → human approval → enqueue to q-social-publish), the scheduled .github/workflows/growth-routines.yml Action (#800 — runs weekly-growth-report + seo-audit headless on a weekly cadence; its only repository-write scope is issues: write so it can only open review-artifact issues, never push/merge/publish, D47); reuse-don’t-fork apps/web/src/lib/seo.ts, apps/cron/src/scheduled.ts lanes, @nowyourlink/social-adapters, @nowyourlink/pixel + METRICS, consent #727/#739silent-failure-hunter (mandatory — every adapter publish / search-ingest / link-probe is a never-throw PublishResult/result with a retryable flag, plus the cron lanes’ per-item .catch() isolation), code-reviewer (no-PII / k-anon admin readouts #753, free-text-vs-CHECK enum hygiene, the human-approval gate is never bypassed), pr-test-analyzer (per-lane idempotent-re-run-is-a-no-op + D48 no-live-network + nothing-auto-publishes coverage)
17 email-template-systemRecipe — “add/modify an email template”: workflows:tdd-cycle / superpowers:test-driven-development (red → write the template’s harness test calling renderAndAssertEmail(<MyEmail/>) and watch it fail; green → author the .tsx wrapping BaseLayout + primitives until the harness passes; refactor → extract copy into i18n string tables, no translated/{{var}} literals in JSX). Gates: the #808 harness (@nowyourlink/email-templates/test — asserts on the rendered {html,text}, never source JSX: <102 KB, all-CSS-inlined, table layout, alt/width/height, plaintext-has-every-CTA-URL, AA contrast, caniemail lint) and the #811 drift check (pnpm email:publish --dry-run validates the case-sensitive {{{VAR}}} contract; --check-drift is read-only/CI-safe). Preview with pnpm --filter @nowyourlink/email-templates email; sign off rendering/dark-mode in real clients per packages/email-templates/docs/manual-qa-checklist.md before release; then wire the render* fn into the call site. frontend-design:frontend-design for brand-token theme work. Full recipe: packages/email-templates/README.md.code-reviewer (ET4 variant/footer rules — marketing carries List-Unsubscribe + preference + postal, transactional carries none — and the {{{VAR}}} casing contract), pr-test-analyzer (the harness gate and drift --dry-run actually run per template; 100% coverage), comment-analyzer

AGENTS.md is the canonical cross-client entrypoint for quality requirements; CLAUDE.md imports it. Maintain the policy link and completion gate there rather than duplicating the instructions in both files. The gate remains pnpm verify (lint, typecheck, test coverage, build), zero warnings, and the coverage policy above. Use the relevant workflow in §10 for new features and pre-merge review.

The root package.json is the source of truth for the command implementation; do not maintain a second copy of its script here.


12. Acceptance criteria (gate for “13 done”)

Section titled “12. Acceptance criteria (gate for “13 done”)”
  1. tsconfig.base.json, biome.json, eslint.config.js checked in at root with the settings in §5.
  2. Shared vitest.base.ts in packages/config referenced by all workspace vitest.config.ts.
  3. All 7 GitHub Actions workflows live under .github/workflows/.
  4. Branch protection rules applied to main on github.com/ghardenlab/nowyourlink matching §7 — captured in infra/terraform via github_branch_protection resource.
  5. README contains the 7 status badges.
  6. CODEOWNERS enforces the 2-reviewer rule on 04/05/12 paths.
  7. release-please config in release-please-config.json + .release-please-manifest.json.
  8. pnpm verify runs clean on the empty scaffold.
  9. CI runs green on the initial scaffolding PR.
  10. Docs-site (11) surfaces a “Quality bar” page in the Operations nav group pointing at this spec.

  • Q12 (carried from research): verify @cloudflare/vitest-pool-workers 0.16.x has resolved historical Queue consumer flakiness under isolatedStorage. If not yet fixed, document a temporary lower threshold (e.g. 95%) for queue-consumer tests with a tracking issue and forced re-evaluation at every minor bump. Otherwise hold at 100%.
  • Q13: Will release-please for a monorepo emit per-app changelogs (apps/web/CHANGELOG.md) or a single root one? Decide before first release.

  • Test files import from cloudflare:test, not from individual @miniflare/* packages — saves churn when CF rev the pool worker.
  • Drizzle migration tests: use migrate from drizzle-orm/d1-migrator in beforeAll, never raw SQL.
  • DO alarm tests with runDurableObjectAlarm(stub) — set vi.setSystemTime first to fix the clock, then trigger.
  • Workflow tests: prefer introspectWorkflowInstance over manual mocks; only mock the underlying Stripe SDK, never step.do itself.
  • pnpm verify must remain a single command — it’s what tooling, hooks, CI, and humans run. Do not split.
  • New CI workflow files require a corresponding entry in this spec’s §6 table — drift is forbidden.