chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { execFileSync } from "child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
import {
|
||||
CLI_SEMVER_PATTERN,
|
||||
parseVersionOptionArgument,
|
||||
validateCliDate,
|
||||
validateCliVersion,
|
||||
type InlineValueOption,
|
||||
} from "./cli-options.ts";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
const REPO_URL = "https://github.com/heygen-com/hyperframes";
|
||||
const DOCS_MARKER =
|
||||
"{/* New release entries are prepended by `bun run changelog:draft <version> --write`. */}";
|
||||
|
||||
const CATEGORY_ORDER = [
|
||||
"Breaking Changes",
|
||||
"Features",
|
||||
"Fixes",
|
||||
"Performance",
|
||||
"Docs & Examples",
|
||||
"Catalog",
|
||||
"Internal",
|
||||
"Other Changes",
|
||||
];
|
||||
|
||||
type Options = {
|
||||
version: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
date: string;
|
||||
write: boolean;
|
||||
force: boolean;
|
||||
};
|
||||
|
||||
export type RawCommit = {
|
||||
sha: string;
|
||||
shortSha: string;
|
||||
author: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export type ParsedCommit = RawCommit & {
|
||||
type: string;
|
||||
scope?: string;
|
||||
summary: string;
|
||||
breaking: boolean;
|
||||
category: string;
|
||||
prNumber?: string;
|
||||
};
|
||||
|
||||
type DraftOutput = {
|
||||
releaseNotes: string;
|
||||
docsUpdate: string;
|
||||
};
|
||||
|
||||
type MutableOptions = Omit<Options, "version"> & {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type ValueOptionKey = "from" | "to" | "date";
|
||||
type BooleanOptionKey = "write" | "force";
|
||||
type ParsedSubject = Pick<ParsedCommit, "type" | "scope" | "summary" | "breaking">;
|
||||
|
||||
const VALUE_OPTIONS = new Map<string, ValueOptionKey>([
|
||||
["--from", "from"],
|
||||
["--to", "to"],
|
||||
["--date", "date"],
|
||||
]);
|
||||
|
||||
const BOOLEAN_OPTIONS = new Map<string, BooleanOptionKey>([
|
||||
["--write", "write"],
|
||||
["--force", "force"],
|
||||
]);
|
||||
|
||||
const INLINE_VALUE_OPTIONS = [
|
||||
{ prefix: "--from=", key: "from" },
|
||||
{ prefix: "--to=", key: "to" },
|
||||
{ prefix: "--date=", key: "date" },
|
||||
] satisfies Array<InlineValueOption<ValueOptionKey>>;
|
||||
|
||||
const TYPE_CATEGORIES = new Map([
|
||||
["feat", "Features"],
|
||||
["fix", "Fixes"],
|
||||
["perf", "Performance"],
|
||||
]);
|
||||
|
||||
const INTERNAL_TYPES = new Set(["build", "chore", "ci", "refactor", "test"]);
|
||||
|
||||
function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const draft = createDraft(options);
|
||||
outputDraft(options, draft);
|
||||
}
|
||||
|
||||
function createDraft(options: Options): DraftOutput {
|
||||
const versionTag = `v${options.version}`;
|
||||
const to = options.to ?? (tagExists(versionTag) ? versionTag : "HEAD");
|
||||
const from = options.from ?? resolvePreviousTag(versionTag, to);
|
||||
const commits = getCommits(from, to).filter((commit) => !shouldSkipCommit(commit));
|
||||
const parsedCommits = commits.map(parseCommit);
|
||||
|
||||
const releaseNotes = renderReleaseNotes(options.version, options.date, from, parsedCommits);
|
||||
const docsUpdate = renderDocsUpdate(options.version, options.date, from, parsedCommits);
|
||||
|
||||
return { releaseNotes, docsUpdate };
|
||||
}
|
||||
|
||||
function outputDraft(options: Options, draft: DraftOutput) {
|
||||
if (!options.write) {
|
||||
console.log(draft.releaseNotes);
|
||||
console.log("\n--- Mintlify update block ---\n");
|
||||
console.log(draft.docsUpdate);
|
||||
console.log(
|
||||
"\nRun with --write to create the release file and prepend the docs changelog entry.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
writeReleaseNotes(options.version, draft.releaseNotes, options.force);
|
||||
prependDocsUpdate(options.version, draft.docsUpdate);
|
||||
}
|
||||
|
||||
export function parseArgs(args: string[]): Options {
|
||||
const parsed = createDefaultOptions();
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
index = parseArgument(args, index, parsed);
|
||||
}
|
||||
|
||||
return finalizeOptions(parsed);
|
||||
}
|
||||
|
||||
function createDefaultOptions(): MutableOptions {
|
||||
return {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
write: false,
|
||||
force: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgument(args: string[], index: number, parsed: MutableOptions) {
|
||||
return parseVersionOptionArgument(args, index, parsed, {
|
||||
inlineValueOptions: INLINE_VALUE_OPTIONS,
|
||||
valueOptions: VALUE_OPTIONS,
|
||||
booleanOptions: BOOLEAN_OPTIONS,
|
||||
printUsage,
|
||||
fail,
|
||||
});
|
||||
}
|
||||
|
||||
function finalizeOptions(parsed: MutableOptions): Options {
|
||||
if (!parsed.version) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
validateCliVersion(parsed.version, CLI_SEMVER_PATTERN, fail);
|
||||
validateCliDate(parsed.date, fail);
|
||||
|
||||
return {
|
||||
version: parsed.version,
|
||||
from: parsed.from,
|
||||
to: parsed.to,
|
||||
date: parsed.date,
|
||||
write: parsed.write,
|
||||
force: parsed.force,
|
||||
};
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage:
|
||||
bun run changelog:draft <version> [--write] [--force] [--from <ref>] [--to <ref>] [--date YYYY-MM-DD]
|
||||
|
||||
Examples:
|
||||
bun run changelog:draft 0.6.53
|
||||
bun run changelog:draft 0.6.53 --write
|
||||
bun run changelog:draft 0.6.53 --from v0.6.52 --to HEAD --write
|
||||
`);
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function git(args: string[]) {
|
||||
return execFileSync("git", args, {
|
||||
cwd: ROOT,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function tagExists(tag: string) {
|
||||
try {
|
||||
git(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePreviousTag(versionTag: string, to: string) {
|
||||
try {
|
||||
if (tagExists(versionTag)) {
|
||||
return git(["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*", `${versionTag}^`]);
|
||||
}
|
||||
return git(["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*", to]);
|
||||
} catch {
|
||||
fail("Could not resolve the previous release tag. Pass --from <tag> explicitly.");
|
||||
}
|
||||
}
|
||||
|
||||
function getCommits(from: string, to: string): RawCommit[] {
|
||||
const output = git(["log", "--format=%H%x09%h%x09%an%x09%s", "--no-merges", `${from}..${to}`]);
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return output.split("\n").map((line) => {
|
||||
const [sha = "", shortSha = "", author = "", ...subjectParts] = line.split("\t");
|
||||
return {
|
||||
sha,
|
||||
shortSha,
|
||||
author,
|
||||
subject: subjectParts.join("\t"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldSkipCommit(commit: RawCommit) {
|
||||
const subject = commit.subject.toLowerCase();
|
||||
return (
|
||||
subject.includes("[skip changelog]") ||
|
||||
/^chore: release v\d+\.\d+\.\d+/.test(subject) ||
|
||||
/^chore: bump version/.test(subject)
|
||||
);
|
||||
}
|
||||
|
||||
export function parseCommit(commit: RawCommit): ParsedCommit {
|
||||
const prNumber = extractPrNumber(commit.subject);
|
||||
const subjectWithoutPr = commit.subject.replace(/\s+\(#\d+\)$/, "");
|
||||
const parsedSubject = parseConventionalSubject(subjectWithoutPr);
|
||||
const category = categorizeCommit(parsedSubject);
|
||||
|
||||
return {
|
||||
...commit,
|
||||
...parsedSubject,
|
||||
category,
|
||||
prNumber,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseConventionalSubject(subject: string): ParsedSubject {
|
||||
const match = /^([a-z]+)(?:\(([^)]+)\))?(!)?:\s+(.+)$/.exec(subject);
|
||||
if (!match) {
|
||||
return {
|
||||
type: "other",
|
||||
summary: subject,
|
||||
breaking: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: match[1],
|
||||
scope: match[2],
|
||||
summary: match[4],
|
||||
breaking: match[3] === "!",
|
||||
};
|
||||
}
|
||||
|
||||
function extractPrNumber(subject: string) {
|
||||
return /\(#(\d+)\)$/.exec(subject)?.[1];
|
||||
}
|
||||
|
||||
export function categorizeCommit(subject: ParsedSubject) {
|
||||
if (subject.breaking) {
|
||||
return "Breaking Changes";
|
||||
}
|
||||
|
||||
if (isCatalogChange(subject)) {
|
||||
return "Catalog";
|
||||
}
|
||||
|
||||
return knownCategoryFor(subject) ?? "Other Changes";
|
||||
}
|
||||
|
||||
function isCatalogChange(subject: ParsedSubject) {
|
||||
const normalizedScope = subject.scope?.toLowerCase() ?? "";
|
||||
const normalizedSummary = subject.summary.toLowerCase();
|
||||
return (
|
||||
["catalog", "registry"].includes(normalizedScope) || /catalog|registry/.test(normalizedSummary)
|
||||
);
|
||||
}
|
||||
|
||||
function knownCategoryFor(subject: ParsedSubject) {
|
||||
return (
|
||||
TYPE_CATEGORIES.get(subject.type) ??
|
||||
docsCategoryFor(subject) ??
|
||||
internalCategoryFor(subject.type)
|
||||
);
|
||||
}
|
||||
|
||||
function docsCategoryFor(subject: ParsedSubject) {
|
||||
return isDocsChange(subject) ? "Docs & Examples" : undefined;
|
||||
}
|
||||
|
||||
function isDocsChange(subject: ParsedSubject) {
|
||||
const normalizedFields = [subject.type, subject.scope?.toLowerCase()];
|
||||
return normalizedFields.includes("docs") || subject.summary.toLowerCase().includes("example");
|
||||
}
|
||||
|
||||
function internalCategoryFor(type: string) {
|
||||
return INTERNAL_TYPES.has(type) ? "Internal" : undefined;
|
||||
}
|
||||
|
||||
function renderReleaseNotes(version: string, date: string, from: string, commits: ParsedCommit[]) {
|
||||
const sections = renderSections(commits, renderCommitBullet);
|
||||
const compareUrl = `${REPO_URL}/compare/${from}...v${version}`;
|
||||
|
||||
return [
|
||||
`# HyperFrames v${version}`,
|
||||
"",
|
||||
`Released on ${date}.`,
|
||||
"",
|
||||
"<!-- TODO: write a 1-2 sentence release summary here. -->",
|
||||
"",
|
||||
sections,
|
||||
"",
|
||||
"## Full changelog",
|
||||
"",
|
||||
compareUrl,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderDocsUpdate(version: string, date: string, from: string, commits: ParsedCommit[]) {
|
||||
const sections = renderSections(commits, renderMdxCommitBullet);
|
||||
const compareUrl = `${REPO_URL}/compare/${from}...v${version}`;
|
||||
const tags = renderTags(commits);
|
||||
|
||||
return [
|
||||
"<Update",
|
||||
` label="HyperFrames v${version}"`,
|
||||
` description="Released - ${date}"`,
|
||||
` tags={${renderTagsLiteral(tags)}}`,
|
||||
">",
|
||||
"<!-- TODO: write a 1-2 sentence release summary here. -->",
|
||||
"",
|
||||
sections,
|
||||
"",
|
||||
`[View the full commit range](${compareUrl}).`,
|
||||
"</Update>",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderSections(commits: ParsedCommit[], renderBullet: (commit: ParsedCommit) => string) {
|
||||
if (commits.length === 0) {
|
||||
return "No notable changes were found in the selected commit range.";
|
||||
}
|
||||
|
||||
return CATEGORY_ORDER.flatMap((category) => {
|
||||
const commitsInCategory = commits.filter((commit) => commit.category === category);
|
||||
if (commitsInCategory.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [`## ${category}`, "", ...commitsInCategory.map(renderBullet), ""];
|
||||
})
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function renderCommitBullet(commit: ParsedCommit) {
|
||||
const scope = commit.scope ? `**${formatScope(commit.scope)}:** ` : "";
|
||||
const links = [`[${commit.shortSha}](${REPO_URL}/commit/${commit.sha})`];
|
||||
if (commit.prNumber) {
|
||||
links.push(`[#${commit.prNumber}](${REPO_URL}/pull/${commit.prNumber})`);
|
||||
}
|
||||
|
||||
return `- ${scope}${capitalize(commit.summary)} (${links.join(", ")})`;
|
||||
}
|
||||
|
||||
export function renderMdxCommitBullet(commit: ParsedCommit) {
|
||||
const scope = commit.scope ? `**${escapeForMdx(formatScope(commit.scope))}:** ` : "";
|
||||
const links = [`[${commit.shortSha}](${REPO_URL}/commit/${commit.sha})`];
|
||||
if (commit.prNumber) {
|
||||
links.push(`[#${commit.prNumber}](${REPO_URL}/pull/${commit.prNumber})`);
|
||||
}
|
||||
|
||||
return `- ${scope}${escapeForMdx(capitalize(commit.summary))} (${links.join(", ")})`;
|
||||
}
|
||||
|
||||
function renderTags(commits: ParsedCommit[]) {
|
||||
return ["Release", ...uniqueScopeTags(commits).slice(0, 3)];
|
||||
}
|
||||
|
||||
function uniqueScopeTags(commits: ParsedCommit[]) {
|
||||
return Array.from(new Set(commits.flatMap(scopeTagsForCommit)));
|
||||
}
|
||||
|
||||
function scopeTagsForCommit(commit: ParsedCommit) {
|
||||
return commit.scope ? [formatScope(commit.scope)] : [];
|
||||
}
|
||||
|
||||
export function formatScope(scope: string) {
|
||||
const knownScopes = new Map([
|
||||
["api", "API"],
|
||||
["aws", "AWS"],
|
||||
["aws-lambda", "AWS Lambda"],
|
||||
["cli", "CLI"],
|
||||
["core", "Core"],
|
||||
["docs", "Docs"],
|
||||
["engine", "Engine"],
|
||||
["ffmpeg", "FFmpeg"],
|
||||
["producer", "Producer"],
|
||||
["readme", "README"],
|
||||
["studio", "Studio"],
|
||||
]);
|
||||
|
||||
const known = knownScopes.get(scope.toLowerCase());
|
||||
if (known) {
|
||||
return known;
|
||||
}
|
||||
|
||||
return scope
|
||||
.split(/[-_]/)
|
||||
.map((part) => capitalize(part))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function capitalize(value: string) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return value[0].toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function renderTagsLiteral(tags: string[]) {
|
||||
return `[${tags.map((tag) => JSON.stringify(tag)).join(", ")}]`;
|
||||
}
|
||||
|
||||
export function escapeForMdx(text: string) {
|
||||
return text
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/</g, "\\<")
|
||||
.replace(/>/g, "\\>")
|
||||
.replace(/\{/g, "\\{")
|
||||
.replace(/\}/g, "\\}");
|
||||
}
|
||||
|
||||
function writeReleaseNotes(version: string, releaseNotes: string, force: boolean) {
|
||||
const releasesDir = join(ROOT, "releases");
|
||||
const releasePath = join(releasesDir, `v${version}.md`);
|
||||
mkdirSync(releasesDir, { recursive: true });
|
||||
|
||||
// Use an exclusive-write flag rather than a separate existsSync check so the
|
||||
// "already exists" guard is atomic with the write (no TOCTOU race).
|
||||
// EEXIST is only reachable when force is false (the "wx" flag); with force the
|
||||
// "w" flag overwrites and never throws it, so no separate force check is needed.
|
||||
try {
|
||||
writeFileSync(releasePath, `${releaseNotes}\n`, { flag: force ? "w" : "wx" });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
||||
console.log(
|
||||
`${releasePath} already exists; leaving it unchanged. Pass --force to overwrite it.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
console.log(`Wrote ${releasePath}`);
|
||||
}
|
||||
|
||||
function prependDocsUpdate(version: string, docsUpdate: string) {
|
||||
const changelogPath = join(ROOT, "docs", "changelog.mdx");
|
||||
const changelog = readFileSync(changelogPath, "utf-8");
|
||||
|
||||
if (changelog.includes(`label="HyperFrames v${version}"`)) {
|
||||
console.log(`docs/changelog.mdx already has a v${version} entry; leaving it unchanged.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changelog.includes(DOCS_MARKER)) {
|
||||
fail(`Could not find insertion marker in ${changelogPath}`);
|
||||
}
|
||||
|
||||
const updated = changelog.replace(DOCS_MARKER, `${DOCS_MARKER}\n\n${docsUpdate}`);
|
||||
writeFileSync(changelogPath, updated);
|
||||
console.log(`Prepended v${version} to ${changelogPath}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user