// What is on the stores now, per platform, without uploading anything (the read of a ship pipeline before a step). // storebound status [--app ] [--json] // Per app and platform: the version reaching users (live) and the one above it (next), each with its build, track, // store state, review state and rollout %. The same reads as `storebound release status` (apple status / play status // through cli/lib/app-status.ts). --app takes an app folder (path), a workspace app name, or a store id (bundle id, // package name, App Store app id); without it: this app folder, or every app of the workspace. Read-only; exit 1 // when a store could not be read. --json: one JSON line on stdout. import type { Args } from './lib/args'; import type { AppStatus, StoreRelease } from './lib/desktop-protocol'; import { log, printJson, warn } from './lib/log'; import { pool } from './lib/app-status'; import { storeNow } from './lib/train'; import { textTable } from './lib/table'; import { readLive, targets } from './release'; export interface ReleaseState { version: string | null; build: string | null; track: string | null; /** The store's own state word (READY_FOR_SALE, WAITING_FOR_REVIEW, completed, inProgress ...). */ state: string; phase: StoreRelease['phase']; /** The state word while the release is with the store's review (sent, in review, approved, rejected), else null. */ reviewState: string | null; /** Staged rollout (Play) / phased release (App Store) share in %, null without one. */ rolloutPct: number | null; } export interface PlatformStatus { platform: 'ios' | 'android'; store: 'apple' | 'play'; live: ReleaseState | null; next: ReleaseState | null; error?: string } const REVIEW = new Set(['sent', 'review', 'approved', 'rejected']); const stateOf = (r: StoreRelease | null): ReleaseState | null => r && { version: r.version, build: r.build, track: r.track ?? (r.store === 'ios' ? 'app-store' : null), state: r.state, phase: r.phase, reviewState: REVIEW.has(r.phase) ? r.state : null, rolloutPct: r.fraction === null || r.fraction === undefined ? null : Math.round(r.fraction * 1000) / 10, }; /** Per platform of an app: live + next release from its status read. Pure. */ export function platformStatus(s: AppStatus): PlatformStatus[] { const out: PlatformStatus[] = []; const all = s.release.all ?? []; for (const platform of ['ios', 'android'] as const) { const on = platform === 'ios' ? s.summary?.apple : s.summary?.play; const error = s.storeErrors[platform]; if (!on && !error) continue; const { now, next } = storeNow(all, platform); out.push({ platform, store: platform === 'ios' ? 'apple' : 'play', live: stateOf(now), next: stateOf(next), ...(error ? { error } : {}) }); } return out; } const cell = (r: ReleaseState | null): string => !r ? '-' : `${r.version ?? '?'}${r.build ? ` (${r.build})` : ''} ${r.state}${r.rolloutPct !== null && r.rolloutPct < 100 ? ` ${r.rolloutPct}%` : ''}${r.track && r.track !== 'app-store' && r.track !== 'production' ? ` [${r.track}]` : ''}`; export async function run(args: Args): Promise { const { list, concurrency } = await targets(args); const read = await pool(list, concurrency, async t => ({ t, s: await readLive(t) })); const apps = read.map(({ t, s }) => ({ app: t.name, folder: t.virtual ? null : t.dir, platforms: platformStatus(s), errors: [...t.problems, ...s.errors], warnings: s.warnings ?? [] })); if (args.has('json')) { const data = { apps, at: new Date().toISOString() }; if (process.env.STOREBOUND_MACHINE === '1') printJson(data); else process.stdout.write(JSON.stringify(data) + '\n'); } else { const rows: string[][] = []; for (const a of apps) { if (!a.platforms.length) rows.push([a.app, '', a.errors[0] ? `error: ${a.errors[0]}` : 'not on a store yet', '']); for (const p of a.platforms) rows.push([a.app, p.store === 'apple' ? 'App Store' : 'Google Play', p.error ? `unreadable: ${p.error}` : cell(p.live), p.error ? '' : cell(p.next)]); for (const w of [...a.warnings, ...a.errors.slice(a.platforms.length ? 0 : 1)]) warn('status', `${a.app}: ${w}`); } console.log(textTable(['app', 'store', 'live', 'next'], rows)); log('status', 'read-only; `storebound release status` shows both stores on one train'); } if (apps.some(a => a.errors.length || a.platforms.some(p => p.error))) process.exitCode = 1; }