A GitHub Action that triggers your Minitest suite from your CI workflow. It authenticates via GitHub OIDC, uploads your build artifacts, and kicks off test execution — all fire-and-forget. Results are reported back to your PR via GitHub Check Runs.
name: Run Minitest Suite
on:
push:
tags: ['v*']
permissions:
id-token: write # Required for OIDC authentication
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Build your app for simulators/emulators (your build steps here)
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
ios-build-path: ./build/MyApp.app
android-build-path: ./build/app-debug.apk| Input | Required | Default | Description |
|---|---|---|---|
app-slug |
Yes | — | The Minitest app slug to test |
run-ios |
No | true |
Run tests on iOS. Minitest builds the app when no ios-build-path is given. |
run-android |
No | true |
Run tests on Android. Minitest builds the app when no path is given. |
ios-device-type |
No | Omitted (phone) | Native cloud iOS form factor: phone or tablet. Requires run-ios: true; does not select a lane. |
android-device-type |
No | Omitted (phone) | Native cloud Android form factor: phone or tablet. Requires run-android: true; does not select a lane. |
ios-build-path |
No | — | Pre-built iOS bundle (.app directory or .ipa file). Optional. |
android-build-path |
No | — | Pre-built Android .apk (must target x86-64). Optional. |
run-web |
No | false |
Run the web lane. For a web app linked to a GitHub repo, Minitest builds and serves the commit this workflow runs on (no web-url needed); otherwise it tests the app's configured web URL. See Web runs. |
web-targets |
No | — | Explicit web targets, comma-separated <browser>:<viewport> (e.g. chrome:desktop,safari:mobile). Enables the web lane on its own. |
targets |
No | — | Every device and browser of the run, one per line (e.g. platform=ios,os=27). Replaces the per-platform device and web inputs. See Run targets. |
web-url |
No | — | Per-run web URL override (e.g. a PR preview deployment). When set, the web lane tests this URL instead of building the commit. Applies when run-web or web-targets is set. |
user-story-types |
No | — | Comma-separated user story types to run (e.g., login,checkout) |
scope |
No | — | affected (impacted since the last release tag), pr-affected (impacted by this pull request, see Scoping a run to a pull request) or full. Ignored when user-stories / user-story-types is set. |
tenant-id |
No | — | Tenant ID (required if repo is linked to multiple tenants) |
api-url |
No | https://testing-service.app.minitap.ai |
Override API base URL |
github-token |
No | ${{ github.token }} |
Used on issue_comment events to resolve the pull request behind the comment so the run is attached to it. Needs pull-requests: read. See Preview deployments posted as PR comments. |
cancel-previous-runs |
No | true |
Cancel previous in-flight batches on the same source branch when it matches the app's release branch patterns. See Cancelling previous runs. |
wait-for-result |
No | false |
Block until the run reaches a verdict instead of exiting immediately. See Gating a release on the suite. |
wait-timeout-minutes |
No | 45 |
How long to wait for a verdict when wait-for-result is enabled. |
fail-on-failure |
No | false |
Fail the step when the verdict is a failure. Requires wait-for-result: true. |
By default, Minitest builds your app for both platforms. Set
run-ios: falseorrun-android: falseto skip a platform, or supply a*-build-pathto use a build you've already produced.
Set ios-device-type: tablet to test your native app on a cloud iPad simulator, or android-device-type: tablet for a cloud Android tablet emulator. These inputs configure only an already-selected native cloud lane, not web/mobile-web targets, physical devices, or Edge executors. They do not enable an OS on their own: providing either phone or tablet while the corresponding run-ios / run-android is false is an error.
Omitting a device-type input keeps the legacy phone behavior and omits that field from the API request. The inputs do not change the app's defaults. V1 supports one form factor per OS per run; to test both phones and tablets on the same OS, invoke the action in separate jobs or runs.
With wait-for-result, each job follows its returned batchId, or its immutable launchIntentId when execution is deferred, via batch_id / launch_intent_id status queries. maintenanceRunId identifies the shared commit analysis, not an execution, and is never used to obtain a test verdict. Separate phone and tablet jobs on the same commit therefore report their own verdicts, regardless of completion order. Missing execution handles produce an unavailable verdict rather than a latest-commit fallback. Only the exact nothing_affected warning confirms a no-op that passes without polling.
For native iPad runs, an iPhone-only build may run in compatibility mode (tablet_iphone_compatibility_mode warning). Missing or unreadable device-family metadata produces tablet_compatibility_unknown. Neither warning blocks launch, and neither run establishes native tablet-layout coverage. The action forwards server warnings; GitHub checks and PR comments use the server's central device labels.
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
ios-device-type: tablet
android-device-type: tablettargets lists every device and browser the run executes on, one per line. Each scenario runs once on every target. A line is a comma-separated list of key=value pairs:
| Key | Values | Applies to | Notes |
|---|---|---|---|
platform |
ios, android, web |
every line | Required. |
backend |
cloud, physical |
ios, android |
Default cloud. physical runs on a phone of the Minitap device farm. |
device |
phone, tablet, tv |
cloud ios, android |
Default phone. tv is Android only. |
model |
farm model, e.g. iPhone 13 |
backend=physical |
The exact farm model to lease. Without it, any farm phone of the platform. |
os |
OS version, e.g. 26, 27, 15 |
ios, android |
Default: Minitest's pick. A cloud device takes the major (27.0 is read as 27); a physical phone keeps the exact version and needs model. Not on a TV. |
browser |
chrome, edge, firefox |
web |
Without it, a platform=web line runs the app's default web targets. |
version |
browser major, e.g. 157 |
web with browser |
Not on viewport=mobile. |
viewport |
desktop, tablet, mobile |
web |
Default desktop. |
Keys and enum values are case-insensitive. A malformed line, an unknown key or the same target listed twice fails the step before anything is uploaded.
How it combines with the other inputs
- Builds still come from
ios-build-path/android-build-path. Everyiosline installs the iOS build and everyandroidline the Android build. A platform listed without a build path is built by Minitest for this commit, as today. A build path without a line of its platform is an error. So is a build path for a platform that mixes cloud and physical lines, because a simulator build and a device build are different files. targetsdefines the platforms.run-ios/run-androidmay stay at their defaulttrue. Setting one tofalsewhiletargetslists that platform is an error.ios-device-type,android-device-type,web-targetsandrun-web: trueare replaced bytargets. Setting any of them next to it is an error.web-urlstill applies, to the web lines without a URL of their own. It needs aplatform=webline.- With
targetsempty, every other input behaves exactly as before.
Several devices on one platform, browser versions, tablets and Android TV are enabled per workspace. When yours does not have one, the run is refused and the step fails with the server's message and what to change. For example: "Several native targets on 'ios' are not available for this workspace yet. This workspace runs one device per platform…".
The job summary and the targets output list one row per target, named like the Minitest run page ("iPhone · iOS 27", "Chrome 157 · Desktop"). With wait-for-result, each row carries its status and its critical and warning counts, and the summary adds a "N critical · M warnings" total per platform.
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
ios-build-path: ./build/Build/Products/Debug-iphonesimulator/MyApp.app
targets: |
platform=ios,os=26
platform=ios,os=27- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-web-app
targets: |
platform=web,browser=chrome,version=157,viewport=desktop
platform=web,browser=chrome,version=156,viewport=desktopA farm iPhone installs a device build (.ipa built for iphoneos), and a simulator installs a simulator build. One ios-build-path cannot fill both, so leave it out and let Minitest build both variants of the commit:
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
run-android: false
targets: |
platform=ios,os=27
platform=ios,backend=physical,model=iPhone 13,os=26To run on the real iPhone only, keep its line alone and point ios-build-path at your device .ipa.
| Output | Description |
|---|---|
batch-id |
The ID of the triggered test batch, empty when no batch was created |
status |
Initial status of the triggered batch |
result |
The verdict when wait-for-result is enabled: passed, failed, error or nothing_affected. Empty otherwise (including on timeout). |
batch-url |
Link to the run in the Minitest dashboard, when one was created |
targets |
JSON array, one entry per target: label, platform, status, criticals, warnings. See Run targets. |
- OIDC Authentication — Requests a GitHub OIDC token scoped to the Minitap API. No secrets to manage!
- Validate Builds — If you supplied any build paths, the action validates the artifacts (see below).
- Upload Builds — Uploads your supplied builds to Minitap (
.appbundles are automatically packaged into.ipa). - Trigger Run — Calls the Minitap CI API. For any enabled platform without a supplied build, Minitest builds the app for this commit on your behalf.
- Fire & Forget — The action exits immediately. Results are reported back via GitHub Check Runs. Set
wait-for-result: trueto block on the verdict instead, so a deploy can be gated on it — see Gating a release on the suite.
By default, Minitest builds your app for both platforms — you don't need to supply anything beyond app-slug. Provide a build path only when you want to use an artifact you've already produced (e.g., to skip a redundant build step in your workflow). Builds you supply must target simulators / emulators.
Provide a simulator .app bundle or a .ipa file.
| Format | Description |
|---|---|
.app |
Simulator bundle directory (automatically packaged into .ipa) |
.ipa |
IPA file (uploaded as-is) |
To build for the iOS Simulator with xcodebuild:
xcodebuild build \
-scheme MyApp \
-sdk iphonesimulator \
-configuration Debug \
-derivedDataPath ./build
# Output: ./build/Build/Products/Debug-iphonesimulator/MyApp.appProvide a .apk file built for x86-64 emulators. The action inspects the APK and verifies it contains native libraries for the x86_64 architecture (lib/x86_64/).
To build an x86-64 debug APK with Gradle, configure your app's build.gradle:
// app/build.gradle
android {
defaultConfig {
ndk { abiFilters 'x86_64' }
}
}Then build:
./gradlew assembleDebug
# Output: app/build/outputs/apk/debug/app-debug.apkNote: If your APK only contains
arm64-v8aorarmeabi-v7alibraries, the action will fail with a clear error telling you which architectures were found.
run-ios, run-android, and the web inputs each select a lane, and lanes are additive: you can run just the web lane, just one native lane, or any mix.
There are two ways to include the web lane:
run-web: trueruns the app's configured default web targets (set per app in Minitest).web-targetsruns an explicit list and includes the web lane on its own (you don't also needrun-web).
For a web app linked to a GitHub repo (set in App Settings), the web lane builds and serves the commit this workflow runs on and tests against that build. No web-url is required, and this holds even when the app also has a configured web URL: the linked-repo lane tests the commit, not the deployment. Supply web-url (for example a PR preview deployment) only to test a separately-deployed URL instead of the commit. For a web app with only a configured URL and no linked repo, the lane tests that configured URL.
web-targets is a comma-separated list of <browser>:<viewport> tokens. The action maps each token to a target:
| Token | Runs as |
|---|---|
safari:mobile |
iOS Safari (mobile web) |
chrome:mobile |
Android Chrome (mobile web) |
chrome:tablet |
Tablet web (Chrome) |
firefox:tablet |
Tablet web (Firefox) |
edge:tablet |
Tablet web (Microsoft Edge) |
chrome:desktop |
Desktop web (Chrome) |
firefox:desktop |
Desktop web (Firefox) |
edge:desktop |
Desktop web (Microsoft Edge) |
Other combinations (such as firefox:mobile, edge:mobile or safari:desktop) are rejected with a clear error.
Microsoft Edge is in early access and is enabled per workspace on request. Until it is enabled for yours, Minitest rejects a run with an edge target (HTTP 422).
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
run-android: false- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
run-ios: false
android-build-path: ./app/build/outputs/apk/debug/app-debug.apk- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
ios-build-path: ./build/Build/Products/Debug-iphonesimulator/MyApp.app- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
user-story-types: login,checkout,onboarding- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
run-web: true- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
web-targets: chrome:desktop,safari:mobile
web-url: https://pr-142.preview.example.comVercel, Netlify & co. announce each preview deployment as a comment on the pull request. Trigger the web lane from that comment and the batch is attached to the PR — its check run shows up in the PR's Checks tab, and — when the PR head branch matches one of the app's release branch patterns — cancel-previous-runs supersedes the batch from the previous preview.
name: Minitest preview
on:
issue_comment:
types: [created, edited]
permissions:
id-token: write
pull-requests: read
jobs:
minitest:
if: github.event.issue.pull_request != null && github.event.comment.user.login == 'vercel[bot]'
runs-on: ubuntu-latest
steps:
- id: parse
run: |
url="$(grep -Eo 'https://[A-Za-z0-9._-]+\.vercel\.app[^ )"'"'"']*' <<< "$COMMENT" | head -n1)"
[ -n "$url" ] || exit 1
echo "url=${url%/}" >> "$GITHUB_OUTPUT"
env:
COMMENT: ${{ github.event.comment.body }}
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
run-ios: false
run-android: false
web-targets: chrome:desktop
web-url: ${{ steps.parse.outputs.url }}
scope: pr-affectedTwo caveats: issue_comment workflows only run from the default branch (the file must be merged before commenting does anything), and a preview behind Vercel Deployment Protection serves an SSO page rather than your app.
scope: pr-affected runs only the scenarios the pull request's code changes could impact, instead of the whole suite. The impact analysis is the one already powering the "Run affected" checkbox on Minitest's pull request comment, so both entry points always agree on what "affected" means.
It needs the Minitap GitHub App installed on the repository — that is what observes the pull request and computes the analysis — and a pull request context (a pull_request event, or an issue_comment on a PR with github-token set).
Four outcomes, all non-fatal:
| Situation | What runs |
|---|---|
| The analysis is ready | Only the impacted scenarios |
| The analysis is still computing | The batch is created and parked; it starts, scoped, as soon as the analysis lands |
| No analysis exists (no GitHub App, or no PR context) | The whole suite, with a warning annotation |
| The analysis found nothing impacted | Nothing — no batch is created, batch-id is empty, and a warning explains why |
- uses: minitap-ai/minitest-trigger@v1
with:
app-slug: my-app
tenant-id: tenant_abc123When you repeatedly push to the same release branch (e.g., reopening a release PR with a fix), older test batches that are still pending or running pile up. The cancel-previous-runs input (enabled by default) tells the server to cancel previous in-flight CI batches for the same source branch.
Cancellation is scoped:
- Same source branch only — matched on the PR head branch (
pull_requestevents) or the branch ref forpush/workflow_dispatch/schedule/merge_group. - Release branches only — the branch must match one of the app's configured
release_branch_patterns(gitignore-style; configured per app in Minitest). - CI-triggered only — only batches triggered by this GitHub Action are cancelled. Webapp, Slack, or API-triggered runs are unaffected.
No-ops:
- Tag pushes (
refs/tags/*). - Branches that don't match a configured release pattern.
- Events where the branch can't be determined (e.g., PR event payload missing).
Opt out with cancel-previous-runs: false.
By default the action is fire-and-forget: it triggers the run and exits, and the results land later as a GitHub Check Run. That is the right behaviour for a PR check, but it means a deploy job can ship before the suite has said anything.
Set wait-for-result: true to block until the run reaches a verdict, and fail-on-failure: true to turn that verdict into a red step. Everything downstream then gates on it with needs:.
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
jobs:
minitest-gate:
runs-on: ubuntu-latest
steps:
- uses: minitap-ai/minitest-trigger@v1
id: minitest
with:
app-slug: my-app
scope: affected
wait-for-result: true
fail-on-failure: true
wait-timeout-minutes: 60
deploy:
needs: minitest-gate
runs-on: ubuntu-latest
steps:
- run: ./scripts/deploy.shdeploy runs only once minitest-gate is green. If the suite fails, the job goes red and every job that needs: it is skipped — nothing ships.
Verdict (result output) |
Gate |
|---|---|
passed |
✅ Passes — every scenario passed |
nothing_affected |
✅ Passes — the impact analysis found no scenario affected by this commit, so no batch was created. A no-op release is not a failure and must not block the deploy. |
failed |
❌ Fails — the step message names the failing stories and links the run |
error |
❌ Fails — the run errored or was cancelled, so there is no trustworthy verdict |
If the timeout elapses first, result is empty and the step fails with a timeout message (or warns, when fail-on-failure is false). Raise wait-timeout-minutes for suites that legitimately run longer than 45 minutes.
Leave fail-on-failure at its default to observe the verdict without gating on it. The step stays green and a failing run is surfaced as a warning annotation, while result and batch-url still carry the outcome for later steps:
- uses: minitap-ai/minitest-trigger@v1
id: minitest
with:
app-slug: my-app
wait-for-result: true
- run: echo "Verdict was ${{ steps.minitest.outputs.result }} — ${{ steps.minitest.outputs.batch-url }}"fail-on-failure: true without wait-for-result: true is a configuration error and fails the step immediately: there is no verdict to fail on.
Your workflow must have the id-token: write permission for OIDC authentication to work:
permissions:
id-token: write# Install dependencies
npm install
# Build TypeScript
npm run build
# Lint
npm run lint
# Bundle for distribution (local testing only — CI builds on release)
npm run bundle
# Run all checks
npm run all- Create a GitHub Release with a semver tag (e.g.,
v1.0.0) - The release workflow automatically builds
dist/, commits it, and updates thev1major version tag - Users referencing
@v1get the latest release automatically
MIT