> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowdeck.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# TestFlight

> Build, upload, and get a build to TestFlight groups and testers

Take a build from source to the people testing it: archive and sign, upload to
App Store Connect, then decide who receives it.

Everything here needs a connected account and a linked project. See
[App Store Connect Account](/cli/commands/ship/account) first.

Every subcommand supports `--json`.

## The whole flow

```bash theme={null}
flowdeck ship auth status --json                        # account connected?
flowdeck ship app status --json                         # repo linked?
flowdeck ship build next-version --platform ios --json  # what will it build against?
flowdeck ship doctor --platform ios                     # will signing work?
flowdeck ship build deliver --platform ios              # archive, sign, upload
flowdeck ship build status --platform ios               # did Apple accept it?
flowdeck ship testflight groups list                    # who can receive it?
flowdeck ship testflight distribute --platform ios --build 1337 --group "Beta Testers"
```

## Building and uploading

| Command                   | What it does                                              |
| ------------------------- | --------------------------------------------------------- |
| `build next-version`      | Which version a build would attach to. Read-only.         |
| `build next-build-number` | The next unused build number for that version. Read-only. |
| `build archive`           | Archive, sign, and export                                 |
| `build upload`            | Upload an existing archive                                |
| `build deliver`           | Archive and upload in one step                            |
| `build status`            | Recent builds and what Apple did with them                |

```bash theme={null}
flowdeck ship build deliver --platform ios
```

### Ask which version first

```bash theme={null}
flowdeck ship build next-version --platform ios --json
```

```json theme={null}
{"platform":"ios","versionString":"2.9.1","source":"appStoreConnect","needsCreate":false}
```

`needsCreate: true` means every version Apple holds is already live, so there
is nothing for a build to attach to. Creating a version record writes to your
real account, so it only happens when you ask:

```bash theme={null}
flowdeck ship build deliver --platform ios --create-version 2.9.2
```

### Check signing before you wait

```bash theme={null}
flowdeck ship doctor --platform ios
```

`doctor` reports signing blockers in a second: a missing or expired
provisioning profile, a profile that does not cover the bundle ID or belongs
to another team, a certificate whose private key is not on this Mac. The same
checks run automatically before a manual-signing archive, so a blocker stops
the build before `xcodebuild` starts rather than ten minutes in.

<Warning>
  `build archive`, `build upload` and `build deliver` are **not safe to retry**
  after an uncertain result. Run `flowdeck ship build status` first. A build
  number can never be reused.
</Warning>

## Groups

A group decides who receives which builds.

```bash theme={null}
flowdeck ship testflight groups list
flowdeck ship testflight groups create --name "Beta Testers"
flowdeck ship testflight groups create --name "Team" --internal
flowdeck ship testflight groups rename --group "Beta Testers" --name "Public Beta"
flowdeck ship testflight groups delete --group "Old Group" --confirm
```

`--group` accepts a **name or an ID**, everywhere it appears.

### Internal and external

|                       | Internal                            | External                 |
| --------------------- | ----------------------------------- | ------------------------ |
| Who can join          | App Store Connect team members only | Anyone                   |
| Beta App Review       | Skipped                             | Required before delivery |
| Time to first install | Immediate                           | A day or two             |

<Warning>
  Internal or external is decided when the group is created and **can never be
  changed**. Apple's `isInternalGroup` is create-only, so `--internal` is the one
  chance to get it right.
</Warning>

`groups create` is the one create command that is **not** safe to retry: Apple
allows two groups with the same name, so a second run makes a second group
rather than returning the first.

### Deleting

Deleting is refused while the group still holds testers:

```json theme={null}
{
  "ok": false,
  "code": "shipGroupNotEmpty",
  "message": "\"Public Testers\" still has 9 testers in it."
}
```

Empty it first. The testers stay on the app; they lose the builds that group
gave them.

## Testers

```bash theme={null}
flowdeck ship testflight testers list
flowdeck ship testflight testers add --email ada@example.com --first-name Ada --group "Beta Testers"
flowdeck ship testflight testers invite --email ada@example.com
flowdeck ship testflight testers remove --email ada@example.com --confirm
```

### Moving testers between groups

```bash theme={null}
flowdeck ship testflight groups add-testers --group "Beta Testers" \
  --email ada@example.com grace@example.com

flowdeck ship testflight groups remove-testers --group "Beta Testers" \
  --email ada@example.com
```

<Note>
  `testers remove` takes somebody off the **app**, revoking every build in every
  group they are in. `groups remove-testers` takes them out of **one group** and
  leaves the rest. They are different actions.
</Note>

### What `testers list` returns

It is a join, not a plain listing: who each tester is, which groups they are
in, and what they have done with the app.

```json theme={null}
{
  "id": "93de929d-3529-40cc-8cbf-18f3b799d9ae",
  "email": "ada@example.com",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "inviteType": "EMAIL",
  "state": "INSTALLED",
  "groups": ["Public Testers"],
  "sessions": 12,
  "crashes": 0,
  "feedback": 1,
  "devices": null
}
```

Two things to know about that shape:

* **A count Apple did not report is `null`, not `0`.** "No crashes" and "Apple
  did not say" are different answers. `devices` is always null, because Apple
  reports no device count on this endpoint.
* **A public-link tester has no `email`.** Apple never collects one. It also
  means they cannot be invited or added to a group by address.

### Internal groups refuse non-members

An internal group only accepts App Store Connect team members, and nothing can
check that in advance. Adding a batch therefore sends one request per address
and reports Apple's refusal per email:

```json theme={null}
{
  "ok": false,
  "code": "shipInternalGroupRejectedTester",
  "message": "App Store Connect would not add contractor@example.com to \"Team\"."
}
```

**Everybody else in the batch is still added.** Do not re-run the whole batch;
only the named addresses failed.

## Getting the build to testers

```bash theme={null}
flowdeck ship testflight distribute --platform ios --build 1337 --group "Beta Testers"
```

Only a build Apple has finished processing (`VALID`) can be distributed. Apple
accepts a request for an unprocessed build and then silently does nothing, so
FlowDeck refuses it rather than reporting a success that did not happen.

### Beta App Review

An external group receives nothing until review passes. Write the tester notes
first, because Apple gates the submission on them:

```bash theme={null}
flowdeck ship testflight notes --platform ios --build 1337 \
  --whats-new "New onboarding. Check the first-run flow."

flowdeck ship testflight submit-review --platform ios --build 1337 --notify
```

`--notify` sets Apple's "Automatically notify testers" so testers are emailed
when the build lands.

Internal groups never wait for review. If people need the build today and
review has not passed, an internal group is the answer.

### Expiring a build

```bash theme={null}
flowdeck ship testflight expire --build 1337
```

Expiring is not deleting. The build stays on the record and stops being
installable. An uploaded build can never be deleted, and its build number can
never be reused.

## Idempotency

| Safe to retry                                                                                                                                                   | Not safe after an uncertain result                                                                                                                                     |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| every `list`, every `status`, `build next-version`, `build next-build-number`, `testers add`, `testers invite`, `groups rename`, `groups add-testers`, `doctor` | `build archive`, `build upload`, `build deliver`, `distribute`, `expire`, `submit-review`, `groups create`, `groups delete`, `groups remove-testers`, `testers remove` |

Anything in the second column may already have taken effect when it failed or
timed out. Check with the matching `list` or `status` command before running
it again.

## Errors

Every failure carries a stable `code`, one sentence saying what happened, and
a hint saying what to do:

```json theme={null}
{
  "ok": false,
  "code": "shipBuildNotProcessed",
  "message": "Build 1337 is PROCESSING, not VALID, so it cannot be distributed yet.",
  "hint": "Wait for Apple to finish processing it, then run this again."
}
```

| Code                              | Meaning                                                          |
| --------------------------------- | ---------------------------------------------------------------- |
| `shipNotSignedIn`                 | No account connected. See [Account](/cli/commands/ship/account). |
| `shipAmbiguousAccount`            | Several accounts connected, none chosen for this project         |
| `shipNoLinkedApp`                 | This repository is not linked to an app record                   |
| `shipBuildNotProcessed`           | Apple has not finished processing the build                      |
| `shipGroupNotFound`               | No group by that name or ID                                      |
| `shipGroupNotEmpty`               | The group still holds testers                                    |
| `shipInternalGroupRejectedTester` | An address is not on the App Store Connect team                  |
