Running twins in CI
Continuous integration is where twin substitution pays its largest dividend: every PR can run against twinned external services without burning vendor sandbox quota, without ngrok shenanigans, and without one engineer’s flaky test blocking everyone.
See also:
guides/ci/covers WonderTwin’s general CI strategy and per-platform setup (CircleCI, GitHub Actions, GitLab CI, shell). This guide layers on top with the twin-specific bits.
Two patterns
Section titled “Two patterns”A. Twins as CI services / sidecars — your CI runner starts a twin (or several) as a service container or sidecar process before the test step. Test code points at localhost:<port> exactly like a developer’s laptop. Twins die when the job finishes.
- Pros: fully self-contained job, no external dependency, no cross-PR state leakage
- Cons: startup cost in each job, larger job image
B. Twins as a shared hosted resource — your CI runner points at the same hosted twins your staging app uses (see hosted-non-prod.md). Same env vars.
- Pros: zero in-job startup, single source of twin truth
- Cons: cross-PR state contention is possible (twins may need per-job isolation or reset)
Most teams use A for unit/integration tests and B for end-to-end suites that benefit from a stable hosted environment.
Wiring (pattern A — service container)
Section titled “Wiring (pattern A — service container)”The CI step starts the twin, exports the base URL, then runs tests. Conceptually:
# Pseudocode — see guides/ci/ for per-platform syntaxsteps: - name: Start twins run: wt up stripe plaid --background --port-stripe 4111 --port-plaid 4112
- name: Run tests against twins env: VITE_STRIPE_BASE_URL: http://localhost:4111 VITE_PLAID_BASE_URL: http://localhost:4112 run: pnpm testConcrete per-platform examples land alongside this section as it grows.
Wiring (pattern B — hosted twins)
Section titled “Wiring (pattern B — hosted twins)”The CI step just exports env vars pointing at your hosted twins:
steps: - name: Run tests against hosted staging twins env: VITE_STRIPE_BASE_URL: https://twins.staging.yourco.com/stripe VITE_PLAID_BASE_URL: https://twins.staging.yourco.com/plaid run: pnpm test:e2eVerifying the substitution worked
Section titled “Verifying the substitution worked”Failures here are silent and dangerous — a misconfigured env var means CI accidentally hit the real vendor, which can mean real Stripe charges or real customer rows touched. Two guards:
- Boot-time assertion in your test setup. Refuse to run if
process.env.VITE_STRIPE_BASE_URLdoesn’t match an allowed-twin URL pattern. Fail loudly before any test ships a request. - Service-registry debug log on test setup. Print the resolved URL per service at the top of the test run. If you see
https://api.stripe.comin CI output, you know immediately.
These are CI-equivalents of the production safety check.
TODO / coming later
Section titled “TODO / coming later”- Per-platform examples (cross-link to
guides/ci/circleci.md,guides/ci/github-actions.md,guides/ci/gitlab-ci.md) - Twin state isolation patterns when multiple PRs share a hosted twin
- Parallel test sharding with twins
- Caching twin binaries / images between CI jobs
- Twin-state seeding from fixtures