Skip to content

PayPal sandbox only. No real money moves.

BuildersBuildersPackages

Infra and tooling

Workflows, deploy configuration, repository scripts and the root tooling that enforce the rules.

Verifiedbacked by a real run or committed evidence

Page last changed 7 Nov 2026 (from the git history)

There is no packages/infra. What this page calls infra is the repository's delivery layer: .github/workflows, infra/, render.yaml, scripts/, and the root tooling (ESLint, Prettier, dependency-cruiser, commitlint, husky, coverage thresholds). By the ownership map these paths belong to the QA area (packages/e2e/**, infra/**, .github/**, scripts/** and the root tooling files).

Heavy work (builds, test suites, browsers, screenshots) runs in GitHub Actions. This page says what runs where; the full workflow map is in Operators: CI and deploy pipeline.

Root tooling

Prop

Type

The dependency rule

.dependency-cruiser.cjs allows these imports and treats every other cross-package import as an error:

PackageMay import
sharednothing
engineshared
paymentsshared, engine
voiceshared
servershared, engine, payments, voice
webshared, voice

A second rule, engine-no-node-io, stops the engine from importing fs, net, http, child_process, os and the other I/O modules (except under src/testing and src/node). npm run check:deps runs the cruiser, then scripts/check-engine-purity.mjs (no direct wall-clock or randomness in the engine) and the payments package's own dependency check. The e2e package is not in the rule.

Scripts

ScriptWhat it checks or does
check-ascii.mjsFails on a non-ASCII character in checked files
check-banned-words.mjsFails on a banned word; the list comes from a CI secret or a local file
check-branch-pr.mjsBranch name and pull request title rules (see Contributing)
check-ownership.mjsA pull request may touch one area only, unless it has the cross-area label
check-interface-label.mjsRegenerates the API reports; a changed report needs the interface label
check-bundle-secrets.mjsScans a built web bundle for secret names with values, token-like strings and canary values
check-no-live-host.mjsFails when source or configuration names a live PayPal host
check-engine-purity.mjsNo direct clock or randomness in engine sources
check-coverage.mjsPer-package coverage floors
check-docs-format.mjsLints docs/DECISIONS.md and docs/GATES.md against their formats
check-gates-result.mjsA flag in gates-result.json may be on only with a closed entry in docs/GATES.md
check-fixtures.mjsAudio sidecars, WAV format and size, recorded speech results
check-audit.mjsDependency audit with expiring waivers
classify-change.mjsDecides whether a pull request changes only documentation
smoke.sh and smoke-dist.mjsPost-deploy smoke of the API host; smoke of the built server
gate-g3-early.mjsEarly G3 evidence from older sandbox authorizations (read, or probe)

The scripts have their own tests (scripts/tests, eslint.rules/tests), run by npm run test:tools.

Workflows

The workflow files are in .github/workflows. Grouped by purpose, with the trigger as written in the file:

WorkflowWhat it does
ciThe main pipeline. Jobs: changes, typecheck, lint, format-check, unit, coverage, property, scenarios, deps, secrets, ascii-banned, docs-format, commitlint, ownership, voice, e2e-fixtures, reference-python, differential, tools, package-checks, prod-build and the required roll-up. On a documentation-only change the build, test and type jobs are skipped and the cheap checks still run
pr-metaValidates branch name and title using the script from the base branch; the pull request code is never checked out
interface-checkAPI reports and the interface label
quality-toolsRuns when scripts, lint rules, hooks or root package.json change
auditDependency audit on lockfile changes, on main, and weekly
engine-readiness, server-postgres, payments-evidence, sim-multiday, load, voice-eval, voice-browser, browser-specs, ui-screensPath-filtered: each runs when the files it covers change

Deploy configuration

render.yaml is the Render blueprint for the single Node host: one instance (numInstances: 1) because the stores are files owned by one process, a persistent disk of 1 GB mounted at /var/data, health check /healthz, and autoDeploy: false (deploys come from the workflows through the deploy hook). The build is npm ci --include=dev --ignore-scripts && npm run build -w @gavel/server; the start command is npm run start:prod -w @gavel/server.

The server validates its environment before it starts. A missing or empty required variable, or a PAYPAL_ENV other than sandbox, ends the process with exit code 78 and one JSON line per problem that names the variable and the reason, never a value. Required everywhere: PUBLIC_URL, TOKEN_SECRET, ADMIN_KEY, PAYMENTS, PAYPAL_ENV; with PAYMENTS=sandbox also PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET and PAYPAL_WEBHOOK_ID.

Three GitHub environments exist: staging, production and sandbox-nightly. Secrets live in environment secrets and in the Render and Cloudflare dashboards; .env.example lists names with empty values.

Invariants

  • Secrets are never printed or committed. Workflow values reach steps through env only. gitleaks-history, the secrets job in ci, and check-bundle-secrets.mjs scan for them.
  • Only the PayPal sandbox. check-no-live-host.mjs fails on a live PayPal host and the server refuses any PAYPAL_ENV other than sandbox.
  • A gate flag follows its record. check-gates-result.mjs ties gates-result.json to a closed entry in docs/GATES.md.
  • Coverage floors only rise. The coverage job fails when a threshold is lowered.
  • Workflows are pinned. Third-party actions are referenced by commit hash.

Tests that guard it

scripts/tests/*.test.mjs and eslint.rules/tests/*.test.mjs (run by npm run test:tools in the tools job of ci), and packages/e2e/test/repo-contracts.test.ts, which reads workflow and script files to check they stay wired.

Edit on GitHub

Last updated on