Play 'Live' only when the public store page proves it: play status checks https://play.google.com/store/apps/details?id=<pkg> (no auth, 4 s timeout, cached 5 min; publicPage in the JSON), a completed production release with a 404 page is 'Sent to Google (in review or not yet published)', 200 is live for the first release or when the page shows the versionName, otherwise 'Released to production (Google may still be reviewing)', offline is unknown; new phases sent / released in the desktop Releases tab, Portfolio, release status | plan; versions matched across the stores without trailing .0 parts (App Store 1.0 and Play 1.0.0 are one row and one train, each side shows its own string); tests/play-public-version-match.test.ts, desktop boot check release-state cases (page 404 and 200) (pitfall 56)

This commit is contained in:
CRTkafa 2026-10-13 20:46:48 +08:00
parent aa72204a06
commit be9ff8b788
18 changed files with 436 additions and 30 deletions

View file

@ -4,6 +4,8 @@ import { playCategoryOf } from '../lib/config';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { printJson } from '../lib/log';
import { playPublicPage } from '../lib/play-public';
import { playReleases, playReleaseLabel } from '../lib/portfolio';
import { playClient } from './client';
import { listOneTimeProducts } from './products';
@ -16,11 +18,11 @@ export async function run(ctx: Ctx): Promise<void> {
const tracks = (await play.call<{ tracks?: Track[] }>('GET', `${play.API}/edits/${id}/tracks`)).tracks ?? [];
const listings = (await play.call<{ listings?: { language: string }[] }>('GET', `${play.API}/edits/${id}/listings`)).listings ?? [];
const details = await play.call<Record<string, string>>('GET', `${play.API}/edits/${id}/details`);
const products = await listOneTimeProducts(play);
const [products, publicPage] = await Promise.all([listOneTimeProducts(play), playPublicPage(play.packageName)]);
// the App category is not in the API (edits.details has no category field): a console step
const cat = playCategoryOf(config);
const category = { config: cat?.category ?? null, source: cat?.source ?? null, api: false, plan: 'storebound console plan play-store-settings' };
if (ctx.args.has('json')) { printJson({ details, category, tracks, listings, products }); return; }
if (ctx.args.has('json')) { printJson({ details, category, tracks, listings, products, publicPage }); return; }
console.log(`${play.packageName}\n details: ${details.defaultLanguage} · ${details.contactEmail ?? '-'} · ${details.contactWebsite ?? '-'}`);
console.log(` category: ${cat ? `${cat.category} (${cat.source})` : 'not in the config (play.category)'} · not readable by the API: Store settings is a console step (${category.plan})`);
console.log(` listings (${listings.length}): ${listings.map(l => l.language).join(', ') || '-'}`);
@ -28,6 +30,8 @@ export async function run(ctx: Ctx): Promise<void> {
const rs = (t.releases ?? []).map(r => `${r.name ?? '?'} [${r.versionCodes?.join(',') ?? '-'}] ${r.status}${r.userFraction ? ' ' + r.userFraction * 100 + '%' : ''}`);
console.log(` track ${t.track.padEnd(11)} ${rs.join(' | ') || '-'}`);
}
console.log(` public store page: ${publicPage.state === 'public' ? 'published (200)' : publicPage.state === 'notPublic' ? 'not public yet (404)' : `unknown (${publicPage.error ?? 'not read'})`}`);
for (const r of playReleases({ tracks, publicPage }).filter(x => x.track === 'production' && x.state === 'completed')) console.log(` production ${r.version ?? '?'} (${r.build ?? '?'}): ${playReleaseLabel(r)}`);
console.log(` products (${products.length}):`);
for (const p of products) console.log(` ${p.productId.padEnd(40)} ${(p.purchaseOptions ?? []).map(o => `${o.purchaseOptionId}:${o.state ?? '?'}`).join(' ')}`);
});