Makefile is the front door for day to day work. Every target wraps a plain pnpm or node command, shown next to it below, so you can always run the underlying command yourself.
Prerequisites
Use the Node version from
.nvmrc (for example nvm use) and let Corepack provide the pinned pnpm:
First-time setup
1
Clone and install
make setup checks the toolchain, installs dependencies, and builds every package.2
Run the tests
3
Start the emulators from source
Command reference
Runmake with no target for this list in your terminal. Every target is a single command, so you can always run the underlying one yourself; CI calls pnpm directly.
Add
TEST_FLAGS=--force to any test target to rerun suites even when the cache has them.
| make typecheck | pnpm type-check | tsc --noEmit in every package |
| make lint | pnpm lint | Biome (Ultracite preset) lint and format check over the repository |
| make format | pnpm format | Biome formatting and safe lint fixes |
| make check | see What CI enforces | Everything the CI build job runs, in order |
| make ci | make check docs-check | check plus the docs job |
| make docs | npx -p node@24 -p [email protected] -c 'mint dev --port 3333' in docs/ | Docs site on port 3333 (DOCS_PORT=) |
| make docs-check | the same, with mint validate && mint broken-links | The docs CI job |
| make api-reference | pnpm api-reference | Regenerate docs/api-reference/ from registered routes |
| make api-reference-check | pnpm api-reference --check | Fail on a stale reference or a docs route that does not exist |
| make conformance | pnpm conformance | Regenerate CONFORMANCE.md and its docs snippets |
| make new-provider NAME=acme | pnpm new:provider acme | Scaffold a provider |
| make vendor-spec P=stripe | build, then node scripts/vendor-spec.mjs stripe | Refresh a vendored OpenAPI document |
| make coverage-matrix P=stripe | node scripts/generate-coverage-matrix.mjs stripe | Rewrite a coverage matrix from the routes |
| make spec-allow P=stripe | node scripts/generate-spec-allow.mjs stripe | Write current spec mismatches to spec-allow.ts |
| make release PKG=stripe BUMP=patch | node scripts/release.mjs stripe --bump patch | Prepare a release branch; see Releasing |
| make clean | pnpm clean && rm -rf .turbo | Remove build output and caches |
| make reset | make clean, then remove every node_modules | Start over (then make setup) |
One letter shortcuts: make i (install), make d (dev), make b (build), make l (lint), make f (format), make t (typecheck), make c (check).
Linting and formatting use Biome with the Ultracite preset, configured in biome.jsonc. pnpm install sets up a Husky pre-commit hook that runs ultracite fix on staged files through lint-staged, so commits arrive formatted. Rules the repository turns off are listed in biome.jsonc with the reason for each.
The daily loop
1
Rebuild what you changed
Packages import each other through their built The test targets build dependencies for you, and
dist/ output, so a change in core is invisible to providers until core is rebuilt. make build is cheap after the first run because Turborepo caches unchanged packages.To rebuild one provider and everything it depends on:make dev serves your changes straight from source, so this step is mostly for type checking and scripts that read dist/.2
Test the provider
3
Try it by hand
/_inspector on the service port. To try it the way a user would, with its own config outside the repository’s packages, see Trying your build.4
Run the full check before pushing
Running a provider from source
make dev (or pnpm dev:cli) runs one script, scripts/dev.mjs, with its output prefixed build and serve:
buildwatches every package’ssrc/with the platform’s native recursive watcher. Each provider runs from its owndist/(the CLI finds the workspace package), so a provider change rebuilds that provider, without declarations, in under a second. A CLI change rebuilds the CLI. The CLI and every provider inlinecore, so acorechange rebuilds all of them, a few at a time. Thestate-*packages are loaded from their owndist/, so a change there rebuilds that package. Edits under__tests__are ignored.serveispackages/cli/dist/index.js start. After every good build the script stops it (the CLI stops its providers) and starts it again on the new build.
core, and the emulators restart with the change about a second later. Ctrl+C stops everything. Seeded data is recreated on each restart; pass --state to keep it. Provider dist/ folders are not rebuilt while you work; make build refreshes them.
Every start flag passes through:
make start runs the same CLI once, without watching. See Configuration and Saved state for the full option set.
Trying your build
./baseline (baseline.cmd on Windows) runs this checkout’s CLI. It is a passthrough: every argument reaches the CLI untouched and the working directory is not changed, so anything in ./baseline --help works and config is found from wherever you run it. That includes your own projects:
__tests__) with the time of its last good build, and when something changed it runs turbo run build for the changed packages and everything that depends on them, so a core change rebuilds the CLI and every provider. Turbo’s cache makes an unneeded build cheap. When the build fails it stops rather than start the old build. Two wrapper flags are consumed and never forwarded:
Everything after a
-- belongs to the CLI. make start and make list go through the wrapper. For a loop that rebuilds and restarts on every save, use make dev instead; it skips declarations, so it is faster per change.
make playground (or make playground SERVICE=stripe) creates .playground/, which is gitignored, and runs ./baseline init in it. Then:
baseline.config.json and baseline.lock, and the console with /docs on the base port minus one.
Running the docs locally
http://localhost:3333. Before pushing docs changes, run make docs-check, which is what the Docs (Mintlify) CI job runs (mint validate and mint broken-links).
Mintlify refuses odd (non LTS) Node releases, so the make targets fetch Node 24 through npx for it whatever your own Node is. Keep
MINT_VERSION in the Makefile in step with .github/workflows/ci.yml. See Docs and API reference for what is generated and must not be edited by hand.How Turborepo runs tasks
Task wiring lives inturbo.json:
^build means “build every workspace dependency of this package first”. That is why pnpm test at the root always tests against fresh builds, while pnpm --filter <pkg> test (which bypasses Turborepo) uses whatever is already in dist/. The CLI’s turbo.json adds its own build to test (its suites run the built binary) and the root files its tests read: tests/contracts/, baseline.config.example.json, and the config examples in README.md, docs/, and skills/ (each must pass validate). Concurrency is 100% in turbo.json, one task per core: more than that made builds, type checks, and tests slower while using twice the memory.
How tests share the machine
Every Vitest process starts about one worker per core, so running each package’s suite at once used to start hundreds of Node processes and could exhaust memory.pnpm test runs scripts/test.mjs, which splits the cores instead: at most half of them run suites side by side, and each suite gets an equal share of workers (4 suites with 2 workers each on 8 cores). A single suite keeps Vitest’s default. It counts only suites that will actually run, so cached suites cost nothing. Override with BASELINE_TEST_CONCURRENCY (suites at once), VITEST_MAX_WORKERS (workers per suite), or turbo’s --concurrency. Anything after a second -- goes to Vitest:
providerTestConfig reuse workers across test files (isolate: false). A per-file setup from @deploybaseline/testing restores spies, stubbed globals and env, and fake timers after each file, and gives each file a fresh spec gate sample budget, so results match a fresh process per file. If a suite’s files depend on fresh module state, pass isolate: true to providerTestConfig.
Troubleshooting
A test passes alone but fails in the full run
A test passes alone but fails in the full run
Provider suites share a worker between test files, so a file can see module state another file left behind. Spies, stubbed globals and env, and fake timers are restored for you; anything else (a module level cache, a mutated
process.env key) is not. Reset it in the test, or pass isolate: true to providerTestConfig for that package.A change in core or testing has no effect
A change in core or testing has no effect
The consuming package is reading a stale
dist/. Rebuild with make build, or pnpm --filter "@deploybaseline/<provider>..." build for one provider and its dependencies. If it still looks stale, make clean and build again.pnpm refuses to install a dependency version
pnpm refuses to install a dependency version
pnpm-workspace.yaml sets minimumReleaseAge: 2880, so pnpm will not install a version published less than two days (2880 minutes) ago. Wait, pick the previous version, or, when the fresh version is genuinely needed, add the exact name@version to minimumReleaseAgeExclude.A package's install script did not run
A package's install script did not run
allowBuilds in pnpm-workspace.yaml lists packages whose build scripts are deliberately disabled (esbuild, sharp, and others). This is expected.Port already in use
Port already in use
The base port is 4000, or
BASELINE_PORT or PORT when set. With no config, each default service listens on the base port plus its fixed registry slot (GitHub 4001 through Twilio 4012). Stop the other process, or move everything with --port 5000. Tests use startService from @deploybaseline/testing, which listens on a free port, so they do not collide with a running emulator.Lockfile errors in CI
Lockfile errors in CI
CI installs with
pnpm install --frozen-lockfile. Commit pnpm-lock.yaml whenever you add, remove, or change a dependency, including after pnpm new:provider.