Skip to content

CI/CD integration

Run Marathon Cloud on every pull request from GitHub Actions, CircleCI, Bitrise, or any Docker-capable CI, and gate the pipeline on the result.

Marathon Cloud ships an official GitHub Action, a Bitrise step, and a Docker image. All of them wrap the same CLI, so anything you can do from a terminal you can do from CI. Working examples live in the MarathonLabs/samples repository.

Examples pin CLI 1.0.65, action-test 1.0.21, setup-marathon-cloud 2.0.2, and the Bitrise step 1.0.1.

A complete GitHub Actions job: build, run on Marathon Cloud, publish JUnit, fail the check on test failures.

name: ui-tests
on: [pull_request]
jobs:
marathon:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- run: ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest
- name: Run tests on Marathon Cloud
uses: MarathonLabs/[email protected]
with:
version: 1.0.65
apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }}
platform: Android
application: app/build/outputs/apk/debug/app-debug.apk
testApplication: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
osVersion: 15
systemImage: google_apis
output: marathon
link: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
branch: ${{ github.head_ref }}
- name: Publish JUnit
if: always()
uses: mikepenz/action-junit-report@v5
with:
report_paths: marathon/tests/omni/marathon_junit_report.xml

The Marathon step exits non-zero when a test fails, so the job goes red without extra wiring. if: always() on the publish step ensures the report is uploaded either way.

The CLI, and every integration built on it, exits 1 when any test fails and 0 when all tests pass.

For richer gating, set resultFile and read the JSON in a later step:

- uses: MarathonLabs/[email protected]
with:
version: 1.0.65
apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }}
platform: Android
application: app-debug.apk
testApplication: app-debug-androidTest.apk
osVersion: 15
systemImage: google_apis
resultFile: marathon-result.json
ignoreTestFailures: true
- name: Summarize
run: |
jq -r '"\(.passed) passed, \(.failed) failed, \(.ignored) ignored — \(.report)"' marathon-result.json >> "$GITHUB_STEP_SUMMARY"
test "$(jq -r .state marathon-result.json)" = "passed"

ignoreTestFailures: true keeps the run step green so the summary step executes; the final test restores the red check. See result file for the schema.

MarathonLabs/action-test installs the CLI and runs it in one step. version (the CLI version to install) and apiKey are required alongside your run inputs.

Android

- uses: MarathonLabs/[email protected]
with:
version: 1.0.65
apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }}
platform: Android
application: app/build/outputs/apk/debug/app-debug.apk
testApplication: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
osVersion: 15
systemImage: google_apis
output: marathon

iOS

- uses: MarathonLabs/[email protected]
with:
version: 1.0.65
apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }}
platform: iOS
application: build/Build/Products/Debug-iphonesimulator/YourApp.app
testApplication: build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app
osVersion: 18.4
output: marathon

Maestro

- uses: MarathonLabs/[email protected]
with:
version: 1.0.65
apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }}
platform: Maestro/Android
application: app-debug.apk
testApplication: flows/
flows: flows/smoke/
output: marathon

Inputs

Input Description
version CLI version to install. Required.
apiKey Marathon Cloud API token. Required.
platform Android, iOS, Maestro/Android, or Maestro/iOS. Required.
application Application binary. Required.
testApplication Test binary, or flows directory for Maestro. Required.
osVersion, device, systemImage Device selection. Android 15+ needs systemImage: google_apis or google_apis_playstore.
output, outputGlob Download artifacts into output, optionally filtered by glob.
resultFile Write the result file.
name, link, branch, project Run metadata.
wait, ignoreTestFailures Exit behaviour.
filterFile, isolated, analyticsReadOnly Execution control.
retryQuotaTestUncompleted, retryQuotaTestPreventive, retryQuotaTestReactive, noRetries Retries.
pullFiles Android: files to pull from the device.
grantedPermission, xctestrunEnv, xctestrunTestEnv, xctestplanFilterFile, xctestplanTargetName iOS.
maestroEnv, flows Maestro.

Flags without an input (--concurrency-limit, --code-coverage, --batch-isolation, --test-timeout-*, --application-bundle, --library-bundle, --instrumentation-arg) are available by installing the CLI with MarathonLabs/setup-marathon-cloud and calling marathon-cloud from a run: step:

- uses: MarathonLabs/[email protected]
with:
version: 1.0.65
- run: |
marathon-cloud run android \
--application app-debug.apk \
--test-application app-debug-androidTest.apk \
--os-version 15 \
--system-image google_apis \
--concurrency-limit 4 \
--output marathon \
--no-progress-bars
env:
MARATHON_CLOUD_API_KEY: ${{ secrets.MARATHON_CLOUD_API_KEY }}

Store the API token in your CI’s secret store and expose it as MARATHON_CLOUD_API_KEY. Never commit it. See API keys.

Set link to the CI job or pull request URL, and branch and name, so a run in the console can be traced to the job that produced it.

Set output and point your CI’s test-report publisher at <output>/tests/omni/marathon_junit_report.xml. Per-test videos and logs are under <output>/video and <output>/logs. See artifacts.

Pass --no-progress-bars when calling the CLI directly so CI logs stay readable.

Pin the action, step, and CLI version. The releases page lists changes.

Devices run in Marathon Cloud’s network and there is no tunnel into yours. Point tests at a staging environment reachable from the internet. Backends that fall over under parallel load are a common cause of “passes locally, fails in Cloud”; see limiting concurrency.

Budget for upload time plus the run. Marathon Cloud targets 15 minutes for the run itself; a 30-minute job timeout is a safe floor for most suites.