GitHub Actions for Mobile App Distribution: The Complete 2026 Guide
Your CI/CD pipeline builds the app. But what happens to the binary after that? Most teams manually download the artifact, open a browser, upload to some distribution service, then message the team on Slack. Others cobble together custom scripts that break every time the CI environment updates. There is a better way.
This guide covers complete, copy-paste-ready GitHub Actions workflows for distributing mobile apps to testers via TestApp.io. We will walk through native Android, native iOS, Flutter, and React Native (including Expo). Every workflow follows the same three-step pattern: build, upload, notify.
The Pattern: Build, Upload, Notify
Regardless of framework, the distribution workflow always looks the same:
- Build step produces a binary (.apk for Android, .ipa for iOS). This part is framework-specific.
- Upload step pushes the binary to TestApp.io using either the ta-cli command-line tool or the testappio/github-action GitHub Action. This part is identical for every framework.
- Notify step (optional) tells your team the build is ready. TestApp.io handles this natively through Slack, Microsoft Teams, and email notifications.
The upload step is the key. Once you have a .apk or .ipa file, a single GitHub Action step sends it to TestApp.io. Your testers get an install link. No manual work, no custom scripts, no managing download URLs.
Setting Up Secrets
Before writing any workflow, add these secrets to your GitHub repository (Settings > Secrets and variables > Actions):
| Secret Name | Where to Find It |
|---|---|
TESTAPPIO_API_TOKEN | Generate at portal.testapp.io/settings/api-credentials |
TESTAPPIO_APP_ID | Your app's Integrations tab in the TestApp.io portal |
These two secrets are all you need. Every workflow below references them.
Native Android: Build APK with Gradle
The simplest case. Gradle builds the APK, and the GitHub Action uploads it.
Full setup guide: GitHub Actions setup guide
That is the entire workflow. The testappio/github-action@v5 step handles authentication, chunked upload, and team notifications. Swap assembleRelease for assembleDebug if you want debug builds instead. The APK path changes to app/build/outputs/apk/debug/app-debug.apk accordingly.
git_release_notes: true to automatically use your latest commit message as the release note. Testers see exactly what changed without any manual effort.Native iOS: Archive and Export IPA
iOS builds require a macOS runner and code signing. The build process has two stages: archive the project, then export the IPA.
Full setup guide: GitHub Actions setup guide
A few things to note about this workflow:
- Code signing is the hard part. The certificate and provisioning profile are stored as base64-encoded secrets, decoded at runtime, and imported into a temporary keychain. This is the standard approach for GitHub Actions iOS builds.
- ExportOptions.plist controls the export method. Use
ad-hocordevelopmentfor TestApp.io distribution. Enterprise certificates work too. - macOS runners cost more than Linux runners in GitHub Actions. Keep your iOS jobs lean by caching dependencies where possible.
Flutter: Build Both Platforms
Flutter can produce both APK and IPA from the same codebase. You can build them in parallel using a matrix strategy, or sequentially in separate jobs. Here is a workflow that builds and distributes both:
Full setup guide: Flutter distribution guide
The Android job runs on ubuntu-latest (faster, cheaper), while the iOS job runs on macos-latest (required for Xcode). Both jobs run in parallel by default, so your total build time equals the slowest job rather than the sum of both.
For a deeper look at Flutter distribution workflows, check out our guide on how to distribute Flutter apps to testers.
React Native: Bare Workflow
React Native bare workflow projects build through native toolchains directly. Android uses Gradle, and iOS uses xcodebuild, just like native projects.
Full setup guide: React Native distribution guide
Notice that the iOS job is essentially identical to the native iOS workflow. That is the point. React Native bare workflow compiles to a standard Xcode project, so the build and signing process is the same.
For more on distributing React Native apps, see our guide on how to distribute React Native apps to testers.
React Native: Expo with EAS Build
If you use Expo with EAS Build, the build happens on Expo's servers rather than in your GitHub Actions runner. The workflow triggers the build, waits for it to finish, downloads the artifact, and then uploads it to TestApp.io.
Full setup guide: Expo EAS + GitHub Actions guide
--output flag tells EAS to download the completed build artifact to a local path. The --wait flag makes the command block until the build completes. Make sure your EAS build profile produces an APK (not APK) for Android distribution through TestApp.io.For iOS with Expo, the workflow is similar. You can let EAS handle signing and use eas build --platform ios --profile preview --non-interactive --wait --output build/app.ipa to download the IPA locally, then upload it with the same GitHub Action step.
Advanced: Matrix Builds
If you want to build for multiple platforms in a single workflow definition, GitHub Actions matrix strategy keeps things DRY:
Full setup guide: GitHub Actions setup guide
This approach is great for native projects where each platform builds independently. For cross-platform frameworks like Flutter and React Native, the separate-jobs approach shown earlier is usually cleaner.
Advanced: Release Notes from Git History
The GitHub Action supports two built-in options for generating release notes from your git history:
git_release_notes: trueuses the latest commit message as the release note. Simple and effective for teams that write meaningful commit messages.include_git_commit_id: trueprepends the short commit hash to the release notes, making it easy to trace a build back to a specific commit.
For more control, you can generate custom release notes in an earlier step and pass them through:
- name: Generate release notes
id: notes
run: |
NOTES=$(git log --oneline ${{ github.event.before }}..${{ github.sha }})
echo "notes=$NOTES" >> $GITHUB_OUTPUT
- name: Upload to TestApp.io
uses: testappio/github-action@v5
with:
api_token: ${{ secrets.TESTAPPIO_API_TOKEN }}
app_id: ${{ secrets.TESTAPPIO_APP_ID }}
file: app/build/outputs/apk/release/app-release.apk
release_notes: ${{ steps.notes.outputs.notes }}
notify: trueThis gives you a changelog-style release note with every commit since the last push. Your testers see exactly what changed, and you can trace any issue back to a specific commit.
The ta-cli Alternative
The testappio/github-action@v5 wraps ta-cli, the TestApp.io command-line tool. If you prefer direct CLI usage or need it for a CI platform other than GitHub Actions, you can install and run ta-cli directly:
- name: Install ta-cli
run: |
curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-linux -o ta-cli
chmod +x ta-cli
- name: Upload with ta-cli
run: |
./ta-cli publish \
--api_token=${{ secrets.TESTAPPIO_API_TOKEN }} \
--app_id=${{ secrets.TESTAPPIO_APP_ID }} \
--release=android \
--apk=app/build/outputs/apk/release/app-release.apk \
--release_notes="Built from ${{ github.sha }}" \
--git_release_notes \
--git_commit_id \
--notifyThe ta-cli publish command accepts these flags:
| Flag | Description | Default |
|---|---|---|
--api_token | Your API token (required) | |
--app_id | Your app ID (required) | |
--release | android, ios, or both | both |
--apk | Path to .apk file | |
--ipa | Path to .ipa file | |
--release_notes | Release notes text (max 1200 chars) | |
--git_release_notes | Use last git commit message as notes | false |
--git_commit_id | Include commit hash in notes | false |
--notify | Notify team members | false |
--archive_latest_release | Archive previous release on upload | false |
This same ta-cli works on GitLab CI, CircleCI, Jenkins, Azure DevOps, Bitrise, Codemagic, Travis CI, Xcode Cloud, and any other CI environment that runs Linux or macOS.
Conditional Uploads
Not every push needs a distribution build. You can limit uploads to specific branches, tags, or conditions:
# Only distribute on release tags
on:
push:
tags:
- "v*"
# Or only distribute when a PR merges to main
on:
push:
branches: [main]
# Or use a manual trigger
on:
workflow_dispatch:
inputs:
platform:
description: "Platform to build"
required: true
default: "both"
type: choice
options:
- android
- ios
- bothYou can also add an if condition to the upload step to skip distribution for certain commits:
- name: Upload to TestApp.io
if: ${{ !contains(github.event.head_commit.message, '[skip-dist]') }}
uses: testappio/github-action@v5
with:
api_token: ${{ secrets.TESTAPPIO_API_TOKEN }}
app_id: ${{ secrets.TESTAPPIO_APP_ID }}
file: app/build/outputs/apk/release/app-release.apk
notify: trueAdd [skip-dist] to any commit message and the distribution step gets skipped, while the build still runs.
Wrapping Up
The core idea is simple: your CI pipeline already builds the binary. Adding distribution is just one more step. The testappio/github-action@v5 works the same way regardless of whether that binary came from Gradle, xcodebuild, Flutter, React Native, or Expo EAS. Build the artifact, point the action at it, and your testers get an install link.
If you are not using GitHub Actions, the same ta-cli that powers this action runs on any CI platform. And if you prefer Fastlane, TestApp.io has a plugin for that too.
For more on automating mobile distribution pipelines, see our guide on automating mobile app distribution with CI/CD. For detailed setup instructions on any specific CI platform, check out the TestApp.io Help Center integration guides.
Ship Mobile Apps Faster with TestApp.io
TestApp.io helps mobile teams distribute builds to testers, collect feedback, and manage releases, all in one place. Support for iOS (IPA) and Android (APK), with integrations for Slack, Microsoft Teams, Jira, Linear, and 10+ CI/CD platforms.
👉 Get started free or explore the Help Center to learn more.