# Send iOS Builds to Testers From GitHub Actions

> Archive and export an ad hoc .ipa on a GitHub macOS runner, then send it to testers’ iPhones with one step. New testers never need a new build.

Last updated: 2026-11-05
Canonical: https://buildferry.app/github-actions-ios-distribution

With one extra step, every push gets a fresh build ready for your testers. The share link stays the same: testers open it and tap Install to get the new build.

## What you need

- A BuildFerry account with your Apple account connected, and a token from **Upload from CI** in the dashboard.
- An App Store Connect API key with Admin access, the same kind BuildFerry uses. [How to create one](https://buildferry.app/app-store-connect-api-key).
- A workflow that runs on a macOS runner, because building for iOS needs Xcode.

## 1. Add the secrets

In your repository, open **Settings**, **Secrets and variables**, **Actions**, and add:

| Secret | Value |
| --- | --- |
| `BUILDFERRY_TOKEN` | The token from Upload from CI |
| `ASC_KEY_P8` | The contents of your AuthKey_XXXXXXXXXX.p8 file |
| `ASC_KEY_ID` | The key's ID |
| `ASC_ISSUER_ID` | Your Issuer ID |

## 2. Add the workflow

Save this as `.github/workflows/testers.yml`, with your own scheme name. It uses Xcode's cloud signing, so you don't import certificates or profiles: `-allowProvisioningUpdates` with the API key lets Xcode fetch what it needs.

```
name: Send to testers
on:
  push:
    branches: [main]

jobs:
  ios:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - name: Archive and export
        env:
          ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
        run: |
          echo "$ASC_KEY_P8" > "$RUNNER_TEMP/AuthKey.p8"
          AUTH="-allowProvisioningUpdates -authenticationKeyPath $RUNNER_TEMP/AuthKey.p8 -authenticationKeyID $ASC_KEY_ID -authenticationKeyIssuerID $ASC_ISSUER_ID"
          xcodebuild archive -scheme MyApp -archivePath build/MyApp.xcarchive -destination 'generic/platform=iOS' $AUTH
          xcodebuild -exportArchive -archivePath build/MyApp.xcarchive -exportPath build -exportOptionsPlist ExportOptions.plist $AUTH

      - name: Send to testers
        id: buildferry
        uses: iamsrikanth-dev/buildferry-upload@v1
        with:
          token: ${{ secrets.BUILDFERRY_TOKEN }}

      - run: echo "Testers install from ${{ steps.buildferry.outputs.share-url }}"
```

`ExportOptions.plist` uses the `release-testing` method. [Here's the file](https://buildferry.app/export-ipa-from-xcode).

If you already sign in CI another way, for example with fastlane match or certificates stored as secrets, keep that and add only the **Send to testers** step.

## 3. Push

The run uploads the .ipa, waits a few seconds while BuildFerry checks it, and shows the share link in the job summary.

- **A new app** starts with its share link on. You still approve each iPhone before anything reaches Apple.
- **A new build** of an app you already have is signed for every approved tester. Their link updates by itself.
- **A build that can't be shared,** such as an App Store export, fails the step with the reason.
- **Re-running a job** uploads the same file, which changes nothing.

## Why the devices in your own profile don't matter

A plain ad hoc export only installs on the iPhones in its provisioning profile, so a new tester normally means registering their UDID and running the workflow again. BuildFerry registers each iPhone you approve and re-signs the build for it, so the workflow only runs when your code changes.

## The action's options

| Input | Default | |
| --- | --- | --- |
| `token` | (required) | Your BuildFerry token |
| `ipa` | the newest .ipa in the workspace | Path to the build |
| `wait` | `true` | Wait for the check, so the step fails when the build can't be shared |
| `timeout` | `600` | Seconds to wait for the check |

Outputs: `share-url`, `app-url`, `status`, `version`, `build` and `testers`. The action is open source at [iamsrikanth-dev/buildferry-upload](https://github.com/iamsrikanth-dev/buildferry-upload).
