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:
parent
f741c15829
commit
bba46b5c7f
13 changed files with 354 additions and 14 deletions
|
|
@ -25,8 +25,8 @@ import { textTable } from '../lib/table';
|
|||
import { BUILD_WAIT_EXIT, type BuildSeen, expireCandidates, planExpire, validHint, waitDelay, type WaitOutcome, waitOutcome } from '../lib/build-ops';
|
||||
import { parseDuration } from '../lib/release';
|
||||
import {
|
||||
type ApiBuild, betaDescriptions, betaReadiness, betaReviewReq, buildsWithVersion, type Group, groupFromApi, inviteReq, maskEmail,
|
||||
parseTesters, pickBuild, planBetaInfo, planBetaReviewDetail, planDistribute, planGroups, planTesters, planWhatToTest, whatToTestFromListing,
|
||||
type ApiBuild, betaDescriptions, betaReadiness, betaReviewReq, buildsWithVersion, getsAllBuilds, type Group, groupFromApi, inviteReq, maskEmail,
|
||||
parseTesters, pickBuild, planBetaInfo, planBetaReviewDetail, planDistribute, planGroups, planTesters, planWhatToTest, unknownGroupMessage, whatToTestFromListing,
|
||||
} from '../lib/testflight';
|
||||
import { type Asc, ascClient, type J } from './asc';
|
||||
import { describeReq, execute } from './jsonapi';
|
||||
|
|
@ -178,7 +178,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const resp = await c.all(`/v1/betaTesters?filter[apps]=${appId}&include=betaGroups&limit=200`);
|
||||
for (const t of resp) existing.set(String(t.attributes.email ?? '').toLowerCase(), { id: t.id, groupIds: (t.relationships?.betaGroups?.data ?? []).map((g: J) => g.id), state: t.attributes.state });
|
||||
const plan = planTesters(gs, existing, want, { remove: a.has('remove'), only });
|
||||
if (plan.unknownGroups.length) throw new Error(`unknown group(s): ${plan.unknownGroups.join(', ')} (storebound apple testflight groups sync creates the configured ones)`);
|
||||
if (plan.unknownGroups.length) throw new Error(unknownGroupMessage(plan.unknownGroups, gs, tf?.groups ?? []));
|
||||
const internal = gs.filter(g => g.internal && want.some(t => t.groups.includes(g.name)));
|
||||
if (internal.length) warn(TAG, `internal groups (${internal.map(g => g.name).join(', ')}) only take App Store Connect users of your team (Users and Access)`);
|
||||
log(TAG, `${want.length} testers in the CSV: ${plan.summary.join(', ')}`);
|
||||
|
|
@ -239,14 +239,25 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const names = a.list('group');
|
||||
if (!names.length) throw new Error('--group <name>[,<name>]: which beta groups get the build');
|
||||
const gs = await groups(c, appId);
|
||||
const target = names.map(n => gs.find(g => g.name === n) ?? (() => { throw new Error(`unknown group "${n}" (${gs.map(g => g.name).join(', ')})`); })());
|
||||
const missing = names.filter(n => !gs.some(g => g.name === n));
|
||||
if (missing.length) throw new Error(unknownGroupMessage(missing, gs, tf?.groups ?? []));
|
||||
const target = names.map(n => gs.find(g => g.name === n)!);
|
||||
const external = target.filter(g => !g.internal);
|
||||
if (a.has('internal-only') && external.length) throw new Error(`external group(s) ${external.map(g => g.name).join(', ')}: distribute to external testers from the terminal (asks first)`);
|
||||
const b = await findBuild(c, appId, number, a.get('version'));
|
||||
const problems = betaReadiness(b.attributes, b.detail);
|
||||
if (problems.length) throw new Error(`build ${number}: ${problems.join('; ')}`);
|
||||
log(TAG, `build ${number} (${b.marketing ?? '?'}): internal ${b.detail?.internalBuildState ?? '?'}, external ${b.detail?.externalBuildState ?? '?'}`);
|
||||
const inGroups = (await c.all(`/v1/betaGroups?filter[app]=${appId}&filter[builds]=${b.id}&limit=200`)).map((g: J) => g.id);
|
||||
// a build's groups, read per group: GET /v1/betaGroups?filter[builds]= is refused live (400) and the spec has no
|
||||
// GET /v1/builds/{id}/betaGroups; internal all-builds groups have every build and take none by hand
|
||||
const inGroups: string[] = [];
|
||||
for (const g of target) {
|
||||
if (getsAllBuilds(g)) { log(TAG, `build ${number}: already available to ${g.name} (internal, all builds)`); continue; }
|
||||
if ((await c.all(`/v1/betaGroups/${g.id}/relationships/builds?limit=200`)).some((x: J) => x.id === b.id)) {
|
||||
inGroups.push(g.id);
|
||||
log(TAG, `build ${number}: already in ${g.name} (${g.internal ? 'internal' : 'external'})`);
|
||||
}
|
||||
}
|
||||
const reqs = planDistribute(b.id, target, inGroups);
|
||||
const review = a.has('submit-review') && external.length ? (await c.maybe(`/v1/builds/${b.id}/betaAppReviewSubmission`))?.data ?? null : null;
|
||||
const wantsReview = a.has('submit-review') && external.length > 0 && !review && !['BETA_APPROVED', 'IN_BETA_REVIEW', 'WAITING_FOR_BETA_REVIEW', 'IN_BETA_TESTING'].includes(b.detail?.externalBuildState);
|
||||
|
|
@ -264,7 +275,8 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (await confirm(ctx, `Submit build ${number} to Beta App Review now? Apple reviews it for external testing (Test Information must be complete: storebound apple testflight info).`))
|
||||
await execute(c.asc, [betaReviewReq(b.id)], { dry: false, tag: TAG });
|
||||
}
|
||||
done(TAG, `build ${number}: ${reqs.length ? `added to ${reqs.length} group(s)` : 'already in every group'}`);
|
||||
const added = target.filter(g => reqs.some(r => r.path.includes(`/betaGroups/${g.id}/`))).map(g => g.name);
|
||||
done(TAG, `build ${number}: ${added.length ? `added to ${added.join(', ')}` : `already available to ${target.map(g => g.name).join(', ')}`}`);
|
||||
return;
|
||||
}
|
||||
if (sub === 'wait') {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue