apple testflight distribute: a build's groups read per group (GET /v1/betaGroups/{id}/relationships/builds) instead of GET /v1/betaGroups?filter[builds]=, which App Store Connect refuses with 400 although the spec declares it; internal all-builds groups reported as already available and never sent the build; a re-run adds nothing; configured groups missing on App Store Connect point at groups sync, unknown ones list what exists; APPLE_LIVE_REJECTED (refused by storebound api too), tests/asc-query-guard.test.ts checks every App Store Connect GET with a query in cli/ against the spec's declared parameters, and the fake App Store Connect answers 400 to undeclared or live-refused parameters (pitfall 42)

This commit is contained in:
CRTkafa 2026-10-13 06:03:16 +08:00
parent f741c15829
commit bba46b5c7f
13 changed files with 354 additions and 14 deletions

View file

@ -179,12 +179,37 @@ export function planTesters(groups: Group[], existing: Map<string, { id: string;
export const inviteReq = (appId: string, testerId: string, label: string): Req => ({ method: 'POST', path: '/v1/betaTesterInvitations', note: `invite ${label}`,
body: { data: { type: 'betaTesterInvitations', relationships: { app: one('apps', appId), betaTester: one('betaTesters', testerId) } } } });
/** POST /v1/betaGroups/{id}/relationships/builds for every group that does not have the build yet. */
/** An internal group with hasAccessToAllBuilds gets every build by itself; App Store Connect refuses adding one by hand. */
export const getsAllBuilds = (g: Group): boolean => g.internal && g.hasAccessToAllBuilds === true;
/**
* POST /v1/betaGroups/{id}/relationships/builds for every group that does not have the build yet. Internal groups with
* all builds are skipped (they have it already). `buildGroupIds`: the groups that list the build, read per group with
* GET /v1/betaGroups/{id}/relationships/builds (App Store Connect answers 400 to GET /v1/betaGroups?filter[builds]=,
* and the spec has no GET of a build's groups).
*/
export function planDistribute(buildId: string, groups: Group[], buildGroupIds: string[]): Req[] {
return groups.filter(g => !buildGroupIds.includes(g.id) && !(g.internal && g.hasAccessToAllBuilds)).map(g => ({ method: 'POST', path: `/v1/betaGroups/${g.id}/relationships/builds`,
return groups.filter(g => !buildGroupIds.includes(g.id) && !getsAllBuilds(g)).map(g => ({ method: 'POST', path: `/v1/betaGroups/${g.id}/relationships/builds`,
body: many('builds', [buildId]), note: `build -> ${g.internal ? 'internal' : 'external'} group "${g.name}"` }));
}
/** Why group names given to distribute / testers are not on App Store Connect: configured but not created yet (with the
* command that creates them), or unknown (with the groups that do exist). Pure. */
export function unknownGroupMessage(names: string[], existing: Group[], configured: TfGroupSpec[]): string {
const sync = '`storebound apple testflight groups sync`';
const quote = (ns: string[]) => ns.map(n => `"${n}"`).join(', ');
const inConfig = names.filter(n => configured.some(g => g.name === n));
const other = names.filter(n => !inConfig.includes(n));
const lines: string[] = [];
if (inConfig.length) lines.push(inConfig.length > 1
? `groups ${quote(inConfig)} are in the config but not on App Store Connect yet: create them with ${sync}`
: `group ${quote(inConfig)} is in the config but not on App Store Connect yet: create it with ${sync}`);
if (other.length) lines.push(`unknown group${other.length > 1 ? 's' : ''} ${quote(other)}: ${existing.length
? `App Store Connect has ${quote(existing.map(g => g.name))}`
: `App Store Connect has no beta groups yet (add them to apple.testflight.groups, then ${sync})`}`);
return lines.join('; ');
}
/**
* "What to Test" texts for TestFlight per App Store locale from the App Store listing file: a `### whatToTest` block
* when a locale section has one, else its `### whatsNew` block (what `storebound whats-new` drafts). Pure.