cross-store text: other platforms named in App Store text (2.3.10: Android, Google Play, ...) or Play text (Metadata: iOS, App Store, Game Center, ...) are refused by every text write (apple / play listing, play release / upload notes, listing set, whats-new, storebound edit, desktop editor), blockers in check / validate, scan other-platforms / play-other-platforms; listing.allowPlatformMentions for the owner's functional mentions; whats-new splits [apple] / [play] lines per store; storebound api --yes without a TTY is refused (exit 1, stderr) instead of a silent dry run; pitfalls 77, 78

This commit is contained in:
CRTkafa 2026-10-20 02:57:25 +07:00
parent 3e4015b686
commit edabbec874
32 changed files with 613 additions and 53 deletions

View file

@ -12,6 +12,7 @@
// storebound play listing pull [--write] [--json] live listings -> diff -> (with --write) the Play listing file
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { parsePlayListing } from '../lib/copy';
import { fieldsMentions, refuseMentions } from '../lib/cross-store';
import { compactDiff } from '../lib/diff';
import { mergePlayListing, overLimits } from '../lib/listing-pull';
import { counts, draftReason, draftSections, type ItemPlan, itemLines, planItem, type Skipped } from '../lib/listing-plan';
@ -21,6 +22,12 @@ import { done, log, printJson, step, warn } from '../lib/log';
import { playClient } from './client';
import { stdinIsTTY } from '../lib/tty';
/** Play release notes that name iOS, iPhone, App Store, ... are refused before any edit, dry runs included (pitfall 77). */
export function refusePlayNotes(ctx: Ctx, notes: { language: string; text: string }[], what: string): void {
const allow = ctx.loaded.config.listing?.allowPlatformMentions;
refuseMentions(notes.flatMap(n => fieldsMentions('play', n.language, { whatsNew: n.text }, allow)), what);
}
export function readPlayCopy(ctx: Ctx) {
const { config, resolve } = ctx.loaded;
const file = resolve(config.play?.listing ?? 'store/listing.md');
@ -125,6 +132,8 @@ export async function run(ctx: Ctx): Promise<void> {
skipped.push({ key: c.lang, reason: draftReason(st) });
return false;
});
// Play Metadata policy: iOS / App Store / Game Center in Play text stop the run, dry runs included (pitfall 77)
refuseMentions(copy.flatMap(c => fieldsMentions('play', c.lang, { title: c.title, short: c.short, full: c.full }, config.listing?.allowPlatformMentions)), `play listing${a.has('dry') ? ' (dry run)' : ''}`);
const details = { defaultLanguage: config.play?.defaultLanguage ?? 'en-US', contactEmail: config.app.contactEmail, contactWebsite: config.app.website };
if (!all.some(c => c.lang === details.defaultLanguage) && !a.has('only')) throw new Error(`listing has no section for the default language ${details.defaultLanguage}`);
if (!a.has('json')) log('play', `${copy.length} listings: ${copy.map(c => c.lang).join(', ')}${config.play?.video ? ` · promo video ${config.play.video}` : ''}`);

View file

@ -14,7 +14,7 @@ import { done, log, warn } from '../lib/log';
import { aabInfo } from '../lib/gradle';
import { playClient } from './client';
import { usedVersionCodes } from './next-version-code';
import { readPlayCopy } from './listing';
import { readPlayCopy, refusePlayNotes } from './listing';
const TRACKS = ['internal', 'alpha', 'beta', 'production'];
@ -45,9 +45,10 @@ export async function run(ctx: Ctx): Promise<void> {
for (const t of tracks) if (!STATUSES.includes(t.status)) throw new Error(`--${t.track}: status must be one of ${STATUSES.join(', ')}`);
const fraction = a.get('fraction');
const live = tracks.find(t => t.track === 'production' && t.status !== 'draft');
const notes = readPlayCopy(ctx).filter(c => c.whatsNew).map(c => ({ language: c.lang, text: c.whatsNew }));
refusePlayNotes(ctx, notes, `play release notes${a.has('dry') ? ' (dry run)' : ''}`);
if (live && !a.has('dry') && !(await confirm(ctx, `Release to PRODUCTION as "${live.status}" (public once Google approves)?`))) return;
const notes = readPlayCopy(ctx).filter(c => c.whatsNew).map(c => ({ language: c.lang, text: c.whatsNew }));
const play = await playClient(root, pkg);
try {
await play.edit(async (id, ctl) => {

View file

@ -13,7 +13,7 @@ import { aabInfo } from '../lib/gradle';
import { done, log, printJson } from '../lib/log';
import { nextVersionCode } from '../lib/release';
import { playClient } from './client';
import { readPlayCopy } from './listing';
import { readPlayCopy, refusePlayNotes } from './listing';
import { usedVersionCodes } from './next-version-code';
const TAG = 'upload';
@ -43,6 +43,7 @@ export async function run(ctx: Ctx): Promise<void> {
const dry = a.has('dry');
const json = a.has('json');
const notes = readPlayCopy(ctx).filter(c => c.whatsNew).map(c => ({ language: c.lang, text: c.whatsNew }));
refusePlayNotes(ctx, notes, `play upload release notes${dry ? ' (dry run)' : ''}`);
const play = await playClient(root, pkg);
const { used, current } = await play.read(async id => ({
used: await usedVersionCodes(play, id),