How to Automate Mobile App Distribution with CI/CD

You just finished a two-hour debugging session. The fix is merged, the build passes, and it's time to get the new APK to your testers. So you open your CI dashboard, download the artifact, log into your distribution platform, click through the upload wizard, type in the release notes, select the right app, the right group, and hit upload. Ten minutes of manual work for something that should have happened automatically when the build succeeded.

Now multiply that by every build, every day, across every branch your team works on. Manual distribution is a tax on your velocity — small enough to ignore on any single day, large enough to matter over weeks and months.

This guide covers how to automate mobile app distribution using TestApp.io's CLI tool (ta-cli) integrated with every major CI/CD platform. By the end, every successful build will be automatically distributed to your testers with zero manual intervention.

Why Automate Distribution?

Before diving into implementation, let's be explicit about what you gain:

  • Speed. Builds reach testers in minutes after a merge, not hours after someone remembers to upload.
  • Consistency. Every build is uploaded the same way with the same metadata. No missed release notes, no wrong app IDs, no "I uploaded the debug build by mistake."
  • Traceability. Automated uploads include commit hashes, branch names, and build numbers. You always know exactly which code a tester is running.
  • Reliability. ta-cli uses resumable upload chunked uploads, which means large IPA and APK files don't fail halfway through on flaky CI network connections. Uploads resume from where they left off.

The overhead of setting this up is measured in minutes. The time savings compound over every build for the life of the project.

Meet ta-cli

ta-cli is TestApp.io's command-line tool for publishing builds. It runs on Linux, macOS, and Windows, and is designed specifically for CI/CD environments.

The basic command:

./ta-cli publish \
  --api_token=YOUR_API_TOKEN \
  --app_id=YOUR_APP_ID \
  --release_notes="Your release notes here" \
  --file=path/to/app.apk

Key Flags

FlagDescription
--api_tokenYour TestApp.io API token. Always store this as a CI secret — never hardcode it.
--app_idThe ID of the app in TestApp.io. Found in your app settings.
--release_notesRelease notes for this build. Supports dynamic values from your CI environment.
--filePath to the build artifact. Supports APK, and IPA files.

ta-cli automatically detects the file type and extracts metadata (version name, version code, bundle identifier) from the binary. You don't need to specify these manually.

resumable upload Chunked Uploads

Mobile build artifacts are large. An IPA file can easily be 100MB or more. APK files can be similarly hefty. Uploading these over CI/CD network connections — which can be throttled, shared, or occasionally unstable — is a reliability problem.

ta-cli solves this with resumable upload (resumable upload protocol) chunked uploads. The file is split into chunks and uploaded sequentially. If a chunk fails, only that chunk is retried — not the entire file. If the network drops and recovers, the upload resumes from the last successful chunk.

This is especially important for CI environments where you're paying per-minute for build time. A failed upload that has to restart from zero on a 15-minute timeout costs real money.

Platform-by-Platform Setup

Let's walk through setting up automated distribution on every major CI/CD platform. Each example assumes you've already configured your build pipeline to produce the artifact (APK, or IPA). We're adding the distribution step.

GitHub Actions

GitHub Actions is the most common CI/CD platform for mobile teams. Here's how to add TestApp.io distribution as a step in your workflow.

name: Build and Distribute

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-distribute:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Build APK
        run: ./gradlew assembleRelease

      - name: Upload to TestApp.io
        run: |
          curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
          chmod +x ta-cli
          ./ta-cli publish \
            --api_token=${{ secrets.TESTAPPIO_API_TOKEN }} \
            --app_id=${{ secrets.TESTAPPIO_APP_ID }} \
            --release_notes="Build from ${{ github.ref_name }} - ${{ github.sha }}" \
            --file=app/build/outputs/apk/release/app-release.apk

Setup steps:

  1. Go to your GitHub repository Settings → Secrets and variables → Actions.
  2. Add TESTAPPIO_API_TOKEN with your API token from TestApp.io.
  3. Add TESTAPPIO_APP_ID with your app's ID.
  4. Add the upload step after your build step in the workflow file.

For iOS builds on macOS runners, swap the download URL:

      - name: Upload IPA to TestApp.io
        run: |
          curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Darwin-x86_64 -o ta-cli
          chmod +x ta-cli
          ./ta-cli publish \
            --api_token=${{ secrets.TESTAPPIO_API_TOKEN }} \
            --app_id=${{ secrets.TESTAPPIO_APP_ID }} \
            --release_notes="iOS build from ${{ github.ref_name }}" \
            --file=build/MyApp.ipa

Bitrise

Bitrise is popular among mobile teams for its first-class support for iOS and Android builds. Adding TestApp.io distribution is a Script step in your workflow.

workflows:
  deploy:
    steps:
      - android-build@1:
          inputs:
            - variant: release
      - script@1:
          title: Upload to TestApp.io
          inputs:
            - content: |
                #!/bin/bash
                set -e
                curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
                chmod +x ta-cli
                ./ta-cli publish \
                  --api_token="$TESTAPPIO_API_TOKEN" \
                  --app_id="$TESTAPPIO_APP_ID" \
                  --release_notes="Bitrise build #$BITRISE_BUILD_NUMBER from $BITRISE_GIT_BRANCH" \
                  --file="$BITRISE_APK_PATH"

Setup steps:

  1. In the Bitrise Workflow Editor, go to Secrets.
  2. Add TESTAPPIO_API_TOKEN and TESTAPPIO_APP_ID as secret environment variables.
  3. Add the Script step after your build step.
  4. Bitrise exposes the build artifact path as $BITRISE_APK_PATH (Android) or $BITRISE_IPA_PATH (iOS), so you can reference it directly.

CircleCI

CircleCI workflows use jobs and steps. Here's how to add TestApp.io distribution to your pipeline.

version: 2.1

jobs:
  build-and-distribute:
    docker:
      - image: cimg/android:2024.01
    steps:
      - checkout
      - run:
          name: Build APK
          command: ./gradlew assembleRelease
      - run:
          name: Upload to TestApp.io
          command: |
            curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
            chmod +x ta-cli
            ./ta-cli publish \
              --api_token="$TESTAPPIO_API_TOKEN" \
              --app_id="$TESTAPPIO_APP_ID" \
              --release_notes="CircleCI build #$CIRCLE_BUILD_NUM from $CIRCLE_BRANCH" \
              --file=app/build/outputs/apk/release/app-release.apk

workflows:
  build-deploy:
    jobs:
      - build-and-distribute:
          filters:
            branches:
              only:
                - main
                - develop

Setup steps:

  1. In your CircleCI project, go to Project Settings → Environment Variables.
  2. Add TESTAPPIO_API_TOKEN and TESTAPPIO_APP_ID.
  3. Add the upload step to your job definition.

Fastlane

Fastlane is the de facto automation tool for iOS and Android build pipelines. TestApp.io provides a dedicated Fastlane plugin: fastlane-plugin-testappio.

# Gemfile
gem 'fastlane-plugin-testappio'
# Fastfile
default_platform(:ios)

platform :ios do
  desc "Build and distribute to TestApp.io"
  lane :distribute do
    build_app(
      scheme: "MyApp",
      export_method: "ad-hoc",
      output_directory: "./build"
    )

    testappio(
      api_token: ENV["TESTAPPIO_API_TOKEN"],
      app_id: ENV["TESTAPPIO_APP_ID"],
      release_notes: "Fastlane build from #{git_branch}",
      file: lane_context[SharedValues::IPA_OUTPUT_PATH]
    )
  end
end

platform :android do
  desc "Build and distribute to TestApp.io"
  lane :distribute do
    gradle(
      task: "assemble",
      build_type: "Release"
    )

    testappio(
      api_token: ENV["TESTAPPIO_API_TOKEN"],
      app_id: ENV["TESTAPPIO_APP_ID"],
      release_notes: "Fastlane Android build from #{git_branch}",
      file: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]
    )
  end
end

Setup steps:

  1. Add the plugin to your Gemfile and run bundle install.
  2. Set TESTAPPIO_API_TOKEN and TESTAPPIO_APP_ID as environment variables (or use a .env file that's gitignored).
  3. Add the testappio action to your lane after the build step.

The Fastlane plugin automatically picks up the build artifact path from Fastlane's lane context, so you don't need to hardcode file paths.

Jenkins

Jenkins pipelines use Groovy-based Jenkinsfiles. Here's how to add TestApp.io distribution.

pipeline {
    agent any

    environment {
        TESTAPPIO_API_TOKEN = credentials('testappio-api-token')
        TESTAPPIO_APP_ID = credentials('testappio-app-id')
    }

    stages {
        stage('Build') {
            steps {
                sh './gradlew assembleRelease'
            }
        }

        stage('Distribute') {
            steps {
                sh '''
                    curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
                    chmod +x ta-cli
                    ./ta-cli publish \
                        --api_token="$TESTAPPIO_API_TOKEN" \
                        --app_id="$TESTAPPIO_APP_ID" \
                        --release_notes="Jenkins build #${BUILD_NUMBER} from ${GIT_BRANCH}" \
                        --file=app/build/outputs/apk/release/app-release.apk
                '''
            }
        }
    }
}

Setup steps:

  1. In Jenkins, go to Manage Jenkins → Manage Credentials.
  2. Add testappio-api-token and testappio-app-id as Secret text credentials.
  3. Reference them in your Jenkinsfile using the credentials() function.

Xcode Cloud

Xcode Cloud uses custom build scripts. Add a post-build script to upload the IPA after a successful archive.

Create the file ci_scripts/ci_post_xcodebuild.sh in your project:

#!/bin/bash
set -e

# Only run after successful archive
if [ "$CI_XCODEBUILD_ACTION" != "archive" ]; then
    exit 0
fi

# Find the IPA
IPA_PATH=$(find "$CI_ARCHIVE_EXPORT_PATH" -name "*.ipa" -print -quit)

if [ -z "$IPA_PATH" ]; then
    echo "No IPA found. Skipping upload."
    exit 0
fi

curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Darwin-x86_64 -o ta-cli
chmod +x ta-cli
./ta-cli publish \
    --api_token="$TESTAPPIO_API_TOKEN" \
    --app_id="$TESTAPPIO_APP_ID" \
    --release_notes="Xcode Cloud build #$CI_BUILD_NUMBER" \
    --file="$IPA_PATH"

Set TESTAPPIO_API_TOKEN and TESTAPPIO_APP_ID as environment variables in your Xcode Cloud workflow settings.

GitLab CI/CD

stages:
  - build
  - distribute

build-android:
  stage: build
  image: jangrewe/gitlab-ci-android
  script:
    - ./gradlew assembleRelease
  artifacts:
    paths:
      - app/build/outputs/apk/release/

distribute:
  stage: distribute
  image: alpine:latest
  dependencies:
    - build-android
  before_script:
    - apk add --no-cache curl
  script:
    - curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
    - chmod +x ta-cli
    - ./ta-cli publish
        --api_token="$TESTAPPIO_API_TOKEN"
        --app_id="$TESTAPPIO_APP_ID"
        --release_notes="GitLab CI build #$CI_PIPELINE_IID from $CI_COMMIT_REF_NAME"
        --file=app/build/outputs/apk/release/app-release.apk
  only:
    - main
    - develop

Azure Pipelines

trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: Gradle@3
    inputs:
      gradleWrapperFile: 'gradlew'
      tasks: 'assembleRelease'

  - script: |
      curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
      chmod +x ta-cli
      ./ta-cli publish \
        --api_token="$(TESTAPPIO_API_TOKEN)" \
        --app_id="$(TESTAPPIO_APP_ID)" \
        --release_notes="Azure build #$(Build.BuildId) from $(Build.SourceBranchName)" \
        --file=app/build/outputs/apk/release/app-release.apk
    displayName: 'Upload to TestApp.io'

Codemagic

workflows:
  android-distribute:
    name: Android Build & Distribute
    scripts:
      - name: Build APK
        script: ./gradlew assembleRelease
      - name: Upload to TestApp.io
        script: |
          curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
          chmod +x ta-cli
          ./ta-cli publish \
            --api_token=$TESTAPPIO_API_TOKEN \
            --app_id=$TESTAPPIO_APP_ID \
            --release_notes="Codemagic build #$CM_BUILD_ID" \
            --file=app/build/outputs/apk/release/app-release.apk

Travis CI

language: android

script:
  - ./gradlew assembleRelease

after_success:
  - curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
  - chmod +x ta-cli
  - ./ta-cli publish
      --api_token="$TESTAPPIO_API_TOKEN"
      --app_id="$TESTAPPIO_APP_ID"
      --release_notes="Travis CI build #$TRAVIS_BUILD_NUMBER from $TRAVIS_BRANCH"
      --file=app/build/outputs/apk/release/app-release.apk

Advanced Patterns

The examples above cover the basics. Here are patterns that make automated distribution even more useful.

Dynamic Release Notes from Commit Messages

Hardcoded release notes like "new build" aren't helpful for testers. Instead, generate release notes from your commit history:

# Get commits since last tag
RELEASE_NOTES=$(git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s" 2>/dev/null || git log -10 --pretty=format:"- %s")

./ta-cli publish \
  --api_token="$TESTAPPIO_API_TOKEN" \
  --app_id="$TESTAPPIO_APP_ID" \
  --release_notes="$RELEASE_NOTES" \
  --file=app-release.apk

This gives testers a clear list of what changed in each build, making their testing more targeted and effective.

Branch-Based Distribution

Different branches often mean different testing contexts. You might want different app IDs or release notes depending on the branch:

# GitHub Actions example
- name: Upload to TestApp.io
  run: |
    curl -Ls https://github.com/testappio/cli/releases/latest/download/ta-cli-Linux-x86_64 -o ta-cli
    chmod +x ta-cli

    if [ "${{ github.ref_name }}" = "main" ]; then
      APP_ID="${{ secrets.TESTAPPIO_APP_ID_PRODUCTION }}"
      NOTES="Release candidate from main - ${{ github.sha }}"
    else
      APP_ID="${{ secrets.TESTAPPIO_APP_ID_DEV }}"
      NOTES="Dev build from ${{ github.ref_name }} - ${{ github.sha }}"
    fi

    ./ta-cli publish \
      --api_token=${{ secrets.TESTAPPIO_API_TOKEN }} \
      --app_id="$APP_ID" \
      --release_notes="$NOTES" \
      --file=app/build/outputs/apk/release/app-release.apk

Connecting CI Uploads to Version Lifecycle

Automated uploads become even more powerful when connected to TestApp.io's version lifecycle. Builds uploaded via CI automatically appear in the version's Releases tab, giving you:

  • A chronological record of every build produced during a version's lifecycle.
  • Direct install links for testers — no digging through CI artifacts.
  • Linkage between builds, tasks, and blockers within the version context.
  • Quality metrics that aggregate across all builds in a version.

When a tester reports a blocker from a CI-uploaded build, it's automatically linked to that build. When a developer pushes a fix and CI uploads a new build, the tester can immediately install and verify. The entire loop �� build, distribute, test, report, fix, rebuild, verify — happens without manual intervention.

Security Best Practices

A few reminders for keeping your CI/CD distribution pipeline secure:

  • Never hardcode API tokens. Always use your CI platform's secret management. Every platform listed above supports encrypted environment variables or credential stores.
  • Use separate tokens for CI and human access. If a CI token is compromised, you can rotate it without affecting your team's personal access.
  • Restrict branch triggers. Don't automatically distribute builds from every feature branch. Limit automated distribution to branches that matter — typically main, develop, and release/*.
  • Review CI logs. Ensure your CI configuration doesn't echo API tokens in build logs. Most CI platforms mask secrets automatically, but verify this for your setup.

Troubleshooting Common Issues

IssueCauseSolution
Upload fails with auth errorInvalid or expired API tokenRegenerate token in TestApp.io settings and update CI secrets
"File not found" errorBuild artifact path is wrongCheck your build output directory — Gradle, Xcode, and Flutter all use different paths
Upload hangs or times outLarge file on slow connectionresumable upload chunked upload should handle this, but check your CI runner's network and any proxy settings
Wrong app or versionIncorrect app IDVerify the app ID in your TestApp.io app settings matches your CI secret
Release notes are emptyEnvironment variable not resolvedEnsure your CI syntax for variable interpolation is correct for the platform

Wrapping Up

Automating mobile app distribution with CI/CD is one of those investments that pays off immediately. The first time a build is automatically uploaded, distributed, and available for testing within minutes of a merge — with proper release notes, metadata, and version linkage — you'll wonder why you ever did it manually.

The setup is straightforward regardless of your CI platform. Download ta-cli, add your credentials as secrets, and add the publish command after your build step. resumable upload chunked uploads handle the reliability, metadata extraction handles the details, and version lifecycle integration handles the traceability.

Start with one pipeline, prove the value, and expand from there.

Get started with TestApp.io and connect your first CI/CD pipeline today. For platform-specific setup details, check the help center.