Files
lzt404 bc8b34f9f1 chore(release): prepare v0.2.99 notes and refine release formatting
- Add v0.2.99 release notes in all four supported languages
- Limit GitHub Release notes to English followed by Simplified Chinese
- Preserve all translations in app update manifests
- Update release documentation and tests
2026-09-19 20:42:30 +08:00

89 lines
4.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { releaseNotesBody, resolveReleaseNotes, validateReleaseNotes } from '../scripts/release-notes.mjs';
async function withNotesRoot(run: (root: string, directory: string) => Promise<void>) {
const root = await mkdtemp(join(tmpdir(), 'easycli-release-notes-'));
const directory = join(root, 'docs', 'release-notes', 'v1.2.3');
try {
await mkdir(directory, { recursive: true });
await run(root, directory);
} finally {
await rm(root, { recursive: true, force: true });
}
}
describe('localized release notes publication', () => {
test('keeps each authored translation under its exact locale', () => withNotesRoot(async (root, directory) => {
const translations = {
'zh-CN': '## 新增\n\n- 更新说明。\n',
'zh-TW': '## 新增\n\n- 更新說明。\n',
en: '## Added\n\n- Release notes.\n',
ja: '## 新機能\n\n- 更新内容。\n',
};
for (const [locale, body] of Object.entries(translations)) {
await writeFile(join(directory, locale + '.md'), body);
}
expect(await resolveReleaseNotes({ root, tag: '1.2.3' })).toEqual(translations);
const body = releaseNotesBody(translations);
expect(body).toBe(`# English\n\n${translations.en.trim()}\n\n---\n\n# 简体中文\n\n${translations['zh-CN']}`);
}));
test('never copies another language into a missing or blank translation', () => withNotesRoot(async (root, directory) => {
await writeFile(join(directory, 'zh-CN.md'), '## 新增\n\n- 中文独有正文。\n');
await writeFile(join(directory, 'en.md'), ' \n');
const notes = await resolveReleaseNotes({ root, tag: 'v1.2.3' });
expect(notes).toEqual({ 'zh-CN': '## 新增\n\n- 中文独有正文。\n' });
const englishSection = releaseNotesBody(notes).split('# English\n\n')[1].split('\n\n---\n\n')[0].trim();
expect(englishSection).toBe('Release notes for this version are not available in English.');
expect(englishSection).not.toContain('中文独有正文');
}));
test('missing versions and legacy flat files produce no translations', () => withNotesRoot(async (root) => {
await writeFile(join(root, 'docs', 'release-notes', 'v1.2.3.md'), 'Legacy single-language text');
expect(await resolveReleaseNotes({ root, tag: 'v1.2.3' })).toEqual({});
expect(await resolveReleaseNotes({ root, tag: 'v9.9.9' })).toEqual({});
expect(releaseNotesBody({})).not.toContain('Legacy single-language text');
}));
test('validates localized maps without requiring every translation', () => {
expect(() => validateReleaseNotes({})).not.toThrow();
expect(() => validateReleaseNotes({ en: 'English only' })).not.toThrow();
for (const invalid of ['Legacy text', null, [], { en: 123 }, { ja: ' ' }, { fr: 'Other language' }]) {
expect(() => validateReleaseNotes(invalid)).toThrow();
}
});
test('CLI preserves all translations in JSON and publishes only English then Chinese in the Release body', () => withNotesRoot(async (root, directory) => {
const translations = {
'zh-CN': '## 新增\n\n- 更新说明。\n',
'zh-TW': '## 新增\n\n- 更新說明。\n',
en: '## Added\n\n- Release notes.\n',
ja: '## 新機能\n\n- 更新内容。\n',
};
for (const [locale, body] of Object.entries(translations)) {
await writeFile(join(directory, locale + '.md'), body);
}
const script = fileURLToPath(new URL('../scripts/release-notes.mjs', import.meta.url));
const jsonPath = join(root, 'notes.json');
const bodyPath = join(root, 'notes.md');
const result = spawnSync('node', [script, '--tag', 'v1.2.3', '--output', jsonPath, '--body-output', bodyPath], {
cwd: root, encoding: 'utf8',
});
expect(result.status).toBe(0);
expect(result.stderr).toBe('');
const notes = JSON.parse(await readFile(jsonPath, 'utf8'));
expect(notes).toEqual(translations);
const body = await readFile(bodyPath, 'utf8');
expect(body).toBe(`# English\n\n${translations.en.trim()}\n\n---\n\n# 简体中文\n\n${translations['zh-CN']}`);
}));
test('rejects tags that could escape the release notes directory', () => withNotesRoot(async (root) => {
await expect(resolveReleaseNotes({ root, tag: '../../README' })).rejects.toThrow('Invalid release tag');
}));
});