Skip to content

CI integration

Tales is designed to be CI-friendly out of the box: a single static binary, deterministic seeded runs, JUnit XML for dashboards, JSONL for log pipelines, and a visual HTML report you can upload as an artifact.

This page shows minimal but production-ready recipes for the three most common CI systems.

A complete Tales job has three concerns:

  1. Install the binary (Homebrew, the tales-testing/setup-tales-action GitHub Action, the release archive, go install, or a source build).
  2. Run the suite with a deterministic seed, a sensible parallelism, and outputs for all three reporters.
  3. Publish artifacts: JUnit for the dashboard, the HTML report and JSONL for forensic debugging.

If you run on GitHub Actions, prefer the official tales-testing/setup-tales-action over curl-ing the release tarball. It pins a version, caches the binary, and exposes tales on PATH for subsequent steps.

- name: Install Tales
uses: tales-testing/setup-tales-action@v1
with:
version: v0.1.0 # pin a tag in production. Use "latest" only for spike PRs.
- name: Run scenarios
run: |
tales test ./e2e/pass \
--seed 1234 \
--parallel 8 \
--timeout 10m \
--report-junit ./reports/junit.xml \
--report-jsonl ./reports/events.jsonl \
--report-html ./reports/visual.html

The GitHub Actions recipe below uses the action; the curl-based fallback is kept for environments that cannot reach the marketplace.

.github/workflows/integration-tests.yml
name: Integration tests
on:
pull_request:
push:
branches: [main]
jobs:
tales:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
steps:
- uses: actions/checkout@v4
- name: Install Tales
uses: tales-testing/setup-tales-action@v1
with:
version: latest
- name: Run tests
env:
POSTGRES_DSN: postgres://postgres:test@localhost:5432/postgres?sslmode=disable
BASE_URL: http://localhost:1337
run: |
tales test ./e2e/pass \
--seed 1234 \
--parallel 8 \
--timeout 10m \
--report-junit ./reports/junit.xml \
--report-jsonl ./reports/events.jsonl \
--report-html ./reports/visual.html
- name: Publish test report
if: always()
uses: dorny/test-reporter@v1
with:
name: Tales
path: reports/junit.xml
reporter: java-junit
- name: Upload reports artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: tales-reports
path: reports/

Use a fixed seed in CI:

Terminal window
tales test ./e2e/pass --seed 1234

A fixed seed means:

  • Every run produces the same generated data (emails, passwords, IDs).
  • A red CI build is replayable locally: copy the seed, copy the command, get the same failure.
  • Flake hunting becomes a deterministic exercise instead of a guessing game.

If you want to guarantee coverage across different generated values, run the suite twice with two different seeds in a matrix, but keep the seeds explicit, not random.

--parallel <n> controls scenario-level concurrency. A good default is --parallel 4 to --parallel 8 on a typical CI runner. Steps inside a scenario remain sequential, so chained captures are unaffected.

Always set --timeout in CI. A hung simulator or unreachable database will otherwise sit on a worker until the CI system kills the entire job, with no clue which scenario stalled.

Terminal window
--timeout 10m

When the timeout fires, Tales prints the list of in-flight scenarios and exits with code 1.

CodeMeaning
0All scenarios passed (or skipped).
1At least one scenario failed.
2Parse or validation error.
3Runtime or reporting fatal error.

Wire your CI to treat any non-zero exit code as failure.