Can Android and iOS Builds Share One Workflow?
Yes. runs-on is a per-job setting, so an Android job on a Linux label and an iOS job on a macOS label live in the same GitHub Actions workflow file.
Last verified:
Yes. runs-on is declared on each job rather than on the workflow, so one file can hold an Android job on a Linux runner and an iOS job on a macOS runner (workflow syntax reference for runs-on, checked on 2026-08-13). WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, so both legs select a warp- label and the pair runs in parallel from the same trigger (WarpBuild cloud runners, checked on 2026-08-13).
Answer
The shape that works is one small job that computes the release metadata, two platform jobs that consume it, and a final job that collects the outputs. Jobs run in parallel by default, and needs is what creates the ordering (workflow syntax reference for needs). Values move down the graph through job outputs (workflow syntax reference for job outputs).
name: mobile-release
on:
pull_request:
push:
branches: [main]
jobs:
version:
runs-on: warp-ubuntu-latest-x64-2x
outputs:
build_number: ${{ steps.compute.outputs.build_number }}
semver: ${{ steps.compute.outputs.semver }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: compute
run: |
echo "build_number=$(git rev-list --count HEAD)" >> "$GITHUB_OUTPUT"
echo "semver=$(cat VERSION)" >> "$GITHUB_OUTPUT"
android:
needs: version
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- uses: WarpBuilds/cache@v1
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', 'gradle/libs.versions.toml') }}
- name: Assemble the release bundle
run: |
./gradlew bundleRelease \
-PversionCode=${{ needs.version.outputs.build_number }} \
-PversionName=${{ needs.version.outputs.semver }}
- uses: actions/upload-artifact@v4
with:
name: android-aab
path: app/build/outputs/bundle/release/app-release.aab
ios:
needs: version
runs-on: warp-macos-latest-arm64-6x
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/Library/Developer/Xcode/DerivedData
.build
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
- name: Stamp the build number
run: agvtool new-version -all ${{ needs.version.outputs.build_number }}
- name: Archive
run: |
xcodebuild archive \
-scheme App \
-destination "generic/platform=iOS" \
-archivePath build/App.xcarchive \
MARKETING_VERSION=${{ needs.version.outputs.semver }}
- uses: actions/upload-artifact@v4
with:
name: ios-archive
path: build/App.xcarchive
release:
needs: [android, ios]
runs-on: warp-ubuntu-latest-x64-2x
steps:
- uses: actions/download-artifact@v4
with:
path: dist
- run: ls -R distThe version job runs once, both platform jobs read needs.version.outputs.build_number, and the two binaries carry the same version string without a second computation. Per-minute rates for every label are on the pricing page.
Detail
The two legs bill at different rates
The Android leg and the iOS leg pay different per-minute rates, so a mixed workflow has one job that carries most of the bill. Every WarpBuild rate below comes from the cloud runners documentation, checked on 2026-08-13. Job minutes are a modeling assumption, so substitute your own.
| Job | Label | Shape | Per minute | Minutes | Cost per run |
|---|---|---|---|---|---|
| version | warp-ubuntu-latest-x64-2x | 2 vCPU, 8 GB | $0.004 | 1 | $0.004 |
| android | warp-ubuntu-latest-x64-8x | 8 vCPU, 32 GB | $0.016 | 9 | $0.144 |
| ios | warp-macos-latest-arm64-6x | 6 vCPU, 22 GB | $0.08 | 14 | $1.12 |
| release | warp-ubuntu-latest-x64-2x | 2 vCPU, 8 GB | $0.004 | 1 | $0.004 |
That is $1.272 per run, of which the macOS job is $1.12, or 88 percent. At 300 runs a month the workflow costs $381.60, split $336.00 on macOS minutes and $45.60 on Linux minutes. Tuning the Android leg moves cents; tuning the iOS leg moves dollars.
The same 14 macOS minutes on the largest GitHub-hosted macOS ARM64 runner, a 5 vCPU and 14 GB machine at $0.102 per minute, is $1.428 per run and $428.40 at 300 runs (GitHub Actions minute multipliers, checked on 2026-08-13). warp-macos-latest-arm64-6x is $0.08 per minute on a machine with one more vCPU and 8 GB more memory, which is about 22 percent lower list price and $92.40 a month at this volume.
Sizing up the iOS leg is arithmetic against that number. warp-macos-26-arm64-12x is 12 vCPU with 44 GB at $0.16 per minute, so it costs less per run than the 6 vCPU label only once the archive finishes under 7 minutes, since $1.12 divided by $0.16 is 7. Measure the current job before you switch. Per-job durations and queue times are visible in CI observability, which sits alongside snapshot runners, remote Docker builders, an MCP server, and the Action Debugger in the WarpBuild product surface.
What the two jobs can share
| Shared thing | Mechanism | Constraint |
|---|---|---|
| Version numbers and release metadata | outputs on the upstream job, read through needs | Strings only, so structured data has to be serialized |
| Build products such as an AAB and an xcarchive | actions/upload-artifact then actions/download-artifact | File permissions are not maintained during upload (upload-artifact), so re-apply an executable bit after download |
| Secrets, variables, and concurrency groups | Repository or environment scope, declared once | Applies to the workflow, no per-platform copy needed |
| Source code | actions/checkout@v4 in each job | Each job checks out separately; runners share no filesystem |
The two platform jobs never share a machine. A job gets its own ephemeral runner, and everything that crosses the gap between jobs crosses it as an artifact or an output.
What the two jobs cannot share
Caches. Cache version is a hash over the compression tool the runner OS selects and the paths being cached, which means a cache created on warp-macos-14-arm64-6x cannot be restored on warp-ubuntu-latest-x64-4x (WarpBuild caching documentation). Even a shared key resolves to a different entry on the other platform.
The paths diverge as well. The Android leg caches ~/.gradle/caches and ~/.gradle/wrapper; the iOS leg caches ~/Library/Developer/Xcode/DerivedData and the SwiftPM .build directory. A single key covering both restores nothing useful on either. Put runner.os in every cache key and keep the two entries independent.
Cache backends differ too. The WarpBuild cache is available on all Linux runners and enabled by default, and it is not supported on Windows runners (cloud runners documentation), so the Android leg can use WarpBuilds/cache@v1 as a drop-in for actions/cache@v4 while the iOS leg stays on actions/cache@v4.
One more split is worth planning for. macOS runners do not support nested virtualization and cannot run Docker, and Android instrumented tests that need /dev/kvm require the nested-virtualization.enabled=true dynamic label on an x86-64 Linux runner (cloud runners documentation). Container fixtures and emulator jobs therefore belong on the Linux side of the file, which is where they already are in this shape.
Related Questions
Can one GitHub Actions workflow file target two operating systems?
Yes. runs-on is declared per job, so the Android job carries warp-ubuntu-latest-x64-8x and the iOS job carries warp-macos-latest-arm64-6x in the same file. Both start in parallel unless a needs edge orders them. The label catalog with sizes and rates is on the macOS runner page.
Should I use a matrix instead of two separate jobs?
No. A matrix repeats one list of steps across values, and the two legs run different toolchains, different caches, and different signing paths. Use a matrix inside a leg instead, such as several simulator destinations in the iOS job or several product flavors in the Android job.
Can the Android job restore a cache the iOS job saved?
No. Cache version differs by runner OS and cached paths, so entries do not cross platforms (caching documentation). Key each leg with runner.os and treat the two caches as unrelated.
Does this change for React Native or Flutter?
The job graph stays the same and the shared step grows. Both frameworks add a JavaScript or Dart dependency install that each platform leg repeats, so the version job often also produces a lockfile hash the two legs key their caches on. The React Native workflow page and the Flutter workflow page cover the framework-specific steps.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.