play-publish-changes: a release is ready only when the store page shows an older version; unknown reads are not determined, never a mismatch

This commit is contained in:
CRTkafa 2026-10-24 12:12:37 +08:00
parent 4e2ffd6b3d
commit 26f9559e1a
6 changed files with 40 additions and 15 deletions

View file

@ -436,11 +436,12 @@ export function verifyPublishChanges(text: string): VerifyResult {
}
/** The same check from the API's ready list (`readyToPublish`). */
export function verifyPublishChangesApi(ready: { track: string; version: string | null; build: string | null; rollout: string }[]): VerifyResult {
export function verifyPublishChangesApi(ready: { track: string; version: string | null; build: string | null; rollout: string }[] & { undetermined?: boolean }): VerifyResult {
const seen = ready.map(r => `${r.track} ${r.version ?? '?'} (${r.build ?? '?'}) ${r.rollout}`).join('; ');
const rows: VerifyRow[] = [{ item: 'Production releases ready to publish', expected: 'none left', seen: ready.length ? seen : 'none', status: ready.length ? 'mismatch' : 'match',
const rows: VerifyRow[] = [{ item: 'Production releases ready to publish', expected: 'none left', seen: ready.length ? seen : ready.undetermined ? 'none proven (the store page does not show an older live version; not determined)' : 'none', status: ready.length ? 'mismatch' : 'match',
...(ready.length ? { note: 'the owner presses "Publish N changes" and confirms (storebound console plan play-publish-changes)' } : {}) }];
return result('play-publish-changes', 'api', rows, ['The API sees production releases only; Data safety and other non-track changes are not readable.']);
return result('play-publish-changes', 'api', rows, ['The API sees production releases only; Data safety and other non-track changes are not readable.',
...(ready.undetermined ? ['A release counts as ready only when the public store page shows an OLDER version; a 404, failed read or same version is never ready. Check Publishing overview ("Last published on ...") or pass the page text with --from-text -.'] : [])]);
}
/** Page-text verification of any form that has one. */

View file

@ -13,9 +13,23 @@ export const PUBLISH_CONFIRM = 'Publish';
export interface ReadyChange { track: string; version: string | null; build: string | null; rollout: string; liveVersion: string | null }
/**
* Ready only when the public store page POSITIVELY shows an older version than the completed production release
* (both parsed and compared; playReleases sets liveVersion only then) and the page read is fresh. A 404, a failed or
* stale read, "Varies with device", a same or newer version, or an unparseable version is never "ready": a release
* already published (managed publishing, "Last published on ...") must not be reported as waiting.
*/
const isReady = (r: ReturnType<typeof playReleases>[number]): boolean => r.track === 'production' && r.managed === true && r.phase === 'review' && !!r.liveVersion && !r.pageStaleAt && !!r.version;
/** True when a completed managed production release exists but the store page cannot tell whether it is published. */
export function readyUndetermined(data: unknown): boolean {
const rs = playReleases(data);
return (data as { managedPublishing?: unknown })?.managedPublishing === true && rs.some(r => r.track === 'production' && r.state === 'completed' && !isReady(r) && r.phase !== 'live');
}
/** Ready-to-publish production releases from the `play status --json` data ({ tracks, publicPage, managedPublishing }). */
export function readyToPublish(data: unknown): ReadyChange[] {
return playReleases(data).filter(r => r.track === 'production' && r.managed === true && r.phase === 'review').map(r => ({
return playReleases(data).filter(isReady).map(r => ({
track: r.track ?? 'production', version: r.version, build: r.build,
rollout: r.fraction !== null && r.fraction < 1 ? `${Math.round(r.fraction * 1000) / 10}% staged` : 'full rollout', liveVersion: r.liveVersion ?? null,
}));

View file

@ -1,18 +1,20 @@
// Read-only: the production releases waiting in Publishing overview (managed publishing), from the tracks and the public store page.
import type { ShipConfig } from '../lib/config';
import { need } from '../lib/ctx';
import { type ReadyChange, readyToPublish } from '../lib/play-publish';
import { type ReadyChange, readyToPublish, readyUndetermined } from '../lib/play-publish';
import { playPublicPage } from '../lib/play-public';
import { playClient } from './client';
export async function readyFromApi(root: string, config: ShipConfig): Promise<ReadyChange[]> {
export async function readyFromApi(root: string, config: ShipConfig): Promise<ReadyChange[] & { undetermined?: boolean }> {
const pkg = need(config.play?.packageName, 'play.packageName');
const play = await playClient(root, pkg);
let out: ReadyChange[] = [];
let out: ReadyChange[] & { undetermined?: boolean } = [];
await play.read(async id => {
const tracks = (await play.call<{ tracks?: unknown[] }>('GET', `${play.API}/edits/${id}/tracks`)).tracks ?? [];
const publicPage = await playPublicPage(play.packageName);
out = readyToPublish({ tracks, publicPage, managedPublishing: config.play?.managedPublishing ?? null });
const data = { tracks, publicPage, managedPublishing: config.play?.managedPublishing ?? null };
out = readyToPublish(data);
out.undetermined = !out.length && readyUndetermined(data);
});
return out;
}