A GitHub Action that cross-compiles a Swift package for Android from a Linux or macOS runner and then runs its SwiftPM test targets inside an Android emulator.
Under the hood the action:
- installs a Swift host toolchain matching the requested version,
- installs the matching Swift SDK for Android (the official cross-compilation bundle from swift.org, or — for Swift 6.0/6.1 — the pre-official bundle maintained at skiptools/swift-android-toolchain),
- builds your
Package.swift, - boots an Android emulator (using the step-security-maintained step-security/android-emulator-runner) and runs the resulting test binaries inside it.
If you only need the build (no tests), set run-tests: false — this skips emulator boot and is dramatically faster.
The shortest possible workflow — checkout, run host-side swift test on Linux, then run the same package on Android:
name: ci
on:
push:
branches: ['*']
pull_request:
branches: ['*']
workflow_dispatch:
jobs:
linux-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Swift tests (Linux)
run: swift test
- name: Swift tests (Android emulator)
uses: step-security/swift-android-action@v2
with:
# Ubuntu runners are often tight on disk; reclaim some before the emulator boots
free-disk-space: truePinning a specific Swift version is just one more input:
- name: Swift tests (Android emulator)
uses: step-security/swift-android-action@v2
with:
swift-version: '6.3'Important
Always quote swift-version. Bare YAML numbers like 6.3 are parsed as floats and would collapse to 6. Use swift-version: '6.3', not swift-version: 6.3.
swift-version accepts several forms:
| Form | Example | What you get |
|---|---|---|
| Released semver | '6.3' / '6.2.3' |
The matching stable Swift release |
Alias latest |
'latest' |
Newest stable release from swift.org |
Alias snapshot |
'snapshot' |
Newest DEVELOPMENT-SNAPSHOT |
Alias latest-6.2 |
'latest-6.2' |
Newest 6.2.x non-snapshot |
| Nightly stream | 'nightly-main', 'nightly-6.3' |
Latest nightly for that branch |
| Pinned dev snapshot | '6.3-DEVELOPMENT-SNAPSHOT-2025-12-07-a', 'DEVELOPMENT-SNAPSHOT-2025-12-07-a' |
Exactly that snapshot |
Note
The official Swift SDK for Android (from swift.org) is used starting with the nightly-main tag, per the Nightly Swift SDK for Android announcement. For Swift 6.0 and 6.1, the action falls back to the pre-official bundle generated by the swift-android-sdk project.
| Name | Default | What it does |
|---|---|---|
swift-version |
latest |
Swift toolchain to install. See the table above for accepted forms. |
swift-branch |
(inferred) | Override for the swift.org download branch (e.g. swift-6.3-release, development). |
ndk-version |
(runner default) | NDK selector for Swift 6.2+. Accepts 27, 27.0, 27.2.12479018, or latest. |
package-path |
. |
Workspace-relative path to the Swift package to build. |
swift-configuration |
debug |
Build configuration: debug or release. |
swift-build-flags |
(empty) | Extra arguments appended to every swift build invocation. |
swift-test-flags |
(empty) | Extra arguments forwarded to the test-runner binaries on device. |
build-package |
true |
If false, the action stops after installing the toolchain/SDK. |
build-tests |
true |
If false, builds sources only (no --build-tests). |
run-tests |
true |
If false, skips emulator launch entirely. |
copy-files |
(empty) | Additional files/dirs (relative to package-path) staged next to the test binaries. |
test-env |
(empty) | Whitespace-separated KEY=VALUE pairs exported into the on-device test process. |
free-disk-space |
false |
If true, deletes large pre-installed runner tools (Xcodes, dotnet, chromium, etc.) before the emulator boots. |
android-api-level |
28 |
API level the emulator boots against (also drives the compile SDK triple by default). |
compile-sdk-api-level |
(= android-api-level) |
Override the API level baked into the compile SDK build triple (e.g. 23 → x86_64-unknown-linux-android23). |
android-channel |
stable |
Android SDK Manager channel: stable, beta, dev, canary. |
android-profile |
(runner default) | Hardware profile name used when creating the AVD (e.g. pixel). |
android-target |
default |
System-image target for the AVD (e.g. default, google_apis, aosp_atd). |
android-cores |
2 |
CPU cores allocated to the emulator. |
android-emulator-options |
-no-window -noaudio -no-boot-anim |
Extra CLI flags forwarded to the emulator process. |
android-emulator-boot-timeout |
600 |
Seconds to wait for the emulator to finish booting. |
android-emulator-test-folder |
/data/local/tmp/swift-android-tests |
On-device folder used to stage and run the test bundle. |
installed-swift |
(empty) | Path to a Swift host toolchain that is already installed on the runner. |
installed-sdk |
(empty) | Triple of a Swift Android SDK already installed on the runner. |
custom-sdk-url |
(empty) | Direct URL to a custom Swift Android SDK artifactbundle archive. Requires installed-sdk and custom-sdk-id. |
custom-sdk-id |
(empty) | Artifact identifier used when installing the SDK referenced by custom-sdk-url. |
| Output | Description |
|---|---|
swift-build |
Fully-formed swift build command, wired to the installed host toolchain and Android SDK. |
swift-sdk |
Selected SDK target triple (e.g. x86_64-unknown-linux-android24). |
swift-install |
Absolute path to the installed Swift host toolchain (the usr directory containing bin/swift). |
The action runs on any GitHub-hosted (or compatible self-hosted) ubuntu-* and macos-* runner image.
ARM macOS caveat. macOS-on-ARM runners (macos-14, macos-15, macos-26, …) cannot do the nested virtualization the Android emulator. On those images you must disable tests:
jobs:
macos-android:
runs-on: macos-26
steps:
- uses: actions/checkout@v6
- name: Swift tests (macOS)
run: swift test
- name: Swift build only (Android)
uses: step-security/swift-android-action@v2
with:
run-tests: falseIf you need the tests to actually execute on a macOS host, use the Intel runner macos-15-intel, or a larger paid macOS image such as macos-14-large / macos-15-large.
Tests written against Bundle.module work transparently — the produced .resources directory is pushed to the emulator automatically.
When a test needs additional files that aren't part of Bundle.module (for example, the test wants to read source files at runtime), list them with copy-files. The paths are resolved relative to package-path and are dropped next to the test binaries on the device.
Environment variables expected by the test process can be set with test-env, which is parsed as a whitespace-separated list of KEY=VALUE pairs.
- name: Run Android tests with fixtures + env
uses: step-security/swift-android-action@v2
with:
copy-files: Tests
test-env: TEST_WORKSPACE=1If you want to build multiple packages against the same toolchain — or you want to drive swift build yourself — set build-package: false. The action will install the host toolchain and the Android SDK and stop. You can then read outputs.swift-build to get a ready-to-run swift build … command with all the right paths and SDK flags:
- name: Stage the Swift toolchain + Android SDK
id: setup-toolchain
uses: step-security/swift-android-action@v2
with:
build-package: false
- name: Check out apple/swift-numerics
uses: actions/checkout@v6
- name: Build apple/swift-numerics in both configurations
run: |
# debug
${{ steps.setup-toolchain.outputs.swift-build }} -c debug
ls .build/${{ steps.setup-toolchain.outputs.swift-sdk }}/debug
# release
${{ steps.setup-toolchain.outputs.swift-build }} -c release
ls .build/${{ steps.setup-toolchain.outputs.swift-sdk }}/releaseThe actual swift-build string varies by host. Two representative examples:
-
Ubuntu 24.04
/home/runner/swift/toolchains/swift-6.3-RELEASE/usr/bin/swift build --swift-sdk x86_64-unknown-linux-android24 -
macOS 15
/Users/runner/Library/Developer/Toolchains/swift-6.3-RELEASE.xctoolchain/usr/bin/swift build --swift-sdk aarch64-unknown-linux-android24
A fuller ci.yml that exercises a Swift package on macOS, iOS, Linux, Android, and Windows:
name: swift-algorithms ci
on:
push:
branches: ['*']
pull_request:
branches: ['*']
workflow_dispatch:
jobs:
linux-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Swift tests (Linux)
run: swift test
- name: Swift tests (Android emulator)
uses: step-security/swift-android-action@v2
macos-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Swift tests (macOS)
run: swift test
- name: Swift tests (iOS simulator)
run: |
xcodebuild test \
-sdk "iphonesimulator" \
-destination "platform=iOS Simulator,name=iPhone 17" \
-scheme "$(xcodebuild -list -json | jq -r '.workspace.schemes[-1]')"
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Install Swift on Windows
uses: step-security/gha-setup-swift@v0
with:
branch: swift-6.3-release
tag: 6.3-RELEASE
- name: Swift tests (Windows)
run: swift testA real run history of this exact pattern lives in the swift-sqlite repository.
Practical guidance for porting existing Swift packages to Android lives in the Skip blog post: Bringing Swift Packages to Android.
If you see this message in the logs, the real failure is almost always inside the emulator launch. Expand the Launch Emulator section in the job logs — the actual cause is usually a few lines above the timeout.
The single most common cause is insufficient disk space on the runner. The emulator needs roughly 7 GB free to create its userdata partition, and a fresh Ubuntu runner often has less than that. The failure typically looks like:
FATAL | Not enough space to create userdata partition. Available: 6791.23 MB at /home/runner/.android/avd/test.avd, need 7372.80 MB.
Two fixes:
-
Reclaim disk yourself earlier in the job (delete tools you don't need).
-
Enable the action's built-in cleanup with
free-disk-space: true:- name: Swift tests (Android emulator) uses: step-security/swift-android-action@v2 with: free-disk-space: true
See the
action.ymlsource for the exact list of folders the cleanup removes — if your job actually needs one of them, do step 1 instead.
YAML coerces unquoted version literals like 6.3 into floats, which collapse to 6 and resolve to Swift 6.x.x — not 6.3. Always quote them:
- uses: step-security/swift-android-action@v2
with:
swift-version: '6.3' # ✅Not:
- uses: step-security/swift-android-action@v2
with:
swift-version: 6.3 # ❌ — becomes "6"