← back to blog

cloud phone CI: GitHub Actions workflow examples 2026

May 07, 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

step 1: store secrets in GitHub

go to your repository -> Settings -> Secrets and variables -> Actions. add:

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.