Play edits under a machine-wide per-package lock (parallel reads of one app no longer delete each other's edits; a read whose edit was deleted is redone once, writes are not); a store that failed to read is 'Couldn't read Google Play' and keeps its last good releases with 'couldn't refresh'; stores on different versions: neutral 'Current · Play x · App Store y' header and each lane its own state, 'Not on this store' only without a record; Play status decided from the public store page's live version (Live / In review / In review (live: x) / managed publishing / Rolling out / Halted / Draft; only a network failure says 'Couldn't check the store page', with the last known answer and its time); tests/play-edit-lock.test.ts, recorded page fixture, boot check auto-read cases (pitfall 58)
This commit is contained in:
parent
e59f439101
commit
714352ec18
24 changed files with 570 additions and 103 deletions
|
|
@ -105,20 +105,22 @@ export function testflightReleases(builds: J[], buildVersions: Record<string, st
|
|||
export const versionOfName = (name: unknown): string | null => (typeof name === 'string' ? name.match(/\d+(?:\.\d+){0,3}/)?.[0] ?? null : null);
|
||||
|
||||
/**
|
||||
* Play tracks (`play status --json`) -> releases. The Developer API has no review state: a completed production release
|
||||
* is only "live" when the public store page proves it (`publicPage` in the status JSON): 404 = sent to Google (in
|
||||
* review or not yet published); 200 = live for the first release or when the page shows this versionName, otherwise
|
||||
* "released" (an update Google may still be reviewing); page not read = "released" with publicPage unknown.
|
||||
* Play tracks (`play status --json`) -> releases. The Developer API has no review state, so a completed production
|
||||
* release is decided by the public store page (`publicPage` in the status JSON): the page's version is this versionName
|
||||
* (or newer, or "Varies with device") = live; 404 = in review (with managed publishing: in review or ready to publish);
|
||||
* an older version on the page = in review, with the live one named. Only a network failure without any earlier page
|
||||
* read leaves it undecided ("released", publicPage unknown: "Couldn't check the store page"); with an earlier read the
|
||||
* phase comes from that read and `pageStaleAt` says when.
|
||||
*/
|
||||
export function playReleases(data: unknown): StoreRelease[] {
|
||||
const out: StoreRelease[] = [];
|
||||
const page = (data as J)?.publicPage as { state?: string; versionName?: string | null } | undefined;
|
||||
const page = (data as J)?.publicPage as { state?: string; versionName?: string | null; varies?: boolean; stale?: boolean; checkedAt?: string } | undefined;
|
||||
const managed = (data as J)?.managedPublishing === true;
|
||||
const pageState = page?.state === 'public' || page?.state === 'notPublic' ? page.state : 'unknown';
|
||||
const raw = (data as J)?.tracks;
|
||||
const tracks: J[] = Array.isArray(raw) ? raw : [];
|
||||
const codesOf = (r: J): number[] => (Array.isArray(r?.versionCodes) ? (r.versionCodes as unknown[]) : []).map(Number).filter(Number.isFinite);
|
||||
const rels = (t: J): J[] => (Array.isArray(t?.releases) ? t.releases : []);
|
||||
const allCodes = tracks.flatMap(t => rels(t).flatMap(codesOf));
|
||||
for (const t of tracks) {
|
||||
for (const r of rels(t)) {
|
||||
const codes = codesOf(r);
|
||||
|
|
@ -129,12 +131,17 @@ export function playReleases(data: unknown): StoreRelease[] {
|
|||
rel.phase = !production ? (status === 'draft' ? 'draft' : 'testing')
|
||||
: status === 'inProgress' ? 'rollout' : status === 'halted' ? 'halted' : status === 'draft' ? 'draft' : 'other';
|
||||
if (production && status === 'completed') {
|
||||
const low = codes.length ? Math.min(...codes) : Infinity;
|
||||
const first = !allCodes.some(c => c < low); // nothing older on any track: the app's first release
|
||||
rel.phase = pageState === 'notPublic' ? 'sent'
|
||||
: pageState === 'public' && (first || sameVersion(page?.versionName, version)) ? 'live'
|
||||
: 'released';
|
||||
const pv = page?.versionName ?? null;
|
||||
if (pageState === 'notPublic') { rel.phase = 'review'; if (managed) rel.managed = true; }
|
||||
else if (pageState === 'public') {
|
||||
// its version (or newer: this is an earlier one), or "Varies with device" (nothing to compare) = live; no
|
||||
// version found on a public page means the page changed shape: not checked, never guessed
|
||||
if (page?.varies || (pv && version && cmpVersion(pv, version) >= 0)) rel.phase = 'live';
|
||||
else if (!pv || !version) rel.phase = 'released';
|
||||
else { rel.phase = 'review'; rel.liveVersion = pv; if (managed) rel.managed = true; }
|
||||
} else rel.phase = 'released';
|
||||
rel.publicPage = pageState;
|
||||
if (page?.stale && pageState !== 'unknown') rel.pageStaleAt = page.checkedAt ?? null;
|
||||
}
|
||||
out.push(rel);
|
||||
}
|
||||
|
|
@ -142,12 +149,19 @@ export function playReleases(data: unknown): StoreRelease[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
/** Plain words for a Play production release's phase (CLI text; the desktop has the same words in i18n). */
|
||||
/** Plain words for a Play release's phase, decided from the store page (CLI text; the desktop has the same words in
|
||||
* i18n, apps/desktop/src/ui/kit.tsx phaseLabel). */
|
||||
export function playReleaseLabel(r: StoreRelease): string {
|
||||
if (r.phase === 'sent') return 'Sent to Google (in review or not yet published)';
|
||||
if (r.phase === 'released') return r.publicPage === 'unknown' ? 'Released to production (public store page not readable: live state unknown)' : 'Released to production (Google may still be reviewing)';
|
||||
if (r.phase === 'live') return 'Live';
|
||||
return r.state;
|
||||
const base = ((): string => {
|
||||
if (r.phase === 'review' || r.phase === 'sent') return r.managed ? 'In review / ready to publish (managed publishing)' : r.liveVersion ? `In review (live: ${r.liveVersion})` : 'In review';
|
||||
if (r.phase === 'released') return "Couldn't check the store page";
|
||||
if (r.phase === 'live') return 'Live';
|
||||
if (r.phase === 'rollout') return `Rolling out ${Math.round((r.fraction ?? 0) * 1000) / 10}%`;
|
||||
if (r.phase === 'halted') return 'Halted';
|
||||
if (r.phase === 'draft') return 'Draft';
|
||||
return r.state;
|
||||
})();
|
||||
return r.pageStaleAt ? `${base} (couldn't check the store page now; as of ${r.pageStaleAt.slice(0, 16).replace('T', ' ')} UTC)` : base;
|
||||
}
|
||||
|
||||
export const PHASE_RANK: Record<ReleasePhase, number> = { live: 0, rollout: 1, halted: 2, released: 3, approved: 4, review: 5, sent: 6, rejected: 7, testing: 8, draft: 9, other: 10 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue