Skip to content

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.

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.

The CI step starts the twin, exports the base URL, then runs tests. Conceptually:

# Pseudocode — see guides/ci/ for per-platform syntax
steps:
- 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 test

Concrete per-platform examples land alongside this section as it grows.

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:e2e

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:

  1. Boot-time assertion in your test setup. Refuse to run if process.env.VITE_STRIPE_BASE_URL doesn’t match an allowed-twin URL pattern. Fail loudly before any test ships a request.
  2. 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.com in CI output, you know immediately.

These are CI-equivalents of the production safety check.

  • 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