validate: macOS versions need Mac screenshots, Play reads survive an ended edit; macOS build numbers across platforms; Mac build prebuild + apple.macAppCheck; signing wording off the Mac; dry flows plan uploads
This commit is contained in:
parent
b1f1b206d3
commit
f5da7c9f7a
21 changed files with 519 additions and 57 deletions
|
|
@ -34,7 +34,7 @@ import { rendersCheck } from './shots/check';
|
|||
import { resolvedTablets } from './shots/report';
|
||||
import { runScan } from './lib/scan';
|
||||
import type { ScanFinding } from './lib/scan/rules';
|
||||
import { playClient } from './play/client';
|
||||
import { EDIT_GONE, playClient } from './play/client';
|
||||
import { listOneTimeProducts } from './play/products';
|
||||
import { creativeNote } from './shots/creative';
|
||||
import { creativeWaiting, localCreative } from './apple/creative-assets';
|
||||
|
|
@ -117,7 +117,7 @@ export async function collectApple(ctx: Ctx): Promise<AppleSnapshot> {
|
|||
const every = await allVersions(c, appId);
|
||||
const editableOf = (x: J) => APPLE_EDITABLE.has(stateOf(x));
|
||||
const withEditable = [...new Set(every.filter(editableOf).map(platformOf))];
|
||||
const platform: AscPlatform = parsePlatform(ctx.args.get('platform')) ?? (withEditable.length === 1 ? withEditable[0]! : withEditable.includes('IOS') || !every.length ? 'IOS' : platformOf(every[0]));
|
||||
const platform: AscPlatform = parsePlatform(ctx.args.get('platform')) ?? (withEditable.length === 1 ? withEditable[0]! : withEditable.includes('IOS') || !every.length || every.some(x => platformOf(x) === 'IOS') ? 'IOS' : platformOf(every[0]));
|
||||
const versions = await c.all(versionsPath(appId, platform, 10));
|
||||
const v: J = (await c.editableVersion(platform)) ?? versions[0] ?? null;
|
||||
const otherPlatformVersions = every.filter(x => platformOf(x) !== platform && editableOf(x)).map(x => ({ versionString: String(x.attributes.versionString), state: stateOf(x), platform: platformOf(x) }));
|
||||
|
|
@ -186,7 +186,7 @@ export async function collectApple(ctx: Ctx): Promise<AppleSnapshot> {
|
|||
deviceFamilies: platform === 'IOS' ? await deviceFamilyVerdicts(ctx, c, appId) : undefined,
|
||||
build, locales, reviewDetail, ageRating,
|
||||
expectedLocales: config.languages.map(l => l.apple).filter((x): x is string => !!x),
|
||||
requiredDisplayTypes: requiredDisplayTypes(config.apple),
|
||||
requiredDisplayTypes: requiredDisplayTypes(config.apple, platform),
|
||||
iphoneDuo: !!config.apple?.iphoneDuo, play: !!config.play,
|
||||
macVersion: every.some(x => platformOf(x) === 'MAC_OS'), mac: !!config.apple?.mac,
|
||||
priceSchedule: !!sched?.included?.some((x: J) => x.type === 'appPrices'),
|
||||
|
|
@ -202,23 +202,25 @@ export async function collectPlay(ctx: Ctx): Promise<PlaySnapshot> {
|
|||
const { config, root } = ctx.loaded;
|
||||
const play = await playClient(root, config.play!.packageName);
|
||||
const tablets = resolvedTablets(config, ctx.loaded.resolve(ctx.args.get('src') ?? config.renders ?? config.shots?.out ?? 'store/renders'));
|
||||
const e = await play.call<{ id: string }>('POST', `${play.API}/edits`, {});
|
||||
log('validate', `play: read-only edit ${e.id} (deleted afterwards)`);
|
||||
try {
|
||||
const details = await play.call<{ defaultLanguage?: string; contactWebsite?: string }>('GET', `${play.API}/edits/${e.id}/details`);
|
||||
const raw = (await play.call<{ listings?: { language: string; title?: string; shortDescription?: string; fullDescription?: string }[] }>('GET', `${play.API}/edits/${e.id}/listings`)).listings ?? [];
|
||||
// play.read: one edit under the machine lock, deleted only after every read inside has finished, and opened anew once
|
||||
// when Play invalidates it meanwhile ("This Edit has been deleted"). A bare POST /edits here was neither locked nor
|
||||
// retried, so another storebound process (or the Console) ending the edit failed the screenshot reads with a false blocker.
|
||||
return play.read(async editId => {
|
||||
log('validate', `play: read-only edit ${editId} (deleted afterwards)`);
|
||||
const details = await play.call<{ defaultLanguage?: string; contactWebsite?: string }>('GET', `${play.API}/edits/${editId}/details`);
|
||||
const raw = (await play.call<{ listings?: { language: string; title?: string; shortDescription?: string; fullDescription?: string }[] }>('GET', `${play.API}/edits/${editId}/listings`)).listings ?? [];
|
||||
const listings: PlaySnapshot['listings'] = [];
|
||||
for (const l of raw) {
|
||||
const images: Record<string, number> = {};
|
||||
for (const type of Object.keys(playRequiredImages(tablets)))
|
||||
images[type] = ((await play.call<{ images?: unknown[] }>('GET', `${play.API}/edits/${e.id}/listings/${l.language}/${type}`)).images ?? []).length;
|
||||
images[type] = ((await play.call<{ images?: unknown[] }>('GET', `${play.API}/edits/${editId}/listings/${l.language}/${type}`)).images ?? []).length;
|
||||
listings.push({ ...l, images });
|
||||
}
|
||||
const tracks = ((await play.call<{ tracks?: { track: string; releases?: { status: string; versionCodes?: string[]; userFraction?: number }[] }[] }>('GET', `${play.API}/edits/${e.id}/tracks`)).tracks ?? [])
|
||||
const tracks = ((await play.call<{ tracks?: { track: string; releases?: { status: string; versionCodes?: string[]; userFraction?: number }[] }[] }>('GET', `${play.API}/edits/${editId}/tracks`)).tracks ?? [])
|
||||
.map(t => ({ track: t.track, releases: (t.releases ?? []).map(r => ({ status: r.status, versionCodes: r.versionCodes ?? [], userFraction: r.userFraction })) }));
|
||||
const products = (await listOneTimeProducts(play)).map(p => ({ productId: p.productId, states: (p.purchaseOptions ?? []).map(o => o.state ?? '') }));
|
||||
// the production track's countries (read-only in the API: set in the console, `console plan play-production-countries`)
|
||||
const productionCountries = await play.call<{ countries?: unknown[]; restOfWorld?: boolean }>('GET', `${play.API}/edits/${e.id}/countryAvailability/production`)
|
||||
const productionCountries = await play.call<{ countries?: unknown[]; restOfWorld?: boolean }>('GET', `${play.API}/edits/${editId}/countryAvailability/production`)
|
||||
.then(r => (r.restOfWorld ? -1 : (r.countries ?? []).length)).catch(() => null);
|
||||
const website = details.contactWebsite ?? null;
|
||||
const appAdsLines = config.ads?.appAdsTxt ?? [];
|
||||
|
|
@ -245,9 +247,7 @@ export async function collectPlay(ctx: Ctx): Promise<PlaySnapshot> {
|
|||
expectedProducts: (config.products ?? []).filter(p => sellsOn(config, p, 'play')).map(p => p.id),
|
||||
privacyUrl: await checkUrl(config.app.privacyPolicyUrl), website, appAds, appAdsLines, adsLikely, category: playCategoryOf(config), productionCountries,
|
||||
};
|
||||
} finally {
|
||||
await play.call('DELETE', `${play.API}/edits/${e.id}`).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Scan findings as validate findings: blocker stays blocker, high / medium become warnings; low and owner-accepted
|
||||
|
|
@ -279,7 +279,13 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const access = await brokerFindings(ctx.loaded).catch(e => [{ store: 'play' as const, level: 'warn' as const, code: 'broker-unavailable', msg: (e as Error).message }]);
|
||||
findings.push(...access);
|
||||
try { findings.push(...playRules({ ...(await collectPlay(ctx)), renders: local?.play })); }
|
||||
catch (e) { findings.push({ store: 'play', level: 'blocker', code: 'api', msg: (e as Error).message.slice(0, 600) }, ...rendersFindings('play', local?.play)); }
|
||||
catch (e) {
|
||||
// a Play edit that vanished under the reads is a tool problem (the retry inside play.read failed too), never a store blocker
|
||||
const gone = EDIT_GONE.test((e as Error).message);
|
||||
findings.push(gone
|
||||
? { store: 'play', level: 'warn', code: 'edit-gone', msg: `Play validate could not finish its read-only edit (${(e as Error).message.slice(0, 300)}): a tool problem, not a store finding`, fix: 'storebound validate --play (run it again; close other storebound runs and Console saves first)' }
|
||||
: { store: 'play', level: 'blocker', code: 'api', msg: (e as Error).message.slice(0, 600) }, ...rendersFindings('play', local?.play));
|
||||
}
|
||||
} else if (a.has('play')) warn('validate', 'no play section in the config');
|
||||
// Game Center / Play Games Services: entitlement, APP_ID resource, store state vs the config
|
||||
if (config.games || config.apple) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue