storebound/cli/status.ts

72 lines
4.3 KiB
TypeScript

// What is on the stores now, per platform, without uploading anything (the read of a ship pipeline before a step).
// storebound status [--app <name|store id|folder>] [--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<void> {
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;
}