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.
Quick start
Section titled “Quick start”A complete GitHub Actions job: build, run on Marathon Cloud, publish JUnit, fail the check on test failures.
name: ui-testson: [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 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.xmlThe 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.
Gate your pipeline on results
Section titled “Gate your pipeline on results”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:
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: Summarizerun: | 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.
Supported platforms
Section titled “Supported platforms”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
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: marathoniOS
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: marathonMaestro
with: version: 1.0.65 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Maestro/Android application: app-debug.apk testApplication: flows/ flows: flows/smoke/ output: marathonInputs
| 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:
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 }}Run the CLI from the official Docker image with the Docker executor.
version: 2.1jobs: ui-tests: docker: - image: marathonlabs/marathon-cloud:1.0.65 steps: - checkout - attach_workspace: at: . - run: name: Run tests on Marathon Cloud command: | marathon-cloud run android \ --application app/build/outputs/apk/debug/app-debug.apk \ --test-application app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --output marathon \ --link "$CIRCLE_BUILD_URL" \ --branch "$CIRCLE_BRANCH" \ --no-progress-bars - store_test_results: path: marathon/tests/omni - store_artifacts: path: marathonworkflows: test: jobs: - build - ui-tests: requires: [build]Set MARATHON_CLOUD_API_KEY in a CircleCI context or project environment variable; the CLI reads it without a flag. The build job should persist the APKs to the workspace.
run-tests-using-marathon-cloud is in the official Bitrise StepLib. On Linux stacks it runs the Docker image; on macOS stacks it installs the CLI from Homebrew.
inputs: - api_key: $MARATHON_CLOUD_API_KEY - platform: Android - application: $BITRISE_APK_PATH - test_application: $BITRISE_TEST_APK_PATH - os_version: 15 - system_image: google_apis - output: $BITRISE_DEPLOY_DIR/marathonInputs: api_key, platform (Android or iOS), application, test_application, os_version, system_image, device, run_name, link, output, filter_file, isolated, ignore_test_failures, xctestplan_filter_file, xctestplan_target_name, docker_image.
Point the Deploy to Bitrise.io step’s test results at $BITRISE_DEPLOY_DIR/marathon/tests/omni.
marathonlabs/marathon-cloud on Docker Hub, tagged latest and per CLI version. The image’s working directory is /work; mount your build output there.
docker run --rm \ -v "$(pwd)":/work \ -e MARATHON_CLOUD_API_KEY \ marathonlabs/marathon-cloud:1.0.65 \ run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --output marathon \ --no-progress-barsThis is the path for GitLab CI, Jenkins, Buildkite, and any other Docker-capable runner. Pin the version tag rather than latest.
Best practices
Section titled “Best practices”Keep secrets out of config
Section titled “Keep secrets out of config”Store the API token in your CI’s secret store and expose it as MARATHON_CLOUD_API_KEY. Never commit it. See API keys.
Link runs back to CI
Section titled “Link runs back to CI”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.
Publish JUnit
Section titled “Publish JUnit”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.
Disable progress bars
Section titled “Disable progress bars”Pass --no-progress-bars when calling the CLI directly so CI logs stay readable.
Pin versions
Section titled “Pin versions”Pin the action, step, and CLI version. The releases page lists changes.
Troubleshooting
Section titled “Troubleshooting”Tests need to reach internal services
Section titled “Tests need to reach internal services”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.
Job timeouts
Section titled “Job timeouts”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.