GitHub PR Previews

The Contrast GitHub App attaches a playable preview to each pull request and keeps live branch builds available for inspection. A PR-diff-driven agent boots your app in headless SootSim, drives the surface your change touched, and posts a single sticky comment with the recording.

Your repo does not need a provider API key or SOOTSIM_API_KEY. The workflow authenticates with the GitHub installation token, and Contrast cloud owns the agent, model proxy, upload, and PR comment.

PR previews and branch builds require the Carbon plan. The agent runs on Contrast’s cloud and model keys, so the automated preview/build flows are gated to Carbon. A pull request on a repo without an entitled account gets a sticky comment noting that previews require Carbon, and no preview is generated.

Set up a repo

Install the Contrast GitHub App on the repo. On first install it opens a bootstrap PR that adds .github/workflows/sootsim.yml tuned to your repo. Merge it and you are done.

To write the workflow by hand, add one job that checks out and calls sootbean/soot-preview-action@v1:

name: Contrast
on:
push:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
contrast:
if: ${{ github.event.pull_request.draft != true }}
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: sootbean/soot-preview-action@v1
with:
start: npm run dev # whatever boots your app dev server

The action installs your dependencies, boots the dev server in the background, waits for it to be ready, drives the app, and reports back. On a pull_request it runs the preview agent; on a push to a tracked branch it runs the branch build.

Package manager

The action auto-detects your package manager from the lockfile or the package.json packageManager field and installs with it (npm ci, yarn install --immutable/--frozen-lockfile, pnpm install --frozen-lockfile, or bun install --frozen-lockfile). Override with package-manager: or a custom install: command for a monorepo.

If your workflow already runs its own setup (checkout, Node, install, e.g. a repo ./.github/actions/setup composite), run it before this action and pass package-manager: none so the action skips straight to the preview:

- uses: ./.github/actions/setup # your own composite
- uses: sootbean/soot-preview-action@v1
with:
package-manager: none
start: pnpm dev:all:lite

Fit previews into an existing CI job

Use the existing job when it already checks out the PR, installs the same dependencies, and runs on a machine that can also host the app. Pick placement from the app server’s lifecycle:

  1. Parallel: start the preview after install, run independent checks, then wait near the end. This avoids a second install and overlaps wall-clock time, but the preview and tests compete for CPU and memory.
  2. Before tests: use the normal action invocation when the preview needs a clean state or its app server would collide with test ports or processes.
  3. After tests: use the normal invocation when tests build the artifact or leave the exact staging server the preview needs running. Do not assume a server survives a completed action step unless the workflow deliberately keeps that process alive.
  4. Separate job: keep isolation when runner labels, permissions, secrets, process lifetime, or resource limits differ. This repeats checkout and often install, but avoids slowing or destabilizing the main test lane.

Parallel example:

- uses: ./.github/actions/setup
- uses: sootbean/soot-preview-action@v1
with:
phase: start
package-manager: none
start: pnpm dev
- run: pnpm test
- uses: sootbean/soot-preview-action@v1
if: always()
with:
phase: wait

Inputs

inputdefaultdescription
startemptycommand that boots your dev server in the background
phaserunrun | start | wait; split start/wait to overlap the preview with other steps in one job
port8081dev server port (Metro/Expo default)
package-managerautoauto | npm | yarn | pnpm | bun | none
installemptyoverride the install command (monorepo filters, etc.)
node-version24Node version installed via actions/setup-node
modeautoauto | preview | build
maestroemptyMaestro flow paths to run against the preview bundle, or auto for every .maestro/*.yaml
runnerlocalwhere the recording executes: local (your CI runner) or hosted (a Contrast GPU recorder)
build-envemptynewline KEY=VALUE pairs baked into the captured bundle (Clerk / API keys); source from ${{ secrets.* }}

Full input list and behavior: the action’s README.

Where the recording runs

The runner input decides where SootSim and the recording execute. Your app’s install and dev server always run on your own CI runner either way — the choice is only about the machine that drives and records it.

runner: local runs everything on your workflow’s runner. Nothing leaves your CI beyond the finished upload, and there is no extra plan requirement beyond previews themselves. The trade-off is hardware: standard hosted CI runners have two CPU cores and no GPU, so SootSim renders through software. Short walkthroughs and quick flows are fine; longer recordings come out choppy, and on complex apps the recording can slow the interaction itself.

runner: hosted keeps your CI runner serving the dev server and moves the driving and recording to a Contrast GPU recorder. Your workflow opens a one-off tunnel for the run, gated by a per-run token so only the assigned recorder can reach your dev server, and tears it down as soon as the run ends. The recorder renders with a real GPU, which is what makes the walkthrough video watchably smooth, and heavier Maestro suites do not eat your CI minutes. It requires the Carbon plan.

Rules of thumb: start with local; switch to hosted when the videos matter (design review, sharing with non-engineers) or when your flows are long enough that software rendering visibly degrades them. If your policy forbids exposing a dev server through any tunnel, even token-gated and per-run, stay on local — that constraint outranks video quality.

Preview vs build

SootSim publishes two share types from GitHub Actions:

typeURLbehavior
PR preview/preview/<id>recorded replay of the changed surface, including bundle, events, storage, network replay data, snapshots, and video when recording succeeds
branch build/build/<owner>/<repo>/<branch>?platform=ioslive configured app build that keeps talking to the endpoint your workflow configured

Keep those modes separate. A recorded PR preview should replay what CI captured; it should not silently fall through to a live backend for missing requests. A branch build is the live inspection mode, so auth and API failures there usually mean the app’s build-time configuration needs work.

What the bot does

For each non-draft PR, the workflow:

  1. reads the PR diff;
  2. asks Contrast cloud whether the change has an app surface worth previewing (and whether the account is entitled);
  3. boots your app and opens it in headless SootSim;
  4. lets the agent drive the relevant surface and save a flow;
  5. replays the flow with recording enabled;
  6. uploads the bundle, events, storage, snapshots, network replay data, and video when present;
  7. updates one sticky PR comment with the preview result and branch build links.

For pushes to configured branches, the build lane uploads the live branch build without running the PR agent.

Verify a preview

A terminal preview or build failure must fail the GitHub workflow. Do not add continue-on-error to the Contrast action. A green check still is not visual proof: it confirms the pipeline finished, but cannot confirm that the changed surface rendered correctly.

Before treating a preview as ready:

  • read the SootSim sticky comment and confirm it says ready, not failed or skipped;
  • open the linked /preview/<id>;
  • confirm the video loads when present;
  • switch to Live and confirm the app catches up to the recorded point;
  • scrub the timeline and confirm the live app follows without getting stuck;
  • check that the frames show the changed surface, not just a login screen or the starting tab twice;
  • open the branch build link separately when you need live staging data.

Skipped previews are valid for docs-only, CI-only, and backend-only changes. If the PR changed user-visible UI and the bot skipped it, adjust the app map or preview guidance instead of accepting the skip.

Troubleshooting

  • “PR previews require a Carbon plan” in the sticky comment: the repo’s Contrast account is on Free. Subscribe to Carbon, then re-run the PR workflow.
  • Preview skipped on a UI change: the relevance gate judged the diff had no observable short-clip delta. Adjust your preview guidance, or open the contrast-agent-logs artifact on the Actions run to see the decision reason.
  • Boot never became ready: the action waits for port (default 8081). If your dev server binds a different port, set port:; if it needs env to boot, pass it via build-env.
  • Auth failed inside the build: the captured bundle needs a runtime key whose allowed origins include sootsim.com. Bake it in with build-env sourced from a repo secret.
  • Deeper diagnostics: every run uploads a contrast-agent-logs artifact (each probe’s stdout/stderr) to the Actions run page; download it for post-mortems.

Re-running previews

When you need to regenerate history, rerun the original pull request workflow run. That preserves the PR event shape the agent expects:

terminal

gh run rerun <pull_request_run_id> -R owner/repo

Do not use a push run when you need a PR preview; push runs only update branch builds. Do not use workflow_dispatch unless your workflow was explicitly written for it.

Ready to build?

Run your React Native app in the browser. No simulators, no native toolchain, no waiting.

curl -fsSL https://sootsim.com/install.sh | sh