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.
- 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.
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.
Questions
Can GitHub Actions distribute an iOS app to testers without TestFlight?
Yes. Export an ad hoc (Release Testing) .ipa on a macOS runner, then send it with the BuildFerry action. Testers install from a link in Safari, with no TestFlight app and no Beta App Review.
Do I need to rebuild when a new tester joins?
No. BuildFerry registers the new iPhone with your Apple team after you approve it and re-signs the latest build for it. The workflow only needs to run when your code changes.
Is the BuildFerry upload step free?
Yes. Upload from CI is on every plan, including Free. Each plan keeps its own limits on apps and build size.