# Use in CI

Run the same application test on a fresh World for each CI job, with exact dependencies and
cleanup after success or failure. This example uses published packages and ordinary CLI commands;
no platform account, private GitHub action or real vendor key is needed.

## Save a test that uses the real SDK

Use Node 22.6 or newer and npm in an empty directory. Save `package.json`:

```json file=package.json
{
  "name": "ci-app",
  "private": true,
  "type": "module",
  "dependencies": { "stripe": "17.7.0" },
  "devDependencies": {
    "@volter/world": "3.0.63",
    "@volter/twin-stripe": "3.0.1"
  }
}
```

Save `.env.example` with an empty credential:

```text file=.env.example
STRIPE_SECRET_KEY=
```

Save `ci.test.mjs`:

```js file=ci.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import Stripe from 'stripe';

test('the real SDK creates and reads a customer', async () => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
  const made = await stripe.customers.create({ email: 'ci@example.com' });
  assert.equal((await stripe.customers.retrieve(made.id)).email, 'ci@example.com');
  assert.equal((await stripe.customers.list({ email: 'ci@example.com' })).data.length, 1);
});

test('an untwinned destination is refused by the cooperating client', async () => {
  await assert.rejects(fetch('https://api.example.com/v1/anything'));
});
```

Install and generate the config locally; review the detected vendor before keeping it:

```bash
npm install
npx volter world init --name ci-app
```

Commit `package.json`, `package-lock.json`, and the project-owned config, seeds and handlers in
`.volter/`. Its ignore file keeps running state out. Installation needs registry access.

## Walk the job locally

```bash
npx volter world up --sandbox
npx volter world clock set 2026-01-15T12:00:00Z
npx volter world run -- node --test --test-reporter=tap ci.test.mjs
```

```text
# pass 2
# fail 0
```

Sandbox mode refuses untwinned destinations in cooperating clients. It is not enforced network
isolation for arbitrary binaries. The second assertion exercises a Node fetch refusal; see
[sandbox mode](../concepts/worlds.md#sandbox-mode) for the boundary.

```bash
npx volter world log
npx volter world down
```

```text
customer.create
Stopped ci-app
```

This local walkthrough exercises the commands. It does not establish that a hosted CI workflow
has run or that repository settings are configured.

## Put the commands in GitHub Actions

Save this as `.github/workflows/world-tests.yml` in your application repository:

```yaml
name: World tests
on: [push, pull_request]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - run: npx volter world up --sandbox
      - run: npx volter world clock set 2026-01-15T12:00:00Z
      - run: npx volter world run -- node --test --test-reporter=tap ci.test.mjs
      - run: npx volter world log
        if: failure()
      - run: npx volter world down
        if: always()
```

This is application CI, not the catalog's assessment or publication workflow. Each fresh
checkout gets its own local simulated World. On a self-hosted runner with retained state,
reset the task-owned World deliberately before the next suite; ordinary `up` resumes it.
Stop app consumers before teardown. Use separate checkout roots for shards that need isolated data.

## Parallel workers

Use the [isolated CI workers cookbook](../../cookbook/ci-workers/README.md) for a downloadable two-worker example. Each worker installs from the same lockfile into its own fresh temporary checkout root, initializes a uniquely named World, freezes its own clock and runs the same assertion. Separate names alone are insufficient if application state is shared.

The script stops each World in `finally`, attempts teardown after a failed boot, preserves diagnostics and returns nonzero when either work or teardown fails. Browser suites should give each worker its own app service, database and session too; one shared `APP_URL` defeats that isolation. On hosted Actions, use a matrix with a fresh runner checkout per job and keep the existing `if: always()` cleanup step.

## Work from a team's World

If your tests need shared history instead of synthetic defaults, [work from a shared World](./work-from-a-shared-world.md).
That flow requires the shared URL and an appropriately scoped token supplied by your team;
it is separate from the credential-free local job above. [Use the hosted product](./use-the-hosted-product.md)
explains platform access. [Run a full stack](./run-a-full-stack.md) adds application services
and browser tests to the local job.

<!-- Fenced tutorial executor: packages/cli/src/journeys/tutorial.ts. Execution scope is recorded separately. -->
