apple: macOS (tvOS, visionOS) versions are no longer invisible: every version and review submission lookup is per platform; status, listing, listing pull, resubmit, submit, release and screenshots take --platform and otherwise pick the platform that has the version they need (refuse naming both when several do); status, validate, release status, the report and the desktop label versions with their platform; MCP and owner actions get platform; pitfall 77
This commit is contained in:
parent
1736300784
commit
47dfd372fe
30 changed files with 423 additions and 127 deletions
|
|
@ -1,5 +1,6 @@
|
|||
// Read-only overview: app record, versions + attached build, recent builds, in-app purchases, review submissions.
|
||||
// storebound apple status [--json]
|
||||
// storebound apple status [--platform ios|macos|tvos|visionos] [--json]
|
||||
// Versions and review submissions of every platform, each labelled (iOS, macOS, ...); --platform shows one.
|
||||
// And the one write it offers: cancel a review submission (to re-add first IAPs to it from the web UI):
|
||||
// storebound apple status --cancel-submission <id> [--yes]
|
||||
// An empty draft (READY_FOR_REVIEW, 0 items) cannot be canceled (409 "Resource is not in cancellable state"); it is
|
||||
|
|
@ -23,6 +24,7 @@ import { type Asc, ascClient, type J } from './asc';
|
|||
import { notCancellable, readItems, type SubItem } from './submission';
|
||||
import { capabilityReport, type CapabilityReport, printCapabilityReport } from './signing';
|
||||
import * as rt from '../lib/rt';
|
||||
import { allVersions, parsePlatform, pickPlatform, platformLabel, platformOf, versionLabel, versionsPath } from './platform';
|
||||
|
||||
async function waitReview(ctx: Ctx, c: Asc, appId: string): Promise<never> {
|
||||
const want = ctx.args.get('version');
|
||||
|
|
@ -31,12 +33,14 @@ async function waitReview(ctx: Ctx, c: Asc, appId: string): Promise<never> {
|
|||
let delay: number | null = null;
|
||||
let last = '';
|
||||
for (;;) {
|
||||
const vs = await c.all(`/v1/apps/${appId}/appStoreVersions?filter[platform]=IOS&limit=10`);
|
||||
const flag = parsePlatform(ctx.args.get('platform'));
|
||||
const platform = flag ?? pickPlatform(await allVersions(c, appId), x => (!want || x.attributes.versionString === want) && classifyReviewState(state(x)) === 'pending', undefined, 'a version in review');
|
||||
const vs = await c.all(versionsPath(appId, platform, 10));
|
||||
const v = want ? vs.find(x => x.attributes.versionString === want) : vs.find(x => classifyReviewState(state(x)) === 'pending') ?? vs[0];
|
||||
if (!v) { log('apple', `no version ${want ?? ''} found`); process.exit(2); }
|
||||
const st = state(v);
|
||||
const outcome = classifyReviewState(st);
|
||||
if (st !== last) { log('apple', `${v.attributes.versionString}: ${st}`); last = st; }
|
||||
if (st !== last) { log('apple', `${versionLabel(v)}: ${st}`); last = st; }
|
||||
if (outcome === 'approved') { log('apple', 'approved (release it yourself, or it goes out automatically if the release type says so)'); process.exit(WAIT_EXIT.approved); }
|
||||
if (outcome === 'rejected') { log('apple', `rejected: read the message in ${consoleLink('asc.app-review')}, then the review-rejections skill`); process.exit(WAIT_EXIT.rejected); }
|
||||
if (outcome === 'not-submitted') { log('apple', 'nothing is waiting for review'); process.exit(2); }
|
||||
|
|
@ -70,15 +74,17 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
|
||||
const app = (await c.asc('GET', `/v1/apps/${appId}`)).data;
|
||||
const versions = await c.all(`/v1/apps/${appId}/appStoreVersions?limit=10&include=build`);
|
||||
// every platform's versions (an app's macOS version has its own state), or one with --platform
|
||||
const only = parsePlatform(ctx.args.get('platform'));
|
||||
const versions = await c.all(only ? `${versionsPath(appId, only, 20)}&include=build` : `/v1/apps/${appId}/appStoreVersions?limit=20&include=build`);
|
||||
const builds = (await c.asc('GET', `/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=5&include=preReleaseVersion`));
|
||||
const iaps = await c.all(`/v1/apps/${appId}/inAppPurchasesV2?limit=200`);
|
||||
const subs = await c.all(`/v1/reviewSubmissions?filter[app]=${appId}&limit=20`);
|
||||
const subs = await c.all(`/v1/reviewSubmissions?filter[app]=${appId}${only ? `&filter[platform]=${only}` : ''}&limit=20`);
|
||||
let caps: CapabilityReport | { error: string };
|
||||
try { caps = await capabilityReport(c, config, root); } catch (e) { caps = { error: (e as Error).message.slice(0, 300) }; }
|
||||
// a submission back from App Review: its items and their states, and how to resubmit it
|
||||
const unresolved: { id: string; items: SubItem[] }[] = [];
|
||||
for (const s of subs.filter((x: J) => x.attributes?.state === 'UNRESOLVED_ISSUES')) unresolved.push({ id: s.id, items: await readItems(c, s.id) });
|
||||
const unresolved: { id: string; platform: string; items: SubItem[] }[] = [];
|
||||
for (const s of subs.filter((x: J) => x.attributes?.state === 'UNRESOLVED_ISSUES')) unresolved.push({ id: s.id, platform: String(s.attributes?.platform ?? 'IOS'), items: await readItems(c, s.id) });
|
||||
// an empty draft: App Store Connect keeps it and refuses to cancel it; harmless
|
||||
const empty = new Set<string>();
|
||||
for (const s of subs.filter((x: J) => x.attributes?.state === 'READY_FOR_REVIEW')) if (!(await readItems(c, s.id)).length) empty.add(s.id);
|
||||
|
|
@ -89,7 +95,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
try { gaps = await firstProductReport(c, appId, { iaps, versionState: reviewed ? vstate(reviewed) : undefined }); } catch (e) { log('apple', `first in-app purchases not compared with the review: ${(e as Error).message.slice(0, 200)}`); }
|
||||
const itemLabel = (kind: string, id: string): string => {
|
||||
const v = versions.find((x: J) => x.id === id);
|
||||
return v ? `app version ${v.attributes.versionString}` : `${kind} ${id}`;
|
||||
return v ? `app version ${versionLabel(v)}` : `${kind} ${id}`;
|
||||
};
|
||||
if (ctx.args.has('json')) {
|
||||
// phased release of the newest versions that are live or close to it (rollout share on the desktop timeline)
|
||||
|
|
@ -115,16 +121,16 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
|
||||
console.log(`${app.attributes.name} · ${app.attributes.bundleId} · Apple ID ${appId}`);
|
||||
for (const v of versions) console.log(` version ${v.attributes.versionString.padEnd(8)} ${v.attributes.appVersionState ?? v.attributes.appStoreState}${v.relationships?.build?.data ? ' (build attached)' : ''}`);
|
||||
for (const v of versions) console.log(` version ${v.attributes.versionString.padEnd(8)} ${platformLabel(platformOf(v)).padEnd(8)} ${v.attributes.appVersionState ?? v.attributes.appStoreState}${v.relationships?.build?.data ? ' (build attached)' : ''}`);
|
||||
const pre = new Map((builds.included ?? []).map((p: { id: string; attributes: { version: string } }) => [p.id, p.attributes.version]));
|
||||
for (const b of builds.data) console.log(` build ${String(pre.get(b.relationships?.preReleaseVersion?.data?.id) ?? '?')} (${b.attributes.version}) ${b.attributes.processingState} ${b.attributes.uploadedDate?.slice(0, 10) ?? ''}`);
|
||||
for (const p of iaps) console.log(` iap ${p.attributes.productId.padEnd(40)} ${p.attributes.inAppPurchaseType} ${p.attributes.state}`);
|
||||
for (const s of subs) console.log(` review submission ${s.id} ${s.attributes.state}${empty.has(s.id) ? `: ${EMPTY_DRAFT}` : ''} ${s.attributes.submittedDate?.slice(0, 10) ?? ''}`.trimEnd());
|
||||
for (const s of subs) console.log(` review submission ${s.id} ${platformLabel(s.attributes.platform)} ${s.attributes.state}${empty.has(s.id) ? `: ${EMPTY_DRAFT}` : ''} ${s.attributes.submittedDate?.slice(0, 10) ?? ''}`.trimEnd());
|
||||
for (const u of unresolved) {
|
||||
console.log(` review submission ${u.id} is back from App Review with UNRESOLVED_ISSUES; its items:`);
|
||||
console.log(` review submission ${u.id} (${platformLabel(u.platform)}) is back from App Review with UNRESOLVED_ISSUES; its items:`);
|
||||
for (const it of u.items) console.log(` ${itemLabel(it.kind, it.versionId).padEnd(36)} ${it.state}${it.state === 'REJECTED' ? ' (not resolved: Apple takes no resubmit until it is; apple resubmit marks it resolved)' : ''}`);
|
||||
const newest = builds.data[0]?.attributes?.version;
|
||||
console.log(` resubmit with: storebound apple resubmit --build <n>${newest ? ` (newest uploaded build: ${newest})` : ''}`);
|
||||
console.log(` resubmit with: storebound apple resubmit --build <n>${u.platform === 'IOS' ? '' : ` --platform ${platformLabel(u.platform).toLowerCase()}`}${newest ? ` (newest uploaded build: ${newest})` : ''}`);
|
||||
}
|
||||
for (const g of gaps) warn('apple', `${g.msg}; ${g.fix}`);
|
||||
if ('error' in caps) log('apple', `capabilities not compared: ${caps.error}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue