play managed publishing: console plan/verify play-publish-changes, ready-to-publish items in play status, desktop hint
This commit is contained in:
parent
29a9683ab7
commit
1bb43fd4a0
20 changed files with 184 additions and 9 deletions
|
|
@ -47,6 +47,12 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const { projectSignals } = await import('../lib/project-signals');
|
||||
ids.signals = projectSignals(detectProject(root));
|
||||
} catch { /* not scanned */ }
|
||||
if (form === 'play-publish-changes' && args.has('lookup') && cfg.play?.packageName) {
|
||||
try {
|
||||
const { readyFromApi } = await import('../play/publish-ready');
|
||||
ids.readyChanges = await readyFromApi(root, cfg);
|
||||
} catch { /* not read: the plan says so */ }
|
||||
}
|
||||
if (args.has('all') && form !== 'play-app-content') throw new Error('console plan: --all is for play-app-content (every App content card inline)');
|
||||
const plan = consolePlan(form, cfg, ids, { all: args.has('all') });
|
||||
if (json) return printJson(plan);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const text = inline ?? await readText(from!);
|
||||
if (!text.trim()) throw new Error('console verify: the page text is empty');
|
||||
r = verifyText(form, config, text);
|
||||
} else if (form === 'play-publish-changes') {
|
||||
const { readyFromApi } = await import('../play/publish-ready');
|
||||
const { verifyPublishChangesApi } = await import('../lib/console-verify');
|
||||
r = verifyPublishChangesApi(await readyFromApi(root, config));
|
||||
} else if (form === 'apple-app-record' || form === 'apple-age-rating') {
|
||||
const bundleId = config.apple?.bundleId;
|
||||
if (!bundleId) throw new Error('console verify: apple.bundleId is not set');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
29
cli/lib/play-publish.ts
Normal 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')})`];
|
||||
}
|
||||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const notSentProp = { type: 'boolean', default: false, description: 'Comm
|
|||
/** Every form `console plan` / `console verify` know (cli/lib/console-answers.ts PLAN_FORM_IDS; a test keeps them equal). */
|
||||
export const CONSOLE_PLAN_FORMS = ['apple-app-record', 'apple-age-rating', 'apple-app-privacy', 'play-app-record', 'play-content-rating', 'play-target-audience',
|
||||
'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'];
|
||||
'play-financial-features', 'play-health-apps', 'play-production-countries', 'play-production-release', 'play-games-services', 'play-publish-changes'];
|
||||
|
||||
export const TOOLS: ToolDef[] = [
|
||||
{ name: 'check', title: 'Offline config check', command: ['check'], write: false, network: false,
|
||||
|
|
|
|||
18
cli/play/publish-ready.ts
Normal file
18
cli/play/publish-ready.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// 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 { playPublicPage } from '../lib/play-public';
|
||||
import { playClient } from './client';
|
||||
|
||||
export async function readyFromApi(root: string, config: ShipConfig): Promise<ReadyChange[]> {
|
||||
const pkg = need(config.play?.packageName, 'play.packageName');
|
||||
const play = await playClient(root, pkg);
|
||||
let out: ReadyChange[] = [];
|
||||
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 });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import { playPublicPage } from '../lib/play-public';
|
|||
import { playReleases, playReleaseLabel } from '../lib/portfolio';
|
||||
import { playClient } from './client';
|
||||
import { listOneTimeProducts } from './products';
|
||||
import { readyLines, readyToPublish } from '../lib/play-publish';
|
||||
import { clearPending, pendingLine, publishingUrl, readPending } from './review-pending';
|
||||
|
||||
interface Track { track: string; releases?: { name?: string; versionCodes?: string[]; status: string; userFraction?: number }[] }
|
||||
|
|
@ -32,7 +33,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
// 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, publicPage, managedPublishing: config.play?.managedPublishing ?? null, reviewPending }); return; }
|
||||
if (ctx.args.has('json')) { printJson({ details, category, tracks, listings, products, publicPage, managedPublishing: config.play?.managedPublishing ?? null, reviewPending, readyToPublish: readyToPublish({ tracks, publicPage, managedPublishing: config.play?.managedPublishing ?? null }) }); 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(', ') || '-'}`);
|
||||
|
|
@ -42,6 +43,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
console.log(` public store page: ${publicPage.state === 'public' ? `published (200), live version ${publicPage.versionName ?? (publicPage.varies ? 'varies with device' : 'not shown')}` : publicPage.state === 'notPublic' ? 'not public yet (404)' : `couldn't check it (${publicPage.error ?? 'not read'})`}${publicPage.stale ? ` (last known, ${publicPage.checkedAt.slice(0, 16).replace('T', ' ')} UTC)` : ''}`);
|
||||
for (const r of playReleases({ tracks, publicPage, managedPublishing: config.play?.managedPublishing ?? null }).filter(x => x.track === 'production')) console.log(` production ${r.version ?? '?'} (${r.build ?? '?'}): ${playReleaseLabel(r)}`);
|
||||
for (const l of readyLines(readyToPublish({ tracks, publicPage, managedPublishing: config.play?.managedPublishing ?? null }))) console.log(l);
|
||||
if (pending) console.log(` review: ${pendingLine(pending)}`);
|
||||
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(' ')}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue