Bitrise

View as Markdown

Bitrise can build your mobile app and then hand the artifact to TesterArmy to run a saved test group. Bitrise owns the build; TesterArmy owns the cloud simulator/emulator, app installation, test execution, and run orchestration, so you do not need to manage devices or emulators in your Bitrise Workflow.

There is no dedicated TesterArmy Bitrise Step. You drive the flow from a Script Step with the TesterArmy CLI, which keeps the integration a plain two-command sequence:

  1. upload-app uploads the build and returns an app ID.
  2. ci starts the group, waits for every run to finish, and exits non-zero when something failed.

Prerequisites

Before you add the Workflow, make sure you have already:

  • created a mobile project in TesterArmy,
  • created at least one mobile test and run it manually so you know it passes,
  • added that test to a test group,
  • confirmed your Bitrise build produces a supported artifact (see Artifact requirements).

Add the secrets

TesterArmy needs three values. Add TESTERARMY_API_KEY on the Secrets tab of the Workflow Editor so it is masked and not exposed to pull requests from forks. The project and group IDs are not sensitive, so you can keep them in app.envs in bitrise.yml.

ValueWhere to put itWhere to find it
TESTERARMY_API_KEYBitrise SecretsProfile → API Keys
TESTERARMY_PROJECT_IDapp.envsProject Settings
TESTERARMY_GROUP_IDapp.envsTests tab → the group’s three-dot menu → Copy ID

Do not tick Expose for Pull Requests on TESTERARMY_API_KEY unless you only build pull requests from branches in your own repository. Bitrise exposes protected secrets to fork builds when that option is enabled.

The CLI reads TESTERARMY_API_KEY from the environment automatically. The project, group, and app IDs have no environment-variable fallback, so they must be passed as flags.

Artifact requirements

TesterArmy runs your app on a cloud simulator or emulator, which constrains what Bitrise needs to produce:

PlatformRequired artifactBitrise StepPath env var
iOSiOS Simulator .app (or .app.zip)Xcode build for simulator (xcode-build-for-simulator)$BITRISE_APP_DIR_PATH
Android.apk (or .apks)Android Build (android-build) with build_type: apk$BITRISE_APK_PATH

An .ipa will not work. xcode-archive produces a device build; you need xcode-build-for-simulator. On Android, an .aab will not work either — set build_type: apk.

The CLI accepts the raw .app directory and zips it for you, so you can pass $BITRISE_APP_DIR_PATH directly.

Run a group after the build

The simplest setup is one Workflow that builds, uploads, and runs the group. Because both CLI commands run in the same Script Step, the app ID stays a local shell variable and you do not need envman.

bitrise.yml
1format_version: "17"
2default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
3project_type: ios
4
5app:
6 envs:
7 - BITRISE_PROJECT_PATH: ios/MyApp.xcworkspace
8 - BITRISE_SCHEME: MyApp
9 - TESTERARMY_PROJECT_ID: <projectId>
10 - TESTERARMY_GROUP_ID: <groupId>
11
12tools:
13 nodejs: "22:installed"
14
15meta:
16 bitrise.io:
17 stack: osx-xcode-16.0.x
18 machine_type_id: g2.mac.medium
19
20workflows:
21 testerarmy_ios:
22 steps:
23 - activate-ssh-key@4: {}
24 - git-clone@8: {}
25 - xcode-build-for-simulator@0:
26 inputs:
27 - configuration: Release
28 - script@1:
29 title: Run TesterArmy mobile tests
30 inputs:
31 - content: |-
32 #!/usr/bin/env bash
33 set -euo pipefail
34
35 mkdir -p .testerarmy
36
37 npx --yes testerarmy@latest upload-app \
38 --app-path "$BITRISE_APP_DIR_PATH" \
39 --project "$TESTERARMY_PROJECT_ID" \
40 --remove-after 3600 \
41 --output .testerarmy/upload.json
42
43 APP_ID="$(node -e 'const fs=require("fs");process.stdout.write(JSON.parse(fs.readFileSync(".testerarmy/upload.json","utf8")).uploadedAppId)')"
44 echo "Uploaded app: $APP_ID"
45
46 args=(
47 ci
48 --group "$TESTERARMY_GROUP_ID"
49 --project "$TESTERARMY_PROJECT_ID"
50 --platform ios
51 --app-id "$APP_ID"
52 --commit-sha "$BITRISE_GIT_COMMIT"
53 --delete-app-after-run
54 --output .testerarmy/ci-result.json
55 )
56
57 if [ -n "${BITRISE_PULL_REQUEST:-}" ]; then
58 args+=(--pr-number "$BITRISE_PULL_REQUEST")
59 fi
60
61 npx --yes testerarmy@latest "${args[@]}"

The tools: nodejs entry makes Node available in $PATH before the first Step, which is what npx needs. If your repository already pins Node through a .nvmrc, .node-version, or .tool-versions file, you can use the Dependency installer Step instead.

Signing an Android release build

If your Android release variant requires signing, add sign-apk@1 after android-build@1 and upload $BITRISE_SIGNED_APK_PATH instead of $BITRISE_APK_PATH. TesterArmy also accepts a debug APK, so building variant: debug is a valid way to avoid signing config in the test Workflow.

Prefer release builds when the app runs without a Metro dev server. See Dev Builds and Metro for the React Native caveats.

Build once, test both platforms

To test iOS and Android from a single trigger, split the work into a Bitrise Pipeline. The build Workflows run on the stack they need, share the artifact as a Pipeline intermediate file, and the test Workflows run on cheaper Linux machines because the CLI only needs Node.

bitrise.yml
1pipelines:
2 testerarmy_mobile:
3 workflows:
4 build_ios: {}
5 build_android: {}
6 test_ios:
7 depends_on: [build_ios]
8 test_android:
9 depends_on: [build_android]
10
11workflows:
12 build_ios:
13 meta:
14 bitrise.io:
15 stack: osx-xcode-16.0.x
16 machine_type_id: g2.mac.medium
17 steps:
18 - git-clone@8: {}
19 - xcode-build-for-simulator@0:
20 inputs:
21 - configuration: Release
22 - deploy-to-bitrise-io@2:
23 inputs:
24 - pipeline_intermediate_files: "$BITRISE_APP_DIR_PATH:BITRISE_APP_DIR_PATH"
25
26 build_android:
27 steps:
28 - git-clone@8: {}
29 - android-build@1:
30 inputs:
31 - build_type: apk
32 - deploy-to-bitrise-io@2:
33 inputs:
34 - pipeline_intermediate_files: "$BITRISE_APK_PATH:BITRISE_APK_PATH"
35
36 test_ios:
37 steps:
38 - pull-intermediate-files@1:
39 inputs:
40 - artifact_sources: build_ios
41 - script@1:
42 title: Run TesterArmy iOS tests
43 inputs:
44 - content: |-
45 #!/usr/bin/env bash
46 set -euo pipefail
47 mkdir -p .testerarmy
48
49 npx --yes testerarmy@latest upload-app \
50 --app-path "$BITRISE_APP_DIR_PATH" \
51 --project "$TESTERARMY_PROJECT_ID" \
52 --remove-after 3600 \
53 --output .testerarmy/upload.json
54
55 APP_ID="$(node -e 'const fs=require("fs");process.stdout.write(JSON.parse(fs.readFileSync(".testerarmy/upload.json","utf8")).uploadedAppId)')"
56
57 npx --yes testerarmy@latest ci \
58 --group "$TESTERARMY_GROUP_ID" \
59 --project "$TESTERARMY_PROJECT_ID" \
60 --platform ios \
61 --app-id "$APP_ID" \
62 --commit-sha "$BITRISE_GIT_COMMIT" \
63 --delete-app-after-run \
64 --output .testerarmy/ci-result.json
65
66 test_android:
67 steps:
68 - pull-intermediate-files@1:
69 inputs:
70 - artifact_sources: build_android
71 - script@1:
72 title: Run TesterArmy Android tests
73 inputs:
74 - content: |-
75 #!/usr/bin/env bash
76 set -euo pipefail
77 mkdir -p .testerarmy
78
79 npx --yes testerarmy@latest upload-app \
80 --app-path "$BITRISE_APK_PATH" \
81 --project "$TESTERARMY_PROJECT_ID" \
82 --remove-after 3600 \
83 --output .testerarmy/upload.json
84
85 APP_ID="$(node -e 'const fs=require("fs");process.stdout.write(JSON.parse(fs.readFileSync(".testerarmy/upload.json","utf8")).uploadedAppId)')"
86
87 npx --yes testerarmy@latest ci \
88 --group "$TESTERARMY_GROUP_ID" \
89 --project "$TESTERARMY_PROJECT_ID" \
90 --platform android \
91 --app-id "$APP_ID" \
92 --commit-sha "$BITRISE_GIT_COMMIT" \
93 --delete-app-after-run \
94 --output .testerarmy/ci-result.json

deploy-to-bitrise-io shares each artifact under an env-var key, and pull-intermediate-files restores that same env var in the downstream Workflow, so the upload command is identical to the single-Workflow version. Directories such as the iOS .app bundle are archived and re-extracted for you.

If you split upload and test across two Workflows instead, pass the app ID between Steps with envman add --key TESTERARMY_APP_ID --value "$APP_ID". Do not use --delete-app-after-run in that case unless the deleting Workflow is the last consumer of the app — rely on --remove-after instead.

Command reference

upload-app

FlagRequiredNotes
--app-path <path>Yes.app, .app.zip, .zip, .apk, or .apks
--project <projectId>YesNo environment-variable fallback
--remove-after <seconds>NoAuto-delete after N seconds. Minimum 60, maximum 604800 (7 days)
--output <path>NoWrites the result JSON. A directory writes ta-app-upload-<date>.json
--jsonNoAlso prints the result JSON to stdout

The written JSON contains app and uploadedAppId:

1{
2 "app": { "id": "app_123", "platform": "ios", "filename": "MyApp.app.zip" },
3 "uploadedAppId": "app_123"
4}

ci

FlagRequiredNotes
--group <groupId>YesThe group whose tests should run
--project <projectId>NoRequired when using --delete-app-after-run
--platform <platform>Noweb, ios, or android. Set it explicitly for mobile runs
--app-id <appId>NoThe uploadedAppId from upload-app
--commit-sha <sha>NoPass $BITRISE_GIT_COMMIT to enable GitHub check reporting
--pr-number <number>NoPass $BITRISE_PULL_REQUEST on pull request builds
--delete-app-after-runNoRequires both --app-id and --project. Skipped if the run timed out
--timeout <ms>NoDefaults to 1800000 (30 minutes)
--poll-interval-seconds <s>NoDefaults to 10
--output <path>NoWrites the result JSON envelope

ci always polls until every run reaches a terminal state or the timeout elapses; there is no separate wait flag.

Exit codes

Exit codeMeaning
0Every run passed
1At least one run failed or was cancelled, or the wait timed out
2The command itself errored — missing API key, invalid flags, bad input

Bitrise fails the Step, and therefore the build, on any non-zero exit code.

Reporting results back to your Git provider

TesterArmy reports through GitHub, not through Bitrise. Passing --commit-sha and --pr-number only produces a GitHub check and PR comment when the TesterArmy project is connected to the GitHub repository containing that commit — configure it on the project’s Integrations tab. See Connect GitHub.

For repositories hosted on GitLab or Bitbucket, the Bitrise build status is the gate and full evidence lives in the TesterArmy dashboard. TesterArmy does not post GitLab merge request or Bitbucket pull request statuses.

Keeping storage under control

Each project has a 2 GB storage limit, and CI uploads accumulate quickly. Use both mechanisms together:

  • --remove-after 3600 on upload-app is the safety net. TesterArmy removes the upload even if the build is aborted or the Step never reaches cleanup.
  • --delete-app-after-run on ci deletes the app as soon as the runs finish. It is skipped when the run timed out or the runs are not terminal, so the app stays available for debugging.

See App Uploads for the storage limits and auto-delete behavior.

Troubleshooting

Missing API key. Run testerarmy auth first or set TESTERARMY_API_KEY.

The Step exits with code 2. The secret is either not defined or not exposed to this Workflow. Check the Secrets tab, and remember that secrets are not available to builds triggered by pull requests from forks unless you explicitly expose them.

npx: command not found

Node is not on $PATH. Add the tools: nodejs entry shown above, or add a dependency-installer@1 Step before the Script Step.

The upload is rejected as an unsupported format

You are passing an .ipa, .aab, or .xapk. Switch the iOS build to xcode-build-for-simulator, or set build_type: apk on android-build.

$BITRISE_APP_DIR_PATH is empty

The Script Step runs before the build Step, or the build Step failed without failing the build. Confirm xcode-build-for-simulator appears earlier in the Workflow. In a Pipeline, confirm pull-intermediate-files lists the correct artifact_sources and that the build Workflow shared the path with deploy-to-bitrise-io.

The build passes but no tests ran

--group points at a group with no mobile tests in it. Groups containing only web tests produce an empty run set.

The Step fails with --delete-app-after-run but the tests passed

--delete-app-after-run requires both --app-id and --project. If the delete request itself fails, the CLI rethrows and exits 2 even though the runs passed. Drop the flag and rely on --remove-after if your API key lacks delete permission.