Files
psysonic/scripts/extract-release-section.mjs
T
cucadmuh c7d71ea57c feat(whats-new): remote release notes with dev workspace mode (#1058)
* feat(whats-new): remote release notes with dev workspace mode

Add WHATS_NEW.md, CI whats-new.md asset upload, and client fetch/cache
with embedded fallbacks. Dev and -dev builds read the full file from the
repo for debugging; RC/stable download the release asset on first use.

* fix(whats-new): render ## headings and add changelog tab

Parse h2 sections in release-notes markdown; load changelog alongside
highlights and let users switch views on the What's New page.

* fix(whats-new): prefetch on startup and fix CI typecheck prebuild

Prefetch whats-new asset when the shell loads on RC/stable builds.
Run prebuild:release-notes before tsc and coverage jobs so the
gitignored generated bundle exists in CI.

* docs: CHANGELOG and credits for What's New remote notes (PR #1058)

* fix(whats-new): always slice embedded release notes to current line

Drop full CHANGELOG embed for -dev bundles; tauri:dev still reads live
markdown from the repo. Ignore all of src/generated/ in git.

* fix(whats-new): fetch release asset via Rust to bypass CORS

Route whats-new.md download through fetch_url_bytes; rename the
technical tab label; add fetch unit tests (PR #1058 review).
2026-06-10 23:35:23 +03:00

92 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Extract the body of a ## [version] section from a Keep-a-Changelog-style file.
* Resolution matches src/utils/releaseNotes/releaseNotesMatch.ts.
*
* Usage: node scripts/extract-release-section.mjs <file> <version> [--allow-empty]
* Stdout: section body (no ## header). Exit 1 if empty unless --allow-empty.
*/
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
const SEMVER_CORE = /^v?(\d+\.\d+\.\d+)/i;
function versionCore(version) {
const m = version.trim().match(SEMVER_CORE);
return m ? m[1] : null;
}
function isPlainTriple(header) {
return /^\d+\.\d+\.\d+$/.test(header.trim());
}
function splitBlocks(raw) {
return raw.split(/\n(?=## \[)/).filter((b) => b.startsWith('## ['));
}
function headerVersion(block) {
const m = block.match(/^## \[([^\]]+)\]/);
return m ? m[1] : null;
}
function parseBlock(block) {
const lines = block.split('\n');
const m = lines[0].match(/## \[([^\]]+)\](?:\s*-\s*(.+))?/);
if (!m) return null;
return {
headerVersion: m[1],
date: (m[2] ?? '').trim(),
body: lines.slice(1).join('\n').trim(),
};
}
export function findReleaseSection(raw, appVersion) {
const blocks = splitBlocks(raw);
const exact = blocks.find((b) => b.startsWith(`## [${appVersion}]`));
if (exact) return parseBlock(exact);
const appCore = versionCore(appVersion);
if (!appCore) return null;
const candidates = blocks.filter((b) => {
const hv = headerVersion(b);
return hv !== null && versionCore(hv) === appCore;
});
if (candidates.length === 0) return null;
const plain = candidates.find((b) => {
const hv = headerVersion(b);
return hv !== null && isPlainTriple(hv);
});
return parseBlock(plain ?? candidates[0]);
}
function main() {
const args = process.argv.slice(2);
const allowEmpty = args.includes('--allow-empty');
const positional = args.filter((a) => a !== '--allow-empty');
const [file, version] = positional;
if (!file || !version) {
console.error('Usage: node scripts/extract-release-section.mjs <file> <version> [--allow-empty]');
process.exit(2);
}
const raw = readFileSync(file, 'utf8');
const entry = findReleaseSection(raw, version);
const body = entry?.body?.trim() ?? '';
if (!body) {
if (allowEmpty) process.exit(0);
console.error(`No release section found in ${file} for version ${version}`);
process.exit(1);
}
process.stdout.write(`${body}\n`);
}
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isMain) main();