apple: release type (apple.releaseType, apple release --after-approval|--manual), device-family preflight, TestFlight invite needs an installable build
This commit is contained in:
parent
4763862178
commit
85cf494c0a
24 changed files with 899 additions and 18 deletions
|
|
@ -26,7 +26,7 @@ import { BUILD_WAIT_EXIT, type BuildSeen, expireCandidates, planExpire, validHin
|
|||
import { parseDuration } from '../lib/release';
|
||||
import {
|
||||
type ApiBuild, betaDescriptions, betaReadiness, betaReviewReq, buildsWithVersion, getsAllBuilds, type Group, groupFromApi, inviteReq, maskEmail,
|
||||
parseTesters, pickBuild, planBetaInfo, planBetaReviewDetail, planDistribute, planGroups, planTesters, planWhatToTest, unknownGroupMessage, whatToTestFromListing,
|
||||
installableBuild, noInstallableBuildHint, noInstallableBuilds409, parseTesters, pickBuild, planBetaInfo, planBetaReviewDetail, planDistribute, planGroups, planTesters, planWhatToTest, unknownGroupMessage, whatToTestFromListing,
|
||||
} from '../lib/testflight';
|
||||
import { type Asc, ascClient, type J } from './asc';
|
||||
import { describeReq, execute, type Req } from './jsonapi';
|
||||
|
|
@ -113,6 +113,17 @@ async function findBuild(c: Asc, appId: string, number: string, marketing?: stri
|
|||
return { ...b, detail };
|
||||
}
|
||||
|
||||
/** Whether the group has a build its testers can install now (past Beta App Review for an external group). An internal
|
||||
* all-builds group has every build of the app. */
|
||||
async function groupHasInstallableBuild(c: Asc, appId: string, g: Group): Promise<boolean> {
|
||||
const builds = getsAllBuilds(g) ? await c.all(`/v1/builds?filter[app]=${appId}&limit=200`) : await c.all(`/v1/betaGroups/${g.id}/builds?limit=200`);
|
||||
for (const b of builds) {
|
||||
const detail = (await c.maybe(`/v1/builds/${b.id}/buildBetaDetail`))?.data?.attributes ?? null;
|
||||
if (installableBuild(b, detail, g.internal)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function run(ctx: Ctx): Promise<void> {
|
||||
const { config, root, resolve } = ctx.loaded;
|
||||
const a = ctx.args;
|
||||
|
|
@ -193,9 +204,35 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
await execute(c.asc, reqs, { dry, tag: TAG });
|
||||
if (a.has('invite')) {
|
||||
const fresh = new Map((await c.all(`/v1/betaTesters?filter[apps]=${appId}&limit=200`)).map((t: J) => [String(t.attributes.email ?? '').toLowerCase(), t] as const));
|
||||
const pending = want.map(t => fresh.get(t.email)).filter((t): t is J => !!t && ['NOT_INVITED', 'INVITED'].includes(t.attributes.state));
|
||||
if (pending.length && await confirm(ctx, `(Re)send the TestFlight invitation email to ${pending.length} tester(s)?`))
|
||||
await execute(c.asc, pending.map(t => inviteReq(appId, t.id, maskEmail(String(t.attributes.email)))), { dry: false, tag: TAG });
|
||||
// an invitation needs a build the tester can install; without one Apple answers 409 NO_INSTALLABLE_BUILDS (seen live
|
||||
// on a new external group): add the tester, skip the invite, say when it can go out
|
||||
const installable = new Map<string, boolean>();
|
||||
const ready: { t: J; groups: string[] }[] = [], waiting = new Set<string>();
|
||||
let waitingTesters = 0;
|
||||
for (const w of want) {
|
||||
const t = fresh.get(w.email);
|
||||
if (!t || !['NOT_INVITED', 'INVITED'].includes(t.attributes.state)) continue;
|
||||
const own = gs.filter(g => w.groups.includes(g.name));
|
||||
for (const g of own) if (!installable.has(g.id)) installable.set(g.id, await groupHasInstallableBuild(c, appId, g));
|
||||
if (own.some(g => installable.get(g.id))) ready.push({ t, groups: own.map(g => g.name) });
|
||||
else { waitingTesters++; for (const g of own) waiting.add(g.name); }
|
||||
}
|
||||
if (waitingTesters) {
|
||||
const msg = `${waitingTesters} ${noInstallableBuildHint([...waiting])}`;
|
||||
warn(TAG, msg);
|
||||
|
||||
}
|
||||
if (ready.length && await confirm(ctx, `(Re)send the TestFlight invitation email to ${ready.length} tester(s)?`)) {
|
||||
for (const { t, groups } of ready) {
|
||||
try { await execute(c.asc, [inviteReq(appId, t.id, maskEmail(String(t.attributes.email)))], { dry: false, tag: TAG }); }
|
||||
catch (e) {
|
||||
if (!noInstallableBuilds409(e)) throw e;
|
||||
const msg = `1 ${noInstallableBuildHint(groups)}`;
|
||||
warn(TAG, msg);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
done(TAG, 'testers done');
|
||||
return;
|
||||
|
|
@ -293,6 +330,12 @@ 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 });
|
||||
}
|
||||
// testers added to an external group but not invited yet: the invitation needs a build past Beta App Review
|
||||
if (a.has('submit-review')) {
|
||||
const waiting: string[] = [];
|
||||
for (const g of external) if ((await c.all(`/v1/betaGroups/${g.id}/betaTesters?limit=200`)).some((t: J) => t.attributes.state === 'NOT_INVITED')) waiting.push(g.name);
|
||||
if (waiting.length) { log(TAG, noInstallableBuildHint(waiting)); }
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue