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:

  1. Build step produces a binary (.apk for Android, .ipa for iOS). This part is framework-specific.
  2. 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.
  3. 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.

ℹ️
TestApp.io supports APK for Android and IPA for iOS. The upload uses chunked, resumable transfers, so large builds will not fail on flaky CI connections.

Setting Up Secrets

Before writing any workflow, add these secrets to your GitHub repository (Settings > Secrets and variables > Actions):

Secret NameWhere to Find It
TESTAPPIO_API_TOKENGenerate at portal.testapp.io/settings/api-credentials
TESTAPPIO_APP_IDYour 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.

💡
Set 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-hoc or development for 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.
⚠️
TestApp.io requires the IPA to be signed with a Development, Ad Hoc, or Enterprise provisioning profile. App Store distribution profiles will not work for direct device installation.

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

ℹ️
The --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: true uses the latest commit message as the release note. Simple and effective for teams that write meaningful commit messages.
  • include_git_commit_id: true prepends 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: true

This 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 \
            --notify

The ta-cli publish command accepts these flags:

FlagDescriptionDefault
--api_tokenYour API token (required)
--app_idYour app ID (required)
--releaseandroid, ios, or bothboth
--apkPath to .apk file
--ipaPath to .ipa file
--release_notesRelease notes text (max 1200 chars)
--git_release_notesUse last git commit message as notesfalse
--git_commit_idInclude commit hash in notesfalse
--notifyNotify team membersfalse
--archive_latest_releaseArchive previous release on uploadfalse

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
          - both

You 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: true

Add [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.