62 lines
3.8 KiB
TypeScript
62 lines
3.8 KiB
TypeScript
// Play changes committed with changesNotSentForReview=true (pitfall 72): Play takes the edit but does not send it for
|
|
// review, the owner presses "Send changes for review" in Publishing overview. The Play Developer API has no read-only
|
|
// way to see that changes wait to be sent (no publishing-state endpoint), so storebound records its own commit in
|
|
// .storebound/play-review-pending.json (gitignored) and `play status` shows it. A later commit that Play sent for
|
|
// review by itself (no refusal) clears it: Play then sends every pending change along. `play status --review-sent`
|
|
// forgets it after the owner pressed the button.
|
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { consoleUrl } from '../lib/console-links';
|
|
|
|
export interface ReviewPending { packageName: string; editId: string; committedAt: string; summary: string; forced: boolean }
|
|
|
|
/** Play's refusal of the automatic send for review (after a rejection, pending changes, managed publishing). */
|
|
export const REVIEW_REFUSED = /changesNotSentForReview|cannot be sent for review automatically/i;
|
|
/** Exactly that 400: the message names it, and a reason, when present, is a bad request / failed precondition. */
|
|
export function isReviewRefused(err: unknown): boolean {
|
|
const s = String(err instanceof Error ? err.message : err);
|
|
const m = /-> 400 (.*)$/s.exec(s);
|
|
if (!m || !REVIEW_REFUSED.test(m[1]!)) return false;
|
|
let reasons: string[] = [];
|
|
try {
|
|
const e = (JSON.parse(m[1]!) as { error?: { errors?: { reason?: string }[] } }).error;
|
|
reasons = (e?.errors ?? []).map(x => x.reason ?? '').filter(Boolean);
|
|
} catch { /* plain text body: the message alone decides */ }
|
|
return reasons.every(r => /badRequest|invalid|failedPrecondition|precondition/i.test(r));
|
|
}
|
|
|
|
/** The query string with changesNotSentForReview=true added to `base` ("?a=1" or ''). */
|
|
export function withNotSent(base: string | undefined, on: boolean): string {
|
|
const parts = [(base ?? '').replace(/^\?/, ''), on ? 'changesNotSentForReview=true' : ''].filter(Boolean);
|
|
return parts.length ? `?${parts.join('&')}` : '';
|
|
}
|
|
|
|
export const publishingUrl = (): string => consoleUrl('play.publishing');
|
|
export const notSentMessage = (): string => `committed; Play did not send it for review: press Send changes for review in Publishing overview: ${publishingUrl()}`;
|
|
|
|
const file = (root: string): string => join(root, '.storebound', 'play-review-pending.json');
|
|
type Store = Record<string, ReviewPending>;
|
|
const readAll = (root: string): Store => {
|
|
try { return existsSync(file(root)) ? JSON.parse(readFileSync(file(root), 'utf8')) as Store : {}; } catch { return {}; }
|
|
};
|
|
const writeAll = (root: string, s: Store): void => {
|
|
if (!Object.keys(s).length) { rmSync(file(root), { force: true }); return; }
|
|
mkdirSync(join(root, '.storebound'), { recursive: true });
|
|
writeFileSync(file(root), JSON.stringify(s, null, 2) + '\n');
|
|
};
|
|
|
|
export function recordPending(root: string, p: ReviewPending): void { writeAll(root, { ...readAll(root), [p.packageName]: p }); }
|
|
export function readPending(root: string, packageName: string): ReviewPending | null { return readAll(root)[packageName] ?? null; }
|
|
export function clearPending(root: string, packageName: string): boolean {
|
|
const s = readAll(root);
|
|
if (!s[packageName]) return false;
|
|
delete s[packageName];
|
|
writeAll(root, s);
|
|
return true;
|
|
}
|
|
|
|
/** The status line: "changes committed <time>, waiting for Send changes for review: <link>". */
|
|
export function pendingLine(p: ReviewPending): string {
|
|
return `changes committed ${p.committedAt.slice(0, 16).replace('T', ' ')} UTC (${p.summary}), waiting for Send changes for review: ${publishingUrl()}`
|
|
+ ' (recorded by storebound; the API cannot read whether it was pressed since: `storebound play status --review-sent` forgets it)';
|
|
}
|