Console pages as absolute deep links: cli/lib/console-links.ts builds the URL of every App Store Connect / Play Console page from apple.appId, play.developerId and play.appId (app list / developer chooser when unknown); every plain 'App Store Connect > ...' / 'Play Console > ...' direction in cli/ and templates/ now carries it, validate / scan / declarations / release plan JSON carry url, the desktop shows an Open button; tests/console-links.test.ts guards it; AGENTS rule 11, pitfall 71

This commit is contained in:
CRTkafa 2026-10-15 19:55:37 +08:00
parent f898d642d0
commit e500e88e40
50 changed files with 381 additions and 92 deletions

View file

@ -3,6 +3,7 @@
// re-exports these) and `storebound release status | plan` (cli/release.ts) share this file, so both show the same
// stage for the same store state. Pure; the store reads are `apple status --json` / `play status --json`, turned into
// StoreRelease rows by cli/lib/portfolio.ts.
import { CONSOLE_PAGES, consoleLink, consoleUrl, playTrackPage } from './console-links';
import type { AppStatus, ReleaseTrain, StoreRelease } from './desktop-protocol';
import { cmpVersion, displayVersion, PHASE_RANK, playReleaseLabel, REACHING, releaseTrains, sameVersion, versionKey } from './portfolio';
import type { Finding } from './validate-rules';
@ -151,6 +152,8 @@ export type PlanKind = 'config' | 'store' | 'blocker' | 'owner' | 'wait' | 'warn
export interface PlanStep {
kind: PlanKind;
text: string;
/** Absolute deep link to the console page the message points to (desktop / MCP render it as an Open link). */
url?: string;
/** The command or console place that does it. Owner steps name what the owner runs; storebound never does them. */
fix?: string;
code?: string;
@ -217,18 +220,18 @@ function storeSteps(store: 'ios' | 'android', v: string, r: StoreRelease | null,
break;
case 'draft':
steps.push(store === 'ios'
? { kind: 'owner', text: `submit ${v} for App Review (owner)`, fix: 'App Store Connect > the version > Add for Review' }
? { kind: 'owner', text: `submit ${v} for App Review (owner)`, fix: consoleLink('asc.version', '> Add for Review'), url: consoleUrl('asc.version') }
: r!.track && r!.track !== 'production'
? { kind: 'owner', text: `roll out the ${r!.track} draft of ${v} to its testers (owner)`, fix: `Play Console > Test and release > ${r!.track}` }
: { kind: 'owner', text: `send the production draft of ${v} for review (owner)`, fix: 'Play Console > Publishing overview > Send for review' });
? { kind: 'owner', text: `roll out the ${r!.track} draft of ${v} to its testers (owner)`, fix: `${CONSOLE_PAGES[playTrackPage(r!.track)].label} (${r!.track}): ${consoleUrl(playTrackPage(r!.track))}`, url: consoleUrl(playTrackPage(r!.track)) }
: { kind: 'owner', text: `send the production draft of ${v} for review (owner)`, fix: consoleLink('play.publishing', '> Send for review'), url: consoleUrl('play.publishing') });
break;
case 'testing':
steps.push(store === 'ios'
? { kind: 'store', text: `${v} is in TestFlight (build ${r!.build ?? '?'}) but has no App Store version yet: create it with that build and its texts`, fix: `storebound apple listing --version ${v} --build ${r!.build ?? '<N>'} --dry` }
: { kind: 'owner', text: `promote ${v} from ${r!.track ?? 'testing'} to production (owner)`, fix: 'Play Console > Test and release > Production, or `storebound play release <aab> --production draft` in a terminal' });
: { kind: 'owner', text: `promote ${v} from ${r!.track ?? 'testing'} to production (owner)`, fix: `${consoleLink('play.production')}, or \`storebound play release <aab> --production draft\` in a terminal`, url: consoleUrl('play.production') });
break;
case 'rejected':
steps.push({ kind: 'owner', text: `resubmit ${v} after the fix (owner)`, fix: store === 'ios' ? 'App Store Connect > App Review > reply / resubmit' : 'Play Console > Publishing overview' });
steps.push({ kind: 'owner', text: `resubmit ${v} after the fix (owner)`, fix: store === 'ios' ? consoleLink('asc.app-review', '> reply / resubmit') : consoleLink('play.publishing'), url: consoleUrl(store === 'ios' ? 'asc.app-review' : 'play.publishing') });
break;
case 'sent':
case 'released':
@ -236,13 +239,13 @@ 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 ? 'Play Console > Publishing overview (publish once approved)' : 'storebound play status' });
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') } : {}) });
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' });
break;
case 'approved':
steps.push({ kind: 'owner', text: `${v} is approved (${r!.state}): release it (owner)`, fix: store === 'ios' ? 'App Store Connect > Release this version, or `storebound apple release --manual-release` in a terminal' : 'Play Console > Publishing overview' });
steps.push({ kind: 'owner', text: `${v} is approved (${r!.state}): release it (owner)`, fix: store === 'ios' ? `${consoleLink('asc.version', '> Release this version')}, or \`storebound apple release --manual-release\` in a terminal` : consoleLink('play.publishing'), url: consoleUrl(store === 'ios' ? 'asc.version' : 'play.publishing') });
break;
case 'rollout':
steps.push({ kind: 'owner', text: `${v} reaches ${pctText(r!.fraction) || 'part'} of users: raise or complete the rollout when health is good (owner)`,