whats-new drafts from player-facing notes first (whatsNew.source, docs/RELEASE-<version>.md, the CHANGELOG section of the version or Unreleased; --release, --from-git) and only then from the git log; internal jargon warnings (SDK / vendor names, developer and version-control words, file paths, issue ids, code identifiers) with plain alternatives in the draft and whats-new check, one list in cli/lib/jargon.ts, whatsNew.allowWords keeps a word; skill: write from what players feel, never from commit messages; tests
This commit is contained in:
parent
e9507d8884
commit
bab3f63227
9 changed files with 287 additions and 21 deletions
|
|
@ -1,7 +1,9 @@
|
|||
// Draft user-facing release notes from the git log and write them into the listing files (text only; nothing is
|
||||
// sent to a store). A human (or an agent with the whats-new skill) rewrites and translates before `play release` /
|
||||
// `apple listing` push them.
|
||||
// storebound whats-new [--since v1.1.0] [--lang en-US] [--all] [--hook "text"] [--dry]
|
||||
// storebound whats-new [--since v1.1.0] [--release 1.2.0] [--from-git] [--lang en-US] [--all] [--hook "text"] [--dry]
|
||||
// The text comes from a player-facing source when there is one (whatsNew.source, docs/RELEASE-<version>.md, the
|
||||
// CHANGELOG.md section of the version or Unreleased; version: --release, else package.json); only then the git log.
|
||||
// --since: default the latest tag (git describe --tags --abbrev=0); --lang: the Play code / App Store locale to write
|
||||
// (default: the Play default language and the first App Store locale); --all: write the same draft into every
|
||||
// language (to translate afterwards; `storebound check` warns while they are identical).
|
||||
|
|
@ -23,7 +25,7 @@ import type { Ctx } from './lib/ctx';
|
|||
import { done, log, printJson, warn } from './lib/log';
|
||||
import { textTable } from './lib/table';
|
||||
import {
|
||||
applyLanguageNotes, checkLanguageNotes, checkNotes, formatNotes, type LangCheck, NOTES_LIMITS, type NotesFile, noteItems, notesFileText, parseNotesFile,
|
||||
applyLanguageNotes, checkLanguageNotes, checkNotes, cutNotes, findNotesSource, formatNotes, type LangCheck, NOTES_LIMITS, type NoteItem, type NotesFile, noteItems, notesFileText, parseNotesFile,
|
||||
resolveLanguages, setAppleWhatsNew, setPlayWhatsNew,
|
||||
} from './lib/whats-new';
|
||||
import * as rt from './lib/rt';
|
||||
|
|
@ -38,14 +40,16 @@ function readDir(dir: string, langs: Language[]): { l: Language; f: NotesFile }[
|
|||
return langs.filter(l => names.has(l.id)).map(l => ({ l, f: parseNotesFile(readFileSync(join(dir, `${l.id}.md`), 'utf8'), l.id) }));
|
||||
}
|
||||
|
||||
function checkAll(files: { l: Language; f: NotesFile }[]): LangCheck[] {
|
||||
function checkAll(files: { l: Language; f: NotesFile }[], allow: readonly string[]): LangCheck[] {
|
||||
const src = files.find(x => x.f.status === 'source');
|
||||
return files.map(x => checkLanguageNotes(x.l, x.f, src && src !== x ? src.f.text : undefined));
|
||||
return files.map(x => checkLanguageNotes(x.l, x.f, src && src !== x ? src.f.text : undefined, allow));
|
||||
}
|
||||
|
||||
function printChecks(checks: LangCheck[]): void {
|
||||
console.log(textTable(['language', 'chars', 'play (500)', 'app store (4000)', 'problems'], checks.map(c => [c.lang, c.chars,
|
||||
c.play ? (c.play.ok ? 'ok' : 'OVER') : '-', c.apple ? (c.apple.ok ? 'ok' : 'OVER') : '-', c.problems.join('; ')])));
|
||||
// jargon never blocks: players read these notes, so name each word and a plain alternative (whatsNew.allowWords keeps one)
|
||||
for (const c of checks) for (const w of c.warnings) warn(TAG, `${c.lang}: ${w}`);
|
||||
}
|
||||
|
||||
async function languagesFlow(ctx: Ctx, draft: { play: string; apple: string }): Promise<void> {
|
||||
|
|
@ -70,7 +74,7 @@ async function languagesFlow(ctx: Ctx, draft: { play: string; apple: string }):
|
|||
files.push({ l, f: { lang: l.id, source: src!.id, status: 'translated', text: out }, write: true, note: 'translated by the hook' });
|
||||
} else files.push({ l, f: { lang: l.id, source: src!.id, status: 'translate', text }, write: true, note: hook ? 'would run the --translate hook' : 'to translate (your agent: whats-new skill)' });
|
||||
}
|
||||
const checks = checkAll(files.map(x => ({ l: x.l, f: x.f })));
|
||||
const checks = checkAll(files.map(x => ({ l: x.l, f: x.f })), config.whatsNew?.allowWords ?? []);
|
||||
if (a.has('json')) printJson({ dir, dryRun: a.has('dry'), files: files.map((x, i) => ({ lang: x.l.id, file: join(dir, `${x.l.id}.md`), status: x.f.status, write: x.write, note: x.note, check: checks[i] })) });
|
||||
else { for (const x of files) log(TAG, `${join(dir, `${x.l.id}.md`)}: ${x.note}`); printChecks(checks); }
|
||||
if (a.has('dry')) { done(TAG, 'dry run: nothing written'); return; }
|
||||
|
|
@ -92,7 +96,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const dir = resolve(a.get('dir') ?? 'store/whats-new');
|
||||
const files = readDir(dir, config.languages);
|
||||
if (!files.length) throw new Error(`${dir}: no <language id>.md files`);
|
||||
const checks = checkAll(files);
|
||||
const checks = checkAll(files, config.whatsNew?.allowWords ?? []);
|
||||
const bad = checks.filter(c => c.problems.some(p => !p.startsWith('first line')));
|
||||
if (a._[0] === 'check' || bad.length) {
|
||||
if (a.has('json')) printJson({ dir, checks, ok: !bad.length });
|
||||
|
|
@ -115,15 +119,27 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
}
|
||||
const since = a.get('since') ?? (() => { try { return git(['describe', '--tags', '--abbrev=0'], root).trim(); } catch { return ''; } })();
|
||||
const range = since ? `${since}..HEAD` : 'HEAD';
|
||||
const subjects = git(['log', range, '--no-merges', '--format=%s'], root).split('\n').filter(Boolean);
|
||||
const items = noteItems(subjects);
|
||||
log('whats-new', `${subjects.length} commits since ${since || 'the first commit'} -> ${items.length} user-facing items`);
|
||||
const allow = config.whatsNew?.allowWords ?? [];
|
||||
// a player-facing source (whatsNew.source, docs/RELEASE-<version>.md, the CHANGELOG section) beats the git log
|
||||
const version = a.get('release') ?? (() => { try { return (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version?: string }).version; } catch { return undefined; } })();
|
||||
const source = a.has('from-git') ? undefined : findNotesSource({ version, source: config.whatsNew?.source, read: rel => { const p = resolve(rel); return existsSync(p) ? readFileSync(p, 'utf8') : undefined; } });
|
||||
const hook = a.get('hook');
|
||||
const play = formatNotes(items, { hook, limit: NOTES_LIMITS.play });
|
||||
const apple = formatNotes(items, { hook, limit: NOTES_LIMITS.apple });
|
||||
for (const [store, text] of [['play', play], ['apple', apple]] as const) for (const p of checkNotes(text, store)) warn('whats-new', `${store}: ${p}`);
|
||||
let play: string, apple: string, subjects: string[] = [], items: NoteItem[] = [];
|
||||
if (source) {
|
||||
log(TAG, `notes from ${source.path} (${source.kind === 'changelog' ? 'CHANGELOG section' : source.kind === 'release-doc' ? 'release doc' : 'whatsNew.source'}), not the git log`);
|
||||
const body = hook ? `${hook}\n${source.text}` : source.text;
|
||||
play = cutNotes(body, NOTES_LIMITS.play);
|
||||
apple = cutNotes(body, NOTES_LIMITS.apple);
|
||||
} else {
|
||||
subjects = git(['log', range, '--no-merges', '--format=%s'], root).split('\n').filter(Boolean);
|
||||
items = noteItems(subjects);
|
||||
log(TAG, `${subjects.length} commits since ${since || 'the first commit'} -> ${items.length} items, drafted from commit messages (no docs/RELEASE-<version>.md, CHANGELOG section or whatsNew.source): rewrite them in player words`);
|
||||
play = formatNotes(items, { hook, limit: NOTES_LIMITS.play });
|
||||
apple = formatNotes(items, { hook, limit: NOTES_LIMITS.apple });
|
||||
}
|
||||
for (const [store, text] of [['play', play], ['apple', apple]] as const) for (const p of checkNotes(text, store, allow)) warn(TAG, `${store}: ${p}`);
|
||||
if (a.has('languages')) return languagesFlow(ctx, { play, apple });
|
||||
if (a.has('json')) printJson({ since: since || null, commits: subjects.length, items, play, apple, limits: NOTES_LIMITS });
|
||||
if (a.has('json')) printJson({ since: since || null, source: source ? { kind: source.kind, path: source.path } : null, commits: subjects.length, items, play, apple, limits: NOTES_LIMITS });
|
||||
else console.log(`--- draft (Play, ${[...play].length}/500) ---\n${play}\n---`);
|
||||
if (a.has('dry')) return;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue