mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|||
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||
|
|
import { tmpdir } from "node:os";
|
||
|
|
import path from "node:path";
|
||
|
|
import { pathToFileURL } from "node:url";
|
||
|
|
import { expect, test } from "vitest";
|
||
|
|
|
||
|
|
const root = process.cwd();
|
||
|
|
const scriptPath = path.resolve(root, "scripts/manage-release-notes.mjs");
|
||
|
|
|
||
|
|
test("archives custom release notes byte-for-byte and permits an identical retry", async () => {
|
||
|
|
expect(existsSync(scriptPath), "release-note manager is missing").toBe(true);
|
||
|
|
const { archiveCustomReleaseNotes } = await import(pathToFileURL(scriptPath).href);
|
||
|
|
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-notes-"));
|
||
|
|
const source = "# SnapOtter 3.0.0\n\nUnicode: 水獺\n\nTrailing line.\n";
|
||
|
|
|
||
|
|
try {
|
||
|
|
writeFileSync(path.join(fixtureRoot, ".release-notes.md"), source);
|
||
|
|
expect(archiveCustomReleaseNotes(fixtureRoot, "3.0.0")).toBe(true);
|
||
|
|
const archived = path.join(fixtureRoot, ".release-notes/v3.0.0.md");
|
||
|
|
expect(readFileSync(archived, "utf8")).toBe(source);
|
||
|
|
expect(existsSync(path.join(fixtureRoot, ".release-notes.md"))).toBe(false);
|
||
|
|
|
||
|
|
writeFileSync(path.join(fixtureRoot, ".release-notes.md"), source);
|
||
|
|
expect(archiveCustomReleaseNotes(fixtureRoot, "3.0.0")).toBe(true);
|
||
|
|
expect(readFileSync(archived, "utf8")).toBe(source);
|
||
|
|
expect(existsSync(path.join(fixtureRoot, ".release-notes.md"))).toBe(false);
|
||
|
|
} finally {
|
||
|
|
rmSync(fixtureRoot, { force: true, recursive: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("materializes archived custom notes byte-for-byte for a retry", async () => {
|
||
|
|
expect(existsSync(scriptPath), "release-note manager is missing").toBe(true);
|
||
|
|
const releaseNotes = await import(pathToFileURL(scriptPath).href);
|
||
|
|
expect(typeof releaseNotes.materializeReleaseNotes).toBe("function");
|
||
|
|
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-notes-"));
|
||
|
|
const source = "## Highlights\n\nExact custom body.\n";
|
||
|
|
|
||
|
|
try {
|
||
|
|
writeFileSync(path.join(fixtureRoot, ".release-notes.md"), source);
|
||
|
|
releaseNotes.archiveCustomReleaseNotes(fixtureRoot, "3.0.0");
|
||
|
|
const output = path.join(fixtureRoot, "materialized.md");
|
||
|
|
expect(releaseNotes.materializeReleaseNotes(fixtureRoot, "3.0.0", output)).toBe(true);
|
||
|
|
expect(readFileSync(output, "utf8")).toBe(source);
|
||
|
|
} finally {
|
||
|
|
rmSync(fixtureRoot, { force: true, recursive: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("recovers generated release notes from the committed changelog", async () => {
|
||
|
|
expect(existsSync(scriptPath), "release-note manager is missing").toBe(true);
|
||
|
|
const { materializeReleaseNotes } = await import(pathToFileURL(scriptPath).href);
|
||
|
|
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-notes-"));
|
||
|
|
const expected = "# [3.0.0](https://example.test/v3.0.0)\n\n### Fixes\n\n* repaired retries\n";
|
||
|
|
|
||
|
|
try {
|
||
|
|
writeFileSync(
|
||
|
|
path.join(fixtureRoot, "CHANGELOG.md"),
|
||
|
|
`${expected}\n# [2.9.0](https://example.test/v2.9.0)\n\nOlder notes.\n`,
|
||
|
|
);
|
||
|
|
const output = path.join(fixtureRoot, "materialized.md");
|
||
|
|
expect(materializeReleaseNotes(fixtureRoot, "3.0.0", output)).toBe(false);
|
||
|
|
expect(readFileSync(output, "utf8")).toBe(expected);
|
||
|
|
} finally {
|
||
|
|
rmSync(fixtureRoot, { force: true, recursive: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("inserts an exact custom-note entry after the published changelog heading once", async () => {
|
||
|
|
expect(existsSync(scriptPath), "release-note manager is missing").toBe(true);
|
||
|
|
const releaseNotes = await import(pathToFileURL(scriptPath).href);
|
||
|
|
expect(typeof releaseNotes.syncDocsChangelog).toBe("function");
|
||
|
|
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-notes-"));
|
||
|
|
const notes = [
|
||
|
|
"# SnapOtter 3.0.0",
|
||
|
|
"",
|
||
|
|
"## Highlights",
|
||
|
|
"",
|
||
|
|
"Retry-safe release.",
|
||
|
|
"",
|
||
|
|
"## Upgrade",
|
||
|
|
"",
|
||
|
|
"Use the new image.",
|
||
|
|
"",
|
||
|
|
"---",
|
||
|
|
"",
|
||
|
|
].join("\n");
|
||
|
|
|
||
|
|
try {
|
||
|
|
mkdirSync(path.join(fixtureRoot, ".release-notes"));
|
||
|
|
writeFileSync(path.join(fixtureRoot, ".release-notes/v3.0.0.md"), notes);
|
||
|
|
writeFileSync(
|
||
|
|
path.join(fixtureRoot, "apps-docs-changelog.md"),
|
||
|
|
"---\ndescription: History.\n---\n\n# Changelog {#changelog}\n\n## v2.9.0\n\nOlder.\n",
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
releaseNotes.syncDocsChangelog(fixtureRoot, "3.0.0", "2.9.0", "apps-docs-changelog.md"),
|
||
|
|
).toBe(true);
|
||
|
|
expect(
|
||
|
|
releaseNotes.syncDocsChangelog(fixtureRoot, "3.0.0", "2.9.0", "apps-docs-changelog.md"),
|
||
|
|
).toBe(false);
|
||
|
|
const changelog = readFileSync(path.join(fixtureRoot, "apps-docs-changelog.md"), "utf8");
|
||
|
|
expect(changelog.match(/^## v3\.0\.0$/gm)).toHaveLength(1);
|
||
|
|
expect(changelog).toContain(
|
||
|
|
"# Changelog {#changelog}\n\n## v3.0.0\n\nRetry-safe release.\n\n" +
|
||
|
|
"[Full diff on GitHub](https://github.com/snapotter-hq/SnapOtter/compare/v2.9.0...v3.0.0)\n\n---\n",
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
rmSync(fixtureRoot, { force: true, recursive: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("CLI exposes archive, materialize, and changelog preparation for release automation", () => {
|
||
|
|
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "snapotter-release-notes-"));
|
||
|
|
const notes = "## Highlights\n\nCLI release.\n";
|
||
|
|
|
||
|
|
try {
|
||
|
|
mkdirSync(path.join(fixtureRoot, "apps/docs"), { recursive: true });
|
||
|
|
writeFileSync(path.join(fixtureRoot, ".release-notes.md"), notes);
|
||
|
|
writeFileSync(
|
||
|
|
path.join(fixtureRoot, "CHANGELOG.md"),
|
||
|
|
"# [3.0.0](https://example.test/v3.0.0)\n\nGenerated.\n",
|
||
|
|
);
|
||
|
|
writeFileSync(path.join(fixtureRoot, "apps/docs/changelog.md"), "# Changelog {#changelog}\n");
|
||
|
|
|
||
|
|
expect(
|
||
|
|
execFileSync(process.execPath, [scriptPath, "archive", "3.0.0", "--root", fixtureRoot], {
|
||
|
|
encoding: "utf8",
|
||
|
|
}),
|
||
|
|
).toBe("custom=true\n");
|
||
|
|
const materialized = path.join(fixtureRoot, "expected.md");
|
||
|
|
expect(
|
||
|
|
execFileSync(
|
||
|
|
process.execPath,
|
||
|
|
[scriptPath, "materialize", "3.0.0", materialized, "--root", fixtureRoot],
|
||
|
|
{ encoding: "utf8" },
|
||
|
|
),
|
||
|
|
).toBe("custom=true\n");
|
||
|
|
expect(readFileSync(materialized, "utf8")).toBe(notes);
|
||
|
|
expect(
|
||
|
|
execFileSync(
|
||
|
|
process.execPath,
|
||
|
|
[scriptPath, "sync-docs", "3.0.0", "2.9.0", "--root", fixtureRoot],
|
||
|
|
{ encoding: "utf8" },
|
||
|
|
),
|
||
|
|
).toBe("changed=true\n");
|
||
|
|
} finally {
|
||
|
|
rmSync(fixtureRoot, { force: true, recursive: true });
|
||
|
|
}
|
||
|
|
});
|