Skip to main content

Continuous integration

@stim-cli/ci builds and launches an app through Stim, runs your test command, collects diagnostics, and stops the workspace. It supports iOS, Android, macOS, and web through the same public API that can be imported from stim.

Install the app's dependencies and native tools first. Give each concurrent run its own checkout; cleanup stops the selected workspace. Keep your developer checkout out of CI jobs.

npm install --global @stim-cli/ci
stim-ci run --platform ios --project ./app --artifacts ./test-results --timeout 1800 -- pnpm test:e2e

Build without launching​

stim-ci build --platform android --project ./app --artifacts ./build-results

build needs no test command or device. It preserves existing workspace sessions. By default, iOS and Android builds use the same native artifact cache as run; a later run validates it normally, and cacheKey and cacheHit in its run.json show whether it reused the build. Android reuse requires the run's emulator system image to have the host's ABI; a configured android.systemImage with another ABI, or a physical device, misses. An iOS Debug build with --arch all is keyed without an architecture, so a later Debug run misses. macOS builds are not cached. A cancel or timeout after the artifact is exported does not fail the build stage. GitHub-hosted steps automatically share the job's temporary home and cache. Use a separate results directory for each step.

Both build and run accept --scheme and --configuration for iOS and --variant for Android, so a run can match a build. build also accepts --arch (arm64, x86_64, all) for iOS and --abi (arm64-v8a, armeabi-v7a, x86, x86_64, all) for Android. Without them, each project's normal build settings apply.

Upload the results directory through your CI provider. It includes result.json, build.json, diagnostics.json, and an APK or a tar.gz app bundle. iOS and macOS also keep the archive command's output in artifact.stdout.log and artifact.stderr.log. Apple bundles are archived to preserve executable modes and symlinks across artifact transport. iOS outputs target the simulator; distribution archives and web compilation are outside this command. Downloaded-artifact import into a Stim run is not provided.

import { buildCI } from '@stim-cli/ci';
const result = await buildCI({ projectRoot: '/checkout/app', build: { platform: 'ios' } });
process.exitCode = result.exitCode;

Run and test​

The command after -- is an argument vector. Use an explicit shell when you need shell syntax. Progress and test output go to stderr; stdout and test-results/result.json contain the structured outcome. A failed test keeps its exit code even if diagnostics or cleanup also fail. Timeout returns 124; cancellation returns 130, including cancellation during cleanup when the test has not already failed. Progress callback failures are retained in reportingError and cannot interrupt cleanup. Leave at least 70 seconds between this timeout and the provider's hard timeout for diagnostics and cleanup. The artifacts directory must be empty; choose a new one for each run.

The command receives STIM_CI_DEVICE_ID, STIM_CI_APP_ID, and STIM_CI_METRO_PORT when those facts apply, plus STIM_CI_PLATFORM, STIM_CI_ARTIFACTS_DIR, and STIM_CI_RUN_RESULT. The last variable points to run.json, written before tests start, with exact { platform, facts } from Stim. Tests must use that exact target and implement app-specific readiness; launched can be bundling or unverified.

The package reference documents the library, result schema, artifacts, cancellation, and cache rules.

import { runCI } from '@stim-cli/ci';

const result = await runCI({
projectRoot: '/checkout/app',
run: { platform: 'android' },
command: ['pnpm', 'test:e2e'],
artifactsDir: '/job/results',
timeoutMs: 30 * 60 * 1000,
});

process.exitCode = result.exitCode;

On GitHub-hosted runners, home and cache paths are automatic: $RUNNER_TEMP/stim-ci/home and $RUNNER_TEMP/stim-ci/build-cache. Repeated steps reuse the job's paths. Detection requires GITHUB_ACTIONS=true, RUNNER_ENVIRONMENT=github-hosted and RUNNER_TEMP.

An explicit --home or STIM_HOME keeps the selected home and normal cache configuration; --build-cache or STIM_BUILD_CACHE overrides the cache path. Self-hosted runners and local runs retain their normal Stim home and capacity limits. For other disposable providers, set job-local paths explicitly. Persist only compatible cache entries, never runtime state or device ledgers. Independent Stim homes must not write one shared filesystem cache.

CI=true does not disable ownership, locking, or cleanup checks. Capacity admission is already skipped when its limits are unset; there is no measured reason yet to bypass the remaining coordination.

GitHub Actions​

The repository includes a thin Stim CI action. It maps workflow inputs to stim-ci, forwards cancellation, and writes the job summary. Native tools and app dependencies must be installed first. The bundled TypeScript action uses GitHub's Node.js 24 runtime and supports macOS and Linux runners with Bash. Windows is refused; self-hosted runners need support for Node.js 24 JavaScript actions.

For dogfood against a built Stim checkout:

- uses: ./.github/actions/stim-ci
with:
cli-path: packages/ci/dist/stim-ci.mjs
platform: ios
project: apps/mobile
command: node ../../scripts/ci/app-smoke.mjs
home: ${{ runner.temp }}/stim-home
artifacts: stim-ci-results
timeout: '1800'
artifact-name: stim-ci-ios
retention-days: '7'

Once released, consumers can pin the action to a commit and supply version with an exact published @stim-cli/ci version instead of cli-path. The command input is Bash source, like a workflow run: step. Pass untrusted event values through environment variables, not interpolation into command.

Use a new, empty artifacts directory for each invocation. Leave at least 70 seconds for cleanup after the action timeout, plus time for artifact upload. The Action automatically uploads result, test and diagnostic/compiler logs on success and failure. It exposes artifact-id and artifact-url after upload, while result and artifacts remain local paths for later steps in the job. A custom artifact-name must be unique across invocations in the workflow run. Set upload-artifacts: 'false' to opt out or use another transport, including on GitHub Enterprise Server where the toolkit service is unavailable. An upload failure preserves an earlier nonzero CI exit code, otherwise it fails the Action. Uploads exclude app binaries, caches, symlinks and arbitrary test files. Raw log copying depends on the selected package version. Hard provider termination can prevent cleanup and uploads from completing.

EAS Workflows​

The checked-in Mobile workflow runs the same package in an EAS custom Android job, using a runner with nested virtualization and eas/upload_artifact for diagnostics. It is manually triggered. EAS Workflows usage counts toward the Expo plan's compute allowance.

Provider configuration contains only environment preparation, invocation and artifact upload; lifecycle and test execution remain in @stim-cli/ci.

Stim app coverage​

The GitHub dogfood workflow builds, launches, probes and stops Mobile on iOS and Android and Desktop on macOS. It then repeats with the job's warm build state. It runs for relevant changes on main, manual dispatches, and relevant pull requests carrying the e2e-smoke label.

These checks prove that the exact installed native app process stays alive for five seconds. They do not assert screen rendering, navigation, or a successful server connection. App unit tests and the broader native fixture tests remain separate.

To try it with an agent:

Use @stim-cli/ci to run this app's existing test command on a dedicated checkout. Use the exact target in STIM_CI_RUN_RESULT, save result.json and diagnostics, and verify cleanup on both success and a deliberate test failure.