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.
- Global
- npx
npm install --global @stim-cli/ci
stim-ci run --platform ios --project ./app --artifacts ./test-results --timeout 1800 -- pnpm test:e2e
npx --yes --package @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.