WonderTwin in Shell / Docker Compose / Custom CI
WonderTwin in shell scripts, Docker Compose, or custom CI
Section titled “WonderTwin in shell scripts, Docker Compose, or custom CI”The universal escape hatch. Works on your laptop, in make test, in Buildkite, Jenkins, TeamCity, Argo, or any CI platform that lets you run shell commands.
If your CI platform isn’t covered by github-actions.md, circleci.md, or gitlab-ci.md, this is the foundation everyone else’s snippet wraps.
The four ingredients
Section titled “The four ingredients”Every WonderTwin CI integration boils down to four things, in order:
- Install
wt— download the binary for your platform - Install license — write the license JSON to disk and run
wt license install - Install + start the twin —
wt install <twin>@<version>then run it in background - Wrap your tests —
wt runs start→ tests →wt runs finish --export
Capture the export as your CI’s artifact mechanism. That’s the integration.
Snippet — minimal shell
Section titled “Snippet — minimal shell”run-tests.sh:
#!/usr/bin/env bashset -euo pipefail
TWIN="${TWIN:-stripe}"TWIN_VERSION="${TWIN_VERSION:-latest}"TWIN_PORT="${TWIN_PORT:-4111}"TWIN_URL="http://127.0.0.1:${TWIN_PORT}"RUN_ID="${RUN_ID:-$(date +%s)-$$}"ARTIFACT_DIR="${ARTIFACT_DIR:-./artifacts}"
mkdir -p "$ARTIFACT_DIR"
# 1. Install wt (skip if already on PATH)if ! command -v wt >/dev/null 2>&1; then case "$(uname -s)" in Linux*) OS=linux ;; Darwin*) OS=darwin ;; *) echo "unsupported OS"; exit 1 ;; esac case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; arm64|aarch64) ARCH=arm64 ;; *) echo "unsupported arch"; exit 1 ;; esac curl -sSL "https://github.com/wondertwin-ai/wondertwin/releases/latest/download/wt-${OS}-${ARCH}" -o /usr/local/bin/wt chmod +x /usr/local/bin/wtfi
# 2. Install license (if provided)if [[ -n "${WONDERTWIN_LICENSE:-}" ]]; then printf '%s' "$WONDERTWIN_LICENSE" > /tmp/license.json wt license install /tmp/license.json rm /tmp/license.jsonfi
# 3. Start the twinwt install "${TWIN}@${TWIN_VERSION}"wt up > /tmp/twin.log 2>&1 &TWIN_PID=$!
cleanup() { kill "$TWIN_PID" 2>/dev/null || true sleep 1 kill -9 "$TWIN_PID" 2>/dev/null || true}trap cleanup EXIT
# Wait for twinfor i in $(seq 1 30); do curl -sf "${TWIN_URL}/admin/state" > /dev/null && break sleep 0.2done
# 4. Run lifecyclewt runs start --twin "$TWIN_URL" --seed 42 --run-id "$RUN_ID"
# Run your tests — they should hit $TWIN_URLexport WT_TWIN_URL="$TWIN_URL""${@:-make test}"TEST_EXIT=$?
wt runs finish --twin "$TWIN_URL" --run-id "$RUN_ID" \ --export "${ARTIFACT_DIR}/wondertwin-replay.jsonl.gz" || true
exit $TEST_EXITMake executable, invoke with the test command:
chmod +x run-tests.sh./run-tests.sh make testSnippet — Docker Compose
Section titled “Snippet — Docker Compose”docker-compose.test.yml:
services: twin: image: wondertwin/twin-<TWIN>:<TWIN_VERSION> ports: - "<TWIN_PORT>:<TWIN_PORT>" healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:<TWIN_PORT>/admin/state"] interval: 1s timeout: 1s retries: 30
tests: image: <YOUR_TEST_IMAGE> depends_on: twin: condition: service_healthy environment: WT_TWIN_URL: http://twin:<TWIN_PORT> volumes: - ./artifacts:/artifacts command: | sh -c ' wt runs start --twin $$WT_TWIN_URL --seed 42 --run-id $${RUN_ID:-local-$$$$} <TEST_COMMAND>; rc=$$? wt runs finish --twin $$WT_TWIN_URL --run-id $${RUN_ID:-local-$$$$} \ --export /artifacts/wondertwin-replay.jsonl.gz || true exit $$rc 'Twin Docker images aren’t published for every twin yet — see the twin’s README. For twins without a published image, run the binary directly in the test container or use the shell snippet above.
Snippet — Makefile
Section titled “Snippet — Makefile”TWIN ?= stripeTWIN_VERSION ?= latestTWIN_PORT ?= 4111TWIN_URL := http://127.0.0.1:$(TWIN_PORT)RUN_ID := $(shell date +%s)-$$$$ARTIFACT_DIR := ./artifacts
.PHONY: testtest: @./run-tests.sh make test-inner
.PHONY: test-innertest-inner: # your actual test invocation here go test ./...The run-tests.sh from above handles the lifecycle; make test is the user-facing entry.
Variations
Section titled “Variations”wt runs wrap for an even shorter shell snippet
Section titled “wt runs wrap for an even shorter shell snippet”If you don’t need a separate test step, wrap collapses lifecycle + test command into one call:
wt runs wrap \ --twin "$TWIN_URL" \ --seed 42 \ --run-id "$RUN_ID" \ --export "${ARTIFACT_DIR}/wondertwin-replay.jsonl.gz" \ -- make testwrap always finishes the run on exit, regardless of the wrapped command’s exit code.
Multi-twin with manifest
Section titled “Multi-twin with manifest”If you need multiple twins, define a manifest and let wt up start them all:
[[twins]]name = "stripe"version = "0.1.0"port = 4111
[[twins]]name = "twilio"version = "0.1.0"port = 4112wt installwt up &# ... rest as before, but tests hit two URLsCI platform-specific artifact upload
Section titled “CI platform-specific artifact upload”The shell snippet writes to $ARTIFACT_DIR/wondertwin-replay.jsonl.gz. Each CI platform has its own artifact upload mechanism. Translate:
- Buildkite:
buildkite-agent artifact upload "artifacts/*" - Jenkins: the post-build action’s
Archive the artifactsglob set toartifacts/** - TeamCity:
##teamcity[publishArtifacts 'artifacts => wondertwin'] - Argo Workflows:
outputs.artifacts:block referencingartifacts/wondertwin-replay.jsonl.gz - Tekton:
Workspacemounted at/artifacts, fetched by a follow-up Task
The snippet stays the same; only the artifact upload command changes.
The four ingredients in detail
Section titled “The four ingredients in detail”1. Install wt
Section titled “1. Install wt”wt is published as platform-specific binaries on GitHub Releases. The download URL pattern is:
https://github.com/wondertwin-ai/wondertwin/releases/<TAG>/download/wt-<OS>-<ARCH>Where <TAG> is latest or a specific version like v0.4.2, <OS> is linux / darwin / windows, and <ARCH> is amd64 / arm64 (no windows-arm64). The shell snippet detects OS/arch automatically.
For air-gapped environments: pre-bake wt into your CI image or download once and cache.
2. Install license
Section titled “2. Install license”Licenses are signed JSON files. Customers receive them from sales. Install path:
wt license install /path/to/license.jsonThis copies to ~/.wondertwin/license.json (or $WONDERTWIN_HOME/license.json). The runtime reads from there at twin startup.
If you don’t have a license, twins still run — soft gate. The startup banner warns; telemetry attributes the run as unlicensed; replay artifacts are unaffected.
3. Install + start the twin
Section titled “3. Install + start the twin”wt install <twin>@<version> fetches the twin binary into ~/.wondertwin/bin/. wt up reads wondertwin.toml (or the default lock file) and starts every twin listed.
For one twin without a manifest:
wt install stripe@0.1.0~/.wondertwin/bin/twin-stripe & # default port 4111For most CI cases, wt up & is the simplest answer.
4. Wrap your tests
Section titled “4. Wrap your tests”The lifecycle endpoints are documented at /admin/runs/{start,finish,current}. The CLI verbs are wt runs start, wt runs finish, wt runs current, and wt runs wrap for the convenience-wrapper case. See concepts.md for the run-lifecycle model.
Troubleshooting
Section titled “Troubleshooting”Tests can’t reach the twin
Section titled “Tests can’t reach the twin”Confirm WT_TWIN_URL is exported AND your SDK respects the base-URL override. Some SDKs require a flag instead of an env var.
Background wt up & exits immediately
Section titled “Background wt up & exits immediately”wt up blocks while serving traffic; it shouldn’t exit unless the twin crashes. If it does exit immediately:
- Check
/tmp/twin.logfor an error - Confirm the twin is installed:
ls ~/.wondertwin/bin/ - Try running the binary directly:
~/.wondertwin/bin/twin-stripe(you’ll see the startup output)
Health check loop times out
Section titled “Health check loop times out”6 seconds is enough for most twins. If yours takes longer, increase the loop count in the snippet. If it consistently exceeds 10 seconds, file an issue — that’s a regression.
Replay file is empty or missing
Section titled “Replay file is empty or missing”The export step needs the run to have actually started. If the test step crashes before any request to the twin, the manifest will exist but with entry_count: 0 — that’s correct, not a bug. The export will still write the file.
If the file is truly missing, confirm wt runs finish ran (check the script’s log) and that the export path is writable.
Different OS / arch
Section titled “Different OS / arch”Adjust the OS and ARCH detection in the snippet. Windows isn’t supported in the shell pattern (use WSL or a Linux container).
Reference
Section titled “Reference”wt --helpfor the full CLI surfaceconcepts.md— twins, runs, replay, licensestrategy.md— why docs first- Other platforms: github-actions.md, circleci.md, gitlab-ci.md