In fastlane, how do you distribute an iOS build already in TestFlight without uploading it again?
answer
- the upload and the distribution are separable
- recovery should not cost a new build number
- no binary needed on the machine
- an option that turns the upload off
basics
~20 sPass distribute_only: true to upload_to_testflight. The action then skips the upload entirely and only runs distribution for a build already in TestFlight, identified by app_identifier with app_version and build_number, so no iOS binary is needed.
solid answer
~40 s`upload_to_testflight` (`pilot`) has a `distribute_only` option. With it set, the action performs no transfer at all: it targets a build already in Apple's beta service and runs only the distribution half, so `ipa` is irrelevant and no archive has to exist on the machine. You name the build with `app_identifier` plus `app_version` and `build_number`; `changelog`, `groups`, `distribute_external` and `notify_external_testers` still apply. It is the right recovery when the upload succeeded and the run then died in the processing wait, and the right shape for a separate promote lane after an internal soak of the bookbinding order app. There is no Android counterpart — fastlane has no core Android beta action to split in two.
code
ruby · 8 linesupload_to_testflight(
api_key_path: "fastlane/asc_api_key.json",
app_identifier: "com.bindery.bookbindingorders",
distribute_only: true,
app_version: "3.4.0",
build_number: "218",
changelog: "Same build, now with binding-batch notes."
)go deeper
Recall that the iOS upload action can run in a mode that distributes an existing build instead of transferring one, so a failed run does not force a rebuild.
Explain which options identify the target build when no local binary is present, and which options are distribution concerns that still apply once the upload is skipped.
Show the operational value: a cheap recovery path off the build machine, and a deliberate split between an upload lane and a human-approved promote lane that carries its own changelog.
Own the release-control argument — separating transfer from distribution puts a human decision between a merge and a tester install without slowing the build pipeline down.
## The situation this option exists for An iOS beta lane does two separable things: it **transfers a binary** to Apple, and it **distributes** a build that Apple has finished processing. Bundling them into one action call is convenient right up to the moment the second half fails on its own — the upload succeeded, the processing wait timed out, the CI job was cancelled, or the run was killed when the runner was reclaimed. The build is sitting in Apple's beta service, perfectly good, and the lane reports failure. The naive recovery is to re-run the whole lane. That rebuilds the bookbinding order app, produces a **new build number** for the same version, uploads a second near-identical binary, and leaves the account cluttered with builds nobody can tell apart. It also burns the build time again, which on a macOS runner is the expensive part. ## `distribute_only` `upload_to_testflight` — the canonical action behind `pilot` — accepts **`distribute_only`**. With it set, the action skips the upload phase entirely and runs only distribution against a build that is already there. Three consequences follow directly: - **No binary is needed.** The `ipa` option is irrelevant; nothing on the local machine has to hold an archive, so the recovery lane can run on any runner rather than the one that built. - **No processing wait applies to a transfer that does not happen.** The build has already been processed, which is precisely why it is distributable. - **The run is cheap.** It is API traffic, not a build, so it fits comfortably in a small job triggered by hand. ## Naming the build Because there is no local file to infer identity from, the action has to be told which build to act on: 1. **`app_identifier`** — which app in the account, essential in a repository that builds more than one bundle. 2. **`app_version`** — the marketing version the build belongs to. 3. **`build_number`** — the specific build within that version. This is the value your CI already knows, because it set it. A lane that omits these is asking the action to guess, and *the newest build* is a poor guess in a repository where several branches upload. ## What still applies, and what does not Still applies, because these are distribution concerns: - `changelog` — the tester-facing note attached to the build. - `groups` and `distribute_external` — which existing audience receives it. - `notify_external_testers` — whether that audience is told. Does **not** apply, or does something else entirely: - `skip_waiting_for_build_processing` governs the wait after an upload; with no upload there is nothing to wait for. - `expire_previous_builds` expires older builds; it is not a way to skip an upload. - `reject_build_waiting_for_review` acts on a build sitting in review; it distributes nothing. ## Two lanes that use it well Beyond recovery, splitting upload from distribution is a design choice worth making deliberately for the bookbinding order app: - **An upload lane** runs on every release-candidate merge, transfers the binary and stops. It is fast, it needs the macOS runner, and its success means only *the binary reached Apple*. - **A promote lane** runs later, by hand or on approval, with `distribute_only: true` — after the release manager has decided this is the build the pilot binderies get. It needs no build machine at all. That separation also gives the changelog a sensible home: the note is written when the decision is made, not when the merge happened. ## There is no Android counterpart This is another place where the platforms genuinely diverge rather than merely differing in spelling. fastlane ships **no core Android beta-distribution action**, so there is no Android lane with an upload phase and a distribution phase to separate. The third-party **Firebase App Distribution plugin** performs its own upload whenever it runs, and the core Google Play route, `upload_to_play_store` (`supply`), is a store lane aimed at a track. Anyone who claims a `distribute_only` equivalent on Android has generalised an Apple-side option into a platform with no core beta action to carry it.
- The lane uploaded successfully and then died during the processing wait. Why not just re-run it?A full re-run rebuilds and uploads a new build number for the same version, cluttering the account with near-identical iOS builds and burning macOS build time again. `distribute_only: true` targets the build that already landed, so the recovery costs a few API calls instead of a whole build, and the version and build number stay the ones you already communicated.
- Is there an Android counterpart to distribute_only?No. fastlane has no core Android beta-distribution action, so there is no upload phase and distribution phase to separate. The third-party Firebase App Distribution plugin uploads its artifact each time it runs, and the core Play route, `upload_to_play_store` (`supply`), is a store lane aimed at a track rather than a two-phase beta action.
saying these in an interview costs you the question
- Re-running the whole lane and burning a new build number
- Thinking distribute_only still needs an ipa path
- Confusing expire_previous_builds with skipping the upload
- Assuming Android has a distribute_only equivalent
- Letting the action guess which build to distribute