play managed publishing: console plan/verify play-publish-changes, ready-to-publish items in play status, desktop hint

This commit is contained in:
CRTkafa 2026-10-24 11:56:07 +08:00
parent 29a9683ab7
commit 1bb43fd4a0
20 changed files with 184 additions and 9 deletions

View file

@ -29,7 +29,7 @@ export type FormId = typeof FORM_IDS[number];
* API cannot set), the App content overview and its declarations without a fill form (cli/lib/console-play-plans.ts). */
export const PLAN_ONLY_FORM_IDS = ['play-store-settings', 'play-app-content', 'play-privacy-policy', 'play-ads', 'play-sign-in-details',
'play-advertising-id', 'play-government-apps', 'play-financial-features', 'play-health-apps', 'play-production-countries', 'play-production-release',
'play-games-services'] as const;
'play-games-services', 'play-publish-changes'] as const;
export type PlanOnlyFormId = typeof PLAN_ONLY_FORM_IDS[number];
/** Every form `console plan` / `console verify` know. */
export const PLAN_FORM_IDS = [...FORM_IDS, ...PLAN_ONLY_FORM_IDS] as const;

View file

@ -108,6 +108,8 @@ export interface PlanIds {
demoAccount?: boolean;
/** What the project scan found (ad / attribution SDKs, the AD_ID permission, support and AR code); undefined: not scanned. */
signals?: ProjectSignals;
/** play-publish-changes: the production releases ready to publish per the API (null/undefined: not read). */
readyChanges?: { track: string; version: string | null; build: string | null; rollout: string }[] | null;
}
const ASC = 'https://appstoreconnect.apple.com';

View file

@ -20,6 +20,7 @@ import { NEVER_PRESS, PLAY_NEVER_PRESS, playAppList } from './console-forms';
import type { ConsolePlan, PlanAnswer, PlanIds, PlanStatus, PlanStep } from './console-plan';
import { AD_ID_PERMISSION, type ProjectSignals } from './project-signals';
import { playGamesPart } from './console-games-plan';
import { PUBLISH_BUTTON, PUBLISH_CONFIRM } from './play-publish';
const PLAY = 'https://play.google.com/console';
@ -503,10 +504,41 @@ function productionRelease(config: ShipConfig, p: PlanIds): Part {
};
}
function publishChanges(config: ShipConfig, p: PlanIds): Part {
const ready = p.readyChanges ?? null;
const readyText = ready === null ? 'not read (add --lookup, or run `storebound play status`)' : ready.length ? ready.map(r => `${r.track} ${r.version ?? '?'} (${r.build ?? '?'}) ${r.rollout}`).join('; ') : 'nothing ready to publish per the API';
return {
title: 'Play Console: publish the approved changes (managed publishing)',
start: start(config, p, 'publishing', ['Publishing overview (left menu)']),
steps: steps([
{ text: 'Precondition: `storebound play status` shows "production <version>: In review / ready to publish" (managed publishing on, Google approved).' },
{ text: 'Open Publishing overview. The section "Changes ready to publish" lists every approved change (e.g. the production release and Data safety); read the list as text and compare it with the ready changes below.' },
{ text: `Stop. The owner presses "${PUBLISH_BUTTON}" (N = the number in the list) and then confirms in its dialog (button "${PUBLISH_CONFIRM}"; a real mouse click, a JS .click() may do nothing). It makes every listed change live at once.`, answers: ['ready'] },
{ text: 'Verify: `storebound console verify play-publish-changes` (API: no production release is left ready to publish) or pipe the page text to `--from-text -`.' },
]),
answers: [ans('ready', 'Changes ready to publish (API)', readyText, 'certain', 'play status (public store page + tracks)', { note: 'The API sees production releases only; Data safety, listing and other non-track changes show in the console list only.', where: 'Publishing overview > Changes ready to publish' })],
final: PUBLISH_BUTTON,
neverPress: ['Publish N changes', 'Send changes for review', 'Submit N changes for review', 'Restart review', 'Discard changes', 'Stop managed publishing', 'Turn off managed publishing', 'Halt rollout', 'Unpublish app', 'Deactivate', ...NEVER_PRESS],
allowed: [
{ button: 'Publishing overview (left menu), expand Changes ready to publish', class: 'navigation', where: 'read-only: lists the approved changes' },
],
uiFacts: [
'Managed publishing (play.managedPublishing: true): after approval the changes wait in Publishing overview > "Changes ready to publish"; the button is "Publish N changes" (N counts every change, e.g. production full rollout + Data safety) and opens a confirm dialog.',
'The Developer API has no publishing-state endpoint: storebound infers "ready to publish" from a completed production release the public store page does not show yet.',
'Never "Discard changes" or "Stop managed publishing" (it would unpublish or drop the approved changes).',
managedPublishingNote(config),
PLAY_MATERIAL_FACT,
],
verify: { command: 'storebound console verify play-publish-changes', source: 'page-text', capture: PAGE_TEXT, expect: ['No production release is left ready to publish (API), or the page shows no "Publish N changes" button / "Changes ready to publish".'] },
guidance: ['Open: Publishing overview > Changes ready to publish'],
};
}
export function playOnlyPart(form: Exclude<PlanOnlyFormId, 'play-app-content'>, config: ShipConfig, p: PlanIds): Part {
switch (form) {
case 'play-production-countries': return productionCountries(config, p);
case 'play-production-release': return productionRelease(config, p);
case 'play-publish-changes': return publishChanges(config, p);
case 'play-store-settings': return storeSettings(config, p);
case 'play-privacy-policy': return privacyPolicy(config, p);
case 'play-ads': return adsDeclaration(config, p);

View file

@ -426,11 +426,29 @@ export function verifyProductionRelease(text: string): VerifyResult {
return result('play-production-release', 'page-text', rows);
}
/** Publishing overview as text (or the API's ready list): nothing left to publish. */
export function verifyPublishChanges(text: string): VerifyResult {
const f = fold(text);
const pending = /publish \d+ changes?|changes ready to publish/.test(f) && !/no changes ready to publish|0 changes ready/.test(f);
const rows: VerifyRow[] = [{ item: 'Changes ready to publish', expected: 'none left', seen: pending ? 'changes ready to publish' : 'none listed', status: pending ? 'mismatch' : 'match',
...(pending ? { note: 'the owner presses "Publish N changes" and confirms' } : {}) }];
return result('play-publish-changes', 'page-text', rows);
}
/** 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 {
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',
...(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.']);
}
/** Page-text verification of any form that has one. */
export function verifyText(form: PlanFormId, config: ShipConfig, text: string): VerifyResult {
if (form === 'play-app-content') return verifyAppContent(text);
if (form === 'play-production-countries') return verifyProductionCountries(config, text);
if (form === 'play-production-release') return verifyProductionRelease(text);
if (form === 'play-publish-changes') return verifyPublishChanges(text);
if (form === 'play-games-services') return result(form, 'page-text', verifyPlayGames(config, text));
if (form === 'play-store-settings' || form === 'play-privacy-policy' || form === 'play-ads' || form === 'play-sign-in-details' || form === 'play-advertising-id'
|| form === 'play-government-apps' || form === 'play-financial-features' || form === 'play-health-apps') return verifyPlayDeclaration(form, config, text);

29
cli/lib/play-publish.ts Normal file
View file

@ -0,0 +1,29 @@
// Play managed publishing: what waits in Publishing overview as "Changes ready to publish" (button "Publish N changes").
// The Developer API has no publishing-state endpoint; `play status` decides it the same way portfolio.playReleases does:
// a completed production release that the public store page does not show yet (404, or an older version) while
// play.managedPublishing is on is "In review / ready to publish". Pure. Non-track changes (Data safety, listing, content
// rating) are listed by the console page only; the API cannot see them.
import { playReleases } from './portfolio';
import { consoleUrl } from './console-links';
export const PUBLISH_PLAN_HINT = 'storebound console plan play-publish-changes';
/** The owner's final button: "Publish N changes" (N is the page's count) and its confirm dialog. */
export const PUBLISH_BUTTON = 'Publish N changes';
export const PUBLISH_CONFIRM = 'Publish';
export interface ReadyChange { track: string; version: string | null; build: string | null; rollout: string; liveVersion: string | null }
/** 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 => ({
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,
}));
}
/** Status lines for `play status` / `status`: the items and the hint (empty when nothing is ready). */
export function readyLines(items: ReadyChange[]): string[] {
if (!items.length) return [];
return [` ready to publish (managed publishing): ${items.map(i => `${i.track} ${i.version ?? '?'} (${i.build ?? '?'}) ${i.rollout}${i.liveVersion ? `, live: ${i.liveVersion}` : ''}`).join(' | ')}; non-track changes (Data safety, listing) show only in the console`,
` next: ${PUBLISH_PLAN_HINT} (${consoleUrl('play.publishing')})`];
}

View file

@ -239,7 +239,7 @@ function storeSteps(store: 'ios' | 'android', v: string, r: StoreRelease | null,
break;
case 'review':
if (store === 'android' && r!.track === 'production') {
steps.push({ kind: r!.managed ? 'owner' : 'wait', text: `${v}: ${playReleaseLabel(r!)}`, fix: r!.managed ? `${consoleLink('play.publishing')} (publish once approved)` : 'storebound play status', ...(r!.managed ? { url: consoleUrl('play.publishing') } : {}) });
steps.push({ kind: r!.managed ? 'owner' : 'wait', text: `${v}: ${playReleaseLabel(r!)}`, fix: r!.managed ? `${consoleLink('play.publishing')} (publish once approved; \`storebound console plan play-publish-changes\`)` : 'storebound play status', ...(r!.managed ? { url: consoleUrl('play.publishing') } : {}) });
break;
}
steps.push({ kind: 'wait', text: `${v} is in review (${r!.state}); nothing to do until the store answers`, fix: store === 'ios' ? 'storebound apple status --wait-review' : 'storebound play status' });