Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9880363
asc: add App Store Connect API client
Interlap01 Sep 16, 2026
6bf8b32
auth: store App Store Connect API keys
Interlap01 Sep 16, 2026
364e38f
ios: upload IPAs to App Store Connect
Interlap01 Sep 16, 2026
146e287
ios: submit builds to TestFlight and App Review
Interlap01 Sep 16, 2026
ef25c19
docs: describe the TestFlight and App Store commands
Interlap01 Sep 16, 2026
7845248
asc: make waits injectable, back off status polls, drop unused helpers
Interlap01 Sep 16, 2026
23b9fd7
ios: print no JSON on a nil result and name the ASC_* variables
Interlap01 Sep 16, 2026
7aa74d6
docs: Release configuration prerequisite, drop roadmap item numbers
Interlap01 Sep 16, 2026
c44fa52
auth: say what auth apple saved
Interlap01 Sep 17, 2026
feb5efa
asc: beta group, tester, team user and build management endpoints
Interlap01 Sep 17, 2026
18e6d36
ios: create missing TestFlight groups on submit, skip automatic-distr…
Interlap01 Sep 17, 2026
854cafe
asc: builder asc command group for apps, builds, groups, testers and …
Interlap01 Sep 17, 2026
8cc9876
docs: describe the asc management commands and group auto-create
Interlap01 Sep 17, 2026
7a67982
asc: invite beta testers, lowercase the email filter, refuse ambiguou…
Interlap01 Sep 17, 2026
eb47d69
distribute: invite NOT_INVITED testers after a group add, share the g…
Interlap01 Sep 17, 2026
9da531e
asc: testers invite, confirmed deletes with previews, one app resolver
Interlap01 Sep 17, 2026
dac9150
docs: asc testers invite, NOT_INVITED testers, confirmed deletes and …
Interlap01 Sep 17, 2026
42948fb
upload: show fractional megabytes, or kilobytes, in the progress line
Interlap01 Sep 17, 2026
741a573
asc: explain a refused invitation when the group has no build
Interlap01 Sep 17, 2026
7092e0e
asc: stop claiming the Apple key went to the keychain
Interlap01 Sep 17, 2026
1f66ee8
asc: trim comments and review fixes
Interlap01 Sep 17, 2026
ef87e3e
distribute: name only the groups the build was really added to
Interlap01 Sep 17, 2026
5b83a4a
asc: trim comments and review fixes
Interlap01 Sep 17, 2026
9cd22f3
auth: drop the unrelated tail from the Apple key message
Interlap01 Sep 17, 2026
c544c2b
Merge branch 'feat/asc-client' into feat/asc-manage
Interlap01 Sep 17, 2026
e97f798
asc: page through the app lookup instead of trusting a two-item filter
Interlap01 Sep 17, 2026
b5d1479
Merge branch 'feat/asc-client' into feat/asc-manage
Interlap01 Sep 17, 2026
e54b088
Merge remote-tracking branch 'origin/main' into feat/asc-manage
Interlap01 Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ go install ./cmd/builder
./builder dev rn --skip-install --bundle-id <id> # Use already installed app
./builder auth apple # Save an App Store Connect API key
./builder ios upload --wait # Upload dist/*.ipa to App Store Connect, wait for processing
./builder ios submit --testflight --group <name> --notes <text> # TestFlight
./builder ios submit --testflight --group <name> --notes <text> # TestFlight (creates the group if missing)
./builder ios submit --app-store --release after-approval # App Review
./builder asc apps|builds|groups|testers|users # App Store Connect listings (--json)
./builder asc groups create <name> [--external] # also: groups delete, groups add-build
./builder asc testers add <email>... --group <name> # also: testers remove, users invite
./builder asc testers invite <email>... # send/resend the TestFlight email
./builder asc builds expire --build-number N --yes # groups delete needs --yes too
```

## Architecture
Expand Down Expand Up @@ -135,8 +140,9 @@ cmd/builder/ # CLI entrypoint (Cobra)
internal/
auth/ # GitHub OAuth device flow + keyring storage (also CI tokens, ASC API key)
github/ # GitHub REST API (workflow dispatch, artifacts)
asc/ # App Store Connect API client (JWT, JSON:API, builds, uploads, TestFlight, review)
distribute/ # Upload / TestFlight / App Store flows on top of asc
asc/ # App Store Connect API client (JWT, JSON:API, apps, builds, uploads, TestFlight,
# beta groups, beta testers, team users/invitations, review)
distribute/ # Upload / TestFlight / App Store / tester flows on top of asc
ipa/ # Info.plist reading from .ipa archives
build/ # Build coordination (snapshot + trigger + poll + download)
signing/ # CSR generation and .p12 assembly (signing without a Mac)
Expand Down Expand Up @@ -235,6 +241,36 @@ internal/
- **Submit Order**: TestFlight is compliance → notes → `betaAppReviewSubmissions` (only for a new
external group) → add groups. App Store reuses an open `reviewSubmission`, skips an item the
version is already in, and rewrites ASC 409/422 with a "complete the metadata" hint.
- **Group Auto-Create**: `SubmitTestFlight` creates any `--group` name the app lacks (internal, or
external with `External`/`--external`) and marks it `GroupRef.Created`; existing groups keep
their type. `asc groups add-build` reuses it, so it inherits the beta-review step too.
- **Automatic Distribution Groups**: an internal group with `hasAccessToAllBuilds: true` gets every
build by itself, so `POST builds/{id}/relationships/betaGroups` answers 422 and the add-build path
skips it (`GroupRef.AutoBuilds`, exit 0). `asc groups create` sets it unless `--no-auto-builds`.
- **Internal Testers**: internal groups take team members only, so `distribute.AddTester` routes by
group type — external creates the tester in the group (409 → find by email → add), internal joins
the member's record or `POST userInvitations` for a stranger, who must accept first.
- **ASC Filters Are Substrings**: Apple's `filter[email]`/`filter[username]` match substrings, so
`FindBetaTester`/`FindUser` compare the address exactly; `filter[email]` goes lowercased because
ASC stores addresses that way.
- **NOT_INVITED Testers**: a team member put into an internal group stays `NOT_INVITED` with no
email until `POST betaTesterInvitations`, so `AddTester` re-reads the state after a group add and
`asc testers invite` sends it on demand (ACCEPTED/INSTALLED are left alone).
- **No Installable Build**: while no group of a tester's has a build, `betaTesterInvitations`
answers 409 `asc.CodeNoInstallableBuilds`: `InviteTester` turns it into a `noBuildError` naming
`asc groups add-build`, and `AddTester` into a plain "added" rather than a failure.
- **Group Name Matching**: `asc.MatchBetaGroup` is the only name lookup (command layer and
`findOrCreateGroup`): case-insensitive, nil when absent, and an error listing the candidates when
several groups fold to the same name, so nothing is created, deleted or linked on a guess.
- **Destructive asc Commands**: `groups delete`, `testers remove` without `--group` and
`builds expire` resolve everything first, print a "Will ..." line naming exactly what goes, and
then need `--yes`; `testers remove` looks every address up before the first deletion.
- **asc Command Layer**: `cmd/builder/asc.go` is thin cobra over `asc` and `distribute`;
`resolveApp` (`--bundle-id` → `--ipa` → `ios.bundleId` → newest `dist/*.ipa`) and `runTestFlight`
are shared with `ios submit`, and builds list with `include=preReleaseVersion,betaGroups`.
- **asc Command Tests**: `getASCClient` is a package var so tests can point it at an httptest
server, and their `run` helper resets every flag first, since cobra keeps flag values on the
shared command tree.
- **Extension Points**: a future `ios release` composes `distribute.Upload` and
`distribute.SubmitTestFlight`, reading `asc.Client.ListBuilds` for the latest build number; the
`pkg/` wrappers do not expose `asc` yet.
Expand All @@ -247,10 +283,12 @@ internal/
"project": "MyApp",
"platform": "ios",
"github": { "owner": "username", "repo": "my-ios-app" },
"ios": { "path": "ios", "scheme": "" }
"ios": { "path": "ios", "scheme": "", "bundleId": "com.example.myapp" }
}
```

`ios.bundleId` is optional; the `asc` commands fall back to the newest IPA in `./dist/`.

## Workflow Features

The embedded workflow template (`internal/workflow/templates/ios-build.yml`):
Expand Down
76 changes: 70 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,25 @@ builder signing setup # Upload code signing secrets to GitHub
builder ios upload --wait # Upload ./dist/*.ipa to App Store Connect and wait for processing
builder ios submit --testflight --group "Beta Testers" --notes "What to test"
builder ios submit --app-store --release after-approval # Submit the version for App Review

# App Store Connect management (needs builder auth apple)
builder asc apps # Apps the API key can see
builder asc builds # Builds of the newest version, with their TestFlight groups
builder asc builds expire --build-number 42 --yes
builder asc groups # TestFlight groups with tester counts
builder asc groups create Nightly # Internal group (add --external for external)
builder asc groups add-build Nightly # Newest VALID build (or --build-number)
builder asc groups delete Nightly --yes
builder asc testers --group Nightly # With each tester's state
builder asc testers add a@example.com --group Nightly --first Ann --last Lee
builder asc testers invite a@example.com # Send or resend the TestFlight email
builder asc testers remove a@example.com --group Nightly
builder asc users # Team members and whether they can test internally
builder asc users invite dev@example.com --role DEVELOPER --first Dee --last Vee
```

Every `upload`/`submit` command takes `--json` for machine-readable output and
never prompts, so agents and CI jobs can drive them.
Every `upload`/`submit`/`asc` command takes `--json` for machine-readable output
and never prompts, so agents and CI jobs can drive them.

## Configuration

Expand Down Expand Up @@ -454,10 +469,12 @@ builder ios submit --testflight --group "Beta Testers" --notes "New login flow"
```

This takes the newest processed build (or `--build-number N`), sets the *What
to Test* notes and adds it to the named groups (`--group` repeats). Internal
groups get the build immediately; the first external group triggers Apple's
beta review, which Builder submits for you (`--wait` follows the decision). Run
it without `--group` to see the build and the groups the app has.
to Test* notes and adds it to the named groups (`--group` repeats). A group
that does not exist yet is created — internal by default, external with
`--external`. Internal groups get the build immediately; the first external
group triggers Apple's beta review, which Builder submits for you (`--wait`
follows the decision). Run it without `--group` to see the build and the
groups the app has.

### 4. Submit to the App Store

Expand All @@ -476,6 +493,53 @@ with [asc-cli](https://github.com/tddworks/asc-cli), whose production use of
the `buildUploads` API also proved that the Mac-free upload path works and
served as the reference for Builder's implementation.

## Managing TestFlight

`builder asc` covers the App Store Connect housekeeping around TestFlight
without the website: apps, builds, groups, testers and team members. Every
command takes `--json` (result on stdout, progress on stderr), never prompts,
and finds the app through `--bundle-id`, then `ios.bundleId` in
`builder.json`, then the newest IPA in `./dist/`.

```bash
builder asc builds # newest version's builds and their groups
builder asc groups create Nightly # internal group; --external for outsiders
builder asc groups add-build Nightly # same as ios submit --testflight --group
builder asc testers add a@example.com b@example.com --group Nightly
builder asc testers # every tester with their state
builder asc testers invite a@example.com # send or resend the TestFlight email
builder asc testers remove a@example.com --group Nightly
builder asc builds expire --build-number 42 --yes
```

Two things about internal groups:

- **They take team members only.** `asc testers add` puts a member's tester
record into the group and invites a stranger to the App Store Connect team
first (`--role`, default `CUSTOMER_SUPPORT`, only this app visible;
`--first` and `--last` required). They must accept that email before a build
reaches them, so rerun the command afterwards. `asc users` shows the team and
who already has TestFlight access; `asc users invite` invites on its own.
- **Automatic distribution.** An internal group with "automatic distribution"
(the default of `asc groups create`, off with `--no-auto-builds`) receives
every processed build by itself and Apple refuses to add builds by hand, so
`asc groups` marks it `internal, all builds` and `ios submit --group` and
`asc groups add-build` skip it with a note instead of failing.

`NOT_INVITED` means no email has gone out — how a team member added to an
internal group in App Store Connect shows up. `asc testers invite` sends it
(or resends while `INVITED`) and `asc testers add` does so by itself, unless
the group has no build yet: Apple refuses to invite anyone into a group with
nothing to install, so `add` reports "invite goes out once the group has a
build" and `invite` says to run `asc groups add-build` first (an external
group's build must also pass Beta App Review).

External groups take anyone by email, reusing a tester the team already has.
`asc groups delete`, and `asc testers remove` without `--group` (which drops
the tester from TestFlight team-wide), print what goes and then need `--yes`.
Group names match case-insensitively; when two differ only by case, the
command refuses and lists both.

## Installing the IPA

Use [MobAI](https://mobai.run) to install your IPA directly on your device. It works with both signed and unsigned builds: an unsigned IPA can be re-signed on install with a free Apple ID (MobAI asks for the account).
Expand Down
Loading
Loading