cloud phone CI: GitHub Actions workflow examples 2026
cloud phone CI: GitHub Actions workflow examples 2026
a cloud phone GitHub Actions setup is what mobile teams reach for when they want every PR to run real-device tests without paying for a third-party device farm. cloudf.one’s REST API, ADB-over-network, and webhook system fit cleanly into GitHub Actions’ job model, and the resulting pipeline costs less than a single physical test device per month.
this guide gives you three production-ready workflow files: a basic smoke test, a parallel device-matrix run, and a release candidate gate. copy them into your .github/workflows/ directory and adapt.
the architecture in one paragraph
a GitHub Actions runner authenticates against the cloudf.one API using a token stored as a GitHub secret. it locks one or more cloud phones, connects ADB over the network endpoint cloudf.one provides, installs the APK, runs the test suite, captures artifacts on failure, and unlocks the devices. the entire flow runs in 5 to 15 minutes per build depending on suite size.
for the broader case why integrating real devices into CI matters, see cloud phone CI/CD integration.
prerequisites
- a cloudf.one account with API access enabled
- a GitHub repository with Actions enabled
- your APK build artifact (or the commands to build it from source)
- ADB installed on the runner (default on
ubuntu-latestafter the setup step)
step 1: store secrets in GitHub
go to your repository -> Settings -> Secrets and variables -> Actions. add:
- CLOUDFONE_TOKEN: your cloudf.one API token
- CLOUDFONE_REGION: SG (or your default region)
these inject as environment variables into the workflow without exposing them in the workflow file.
workflow 1: basic smoke test on every PR
.github/workflows/smoke-test.yml:
name: Cloud phone smoke test
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Build APK
run: ./gradlew assembleDebug
- name: Set up ADB
run: |
sudo apt-get update
sudo apt-get install -y android-tools-adb
- name: Lock cloud phone
id: lock
run: |
response=$(curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"region": "${{ secrets.CLOUDFONE_REGION }}", "session_minutes": 15}' \
https://api.cloudf.one/v1/devices/lock)
echo "device_id=$(echo $response | jq -r .device_id)" >> $GITHUB_OUTPUT
echo "adb_endpoint=$(echo $response | jq -r .adb_endpoint)" >> $GITHUB_OUTPUT
echo "adb_token=$(echo $response | jq -r .adb_token)" >> $GITHUB_OUTPUT
- name: Connect ADB
run: |
adb connect ${{ steps.lock.outputs.adb_endpoint }}
adb devices
- name: Install APK
run: adb install -r app/build/outputs/apk/debug/app-debug.apk
- name: Run smoke test
run: |
adb shell am instrument -w -r \
com.example.app.test/androidx.test.runner.AndroidJUnitRunner
- name: Capture screenshot on failure
if: failure()
run: |
adb shell screencap /sdcard/failure.png
adb pull /sdcard/failure.png ./failure.png
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-artifacts
path: |
failure.png
**/build/reports/
- name: Unlock cloud phone
if: always()
run: |
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
https://api.cloudf.one/v1/devices/${{ steps.lock.outputs.device_id }}/unlock
every PR runs this workflow. failures upload screenshots and test reports as artifacts for debugging.
workflow 2: parallel device matrix testing
testing across multiple device tiers in parallel:
.github/workflows/device-matrix.yml:
name: Device matrix test
on:
push:
branches: [main, release/*]
schedule:
- cron: '0 18 * * 1-5' # 02:00 SGT weekdays
jobs:
matrix-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
device_tier: [low-end, mid-range, flagship, tablet]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Build APK
run: ./gradlew assembleDebug
- name: Set up ADB
run: |
sudo apt-get update
sudo apt-get install -y android-tools-adb
- name: Lock cloud phone (${{ matrix.device_tier }})
id: lock
run: |
response=$(curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"region": "SG", "device_tier": "${{ matrix.device_tier }}", "session_minutes": 30}' \
https://api.cloudf.one/v1/devices/lock)
echo "device_id=$(echo $response | jq -r .device_id)" >> $GITHUB_OUTPUT
echo "adb_endpoint=$(echo $response | jq -r .adb_endpoint)" >> $GITHUB_OUTPUT
- name: Connect and run tests
run: |
adb connect ${{ steps.lock.outputs.adb_endpoint }}
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am instrument -w -r \
-e size medium \
com.example.app.test/androidx.test.runner.AndroidJUnitRunner
- name: Upload device-specific results
if: always()
uses: actions/upload-artifact@v4
with:
name: results-${{ matrix.device_tier }}
path: app/build/reports/androidTests/
- name: Unlock cloud phone
if: always()
run: |
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
https://api.cloudf.one/v1/devices/${{ steps.lock.outputs.device_id }}/unlock
GitHub Actions runs four parallel jobs, one per device tier. all four cloud phones lock simultaneously, run the test suite, and report back independently. total wall time stays around 15 minutes regardless of how many tiers you test.
workflow 3: release candidate gate with full regression
before any release ships, run the full regression suite across all device tiers:
.github/workflows/release-gate.yml:
name: Release candidate gate
on:
push:
tags: ['v*']
jobs:
rc-gate:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
device_tier: [low-end, mid-range, flagship, tablet]
test_suite: [smoke, regression, payment, performance]
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Download release APK
run: gh release download ${{ github.ref_name }} -p '*.apk'
env:
GH_TOKEN: ${{ github.token }}
- name: Lock cloud phone
id: lock
run: |
response=$(curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"region": "SG", "device_tier": "${{ matrix.device_tier }}", "session_minutes": 45}' \
https://api.cloudf.one/v1/devices/lock)
echo "device_id=$(echo $response | jq -r .device_id)" >> $GITHUB_OUTPUT
echo "adb_endpoint=$(echo $response | jq -r .adb_endpoint)" >> $GITHUB_OUTPUT
- name: Run test suite
run: |
adb connect ${{ steps.lock.outputs.adb_endpoint }}
adb install -r ./*.apk
adb shell am instrument -w -r \
-e package com.example.app.test.${{ matrix.test_suite }} \
com.example.app.test/androidx.test.runner.AndroidJUnitRunner
- name: Capture failure artifacts
if: failure()
run: |
adb shell screencap /sdcard/failure.png
adb pull /sdcard/failure.png ./failure-${{ matrix.device_tier }}-${{ matrix.test_suite }}.png
adb logcat -d > logcat-${{ matrix.device_tier }}-${{ matrix.test_suite }}.txt
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: rc-failure-${{ matrix.device_tier }}-${{ matrix.test_suite }}
path: |
failure-*.png
logcat-*.txt
- name: Unlock cloud phone
if: always()
run: |
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.CLOUDFONE_TOKEN }}" \
https://api.cloudf.one/v1/devices/${{ steps.lock.outputs.device_id }}/unlock
promote-release:
needs: rc-gate
runs-on: ubuntu-latest
steps:
- name: Mark release ready
run: gh release edit ${{ github.ref_name }} --draft=false
env:
GH_TOKEN: ${{ github.token }}
this runs 16 parallel jobs (4 tiers x 4 test suites). if all pass, the release candidate auto-promotes from draft to published. if any fail, the release stays in draft and the failure artifacts give the team everything they need to debug.
error handling and timeouts
flaky tests can hang devices indefinitely. the timeout-minutes on each job protects against this. the cloud phone session also auto-expires (set via session_minutes in the lock request), so even if the workflow crashes without the unlock step, the device returns to the pool.
for cleanup safety, always include the unlock step with if: always(). this guarantees the device returns to the pool even on workflow failures.
authoritative reference on GitHub Actions matrix strategies is the GitHub Actions matrix documentation.
try cloud phone GitHub Actions integration on a real Singapore device
if you build mobile apps and want every PR to run on real devices, start a trial, generate an API token, and copy one of the workflows above into your repo to start in 30 minutes.
frequently asked questions
what’s the latency from a GitHub-hosted runner to a Singapore cloud phone?
ADB-over-network latency from GitHub’s US-east runners to Singapore is typically 200 to 300ms per command. for most test suites this is acceptable. for latency-sensitive tests, use self-hosted runners in the same region.
can I run iOS tests this way?
cloudf.one’s pool is Android. for iOS, integrate AWS Device Farm, Firebase Test Lab iOS, or BrowserStack into your GitHub Actions workflow alongside cloud Android.
how much does this cost in GitHub Actions minutes?
a typical 15-minute workflow on a 4-device matrix uses 60 GitHub Actions minutes per run. on the free tier (2,000 minutes per month), this supports about 30 runs per month. private repos with paid plans have higher limits.
can I use Maestro or Appium instead of Espresso?
yes. any test framework that talks to ADB works against cloud phones. swap the am instrument command for your Maestro CLI invocation or Appium server start.
what happens if the cloud phone lock request fails?
the workflow fails fast at the lock step. add a retry loop in the lock step using a wrapper script if your fleet is occasionally at capacity. cloud phones typically lock in under 5 seconds during business hours.