docs: generate docs map at publish time (#117398)

* docs: generate docs map at publish time

* fix: canonicalize Docker pack entry path

* fix: preserve package lifecycle ownership
This commit is contained in:
Peter Steinberger
2026-08-01 07:01:43 -07:00
committed by GitHub
parent e4d1b7e0d3
commit 385f1dee16
27 changed files with 962 additions and 11411 deletions

View File

@@ -0,0 +1,131 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { renderDocsHeadingMap } from "../../scripts/docs-list.js";
import { restorePrepackArtifacts } from "../../scripts/openclaw-postpack.mjs";
import { preparePackageChangelog } from "../../scripts/package-changelog.mjs";
import { preparePackageDocsMap, restorePackageDocsMap } from "../../scripts/package-docs-map.mjs";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const tempDirs: string[] = [];
const sourceChangelog = `# Changelog
## 2026.8.1
- Current release notes with enough detail for package validation.
## 2026.7.1
- Previous release notes with enough detail for package validation.
`;
function makePackageRoot(): string {
const root = makeTempDir(tempDirs, "openclaw-package-docs-map-");
mkdirSync(path.join(root, "docs"), { recursive: true });
writeFileSync(path.join(root, "docs", "page.md"), "# Package docs\n", "utf8");
writeFileSync(path.join(root, "package.json"), '{"name":"openclaw","version":"2026.8.1"}\n');
writeFileSync(path.join(root, "CHANGELOG.md"), sourceChangelog);
return root;
}
afterEach(() => {
cleanupTempDirs(tempDirs);
});
describe("package docs map", () => {
it("materializes exact generated bytes and removes only its transient copy", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
await expect(preparePackageDocsMap(root)).resolves.toBe(true);
expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
await expect(restorePackageDocsMap(root)).resolves.toBe(true);
expect(existsSync(mapPath)).toBe(false);
});
it("preserves an identical map that existed before packaging", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
const content = renderDocsHeadingMap(path.join(root, "docs"));
writeFileSync(mapPath, content, "utf8");
await expect(preparePackageDocsMap(root)).resolves.toBe(false);
await expect(restorePackageDocsMap(root)).resolves.toBe(false);
expect(readFileSync(mapPath, "utf8")).toBe(content);
});
it("restores a tracked source stub after packaging", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
const stub = "# Docs map source\n";
writeFileSync(mapPath, stub, "utf8");
await expect(preparePackageDocsMap(root)).resolves.toBe(true);
expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
await expect(restorePackageDocsMap(root)).resolves.toBe(true);
expect(readFileSync(mapPath, "utf8")).toBe(stub);
});
it("serializes concurrent package preparations with the receipt", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
const stub = "# Docs map source\n";
writeFileSync(mapPath, stub, "utf8");
const results = await Promise.allSettled([
preparePackageDocsMap(root),
preparePackageDocsMap(root),
]);
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
const rejected = results.find((result) => result.status === "rejected");
expect(rejected).toMatchObject({
reason: expect.objectContaining({
code: "PACKAGE_DOCS_MAP_ACTIVE",
message: expect.stringContaining("node scripts/openclaw-postpack.mjs"),
}),
});
expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
await expect(restorePackageDocsMap(root)).resolves.toBe(true);
expect(readFileSync(mapPath, "utf8")).toBe(stub);
});
it("refuses to remove a transient map changed after prepack", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
await preparePackageDocsMap(root);
writeFileSync(mapPath, "operator change\n", "utf8");
await expect(restorePackageDocsMap(root)).rejects.toThrow("changed after prepack");
expect(readFileSync(mapPath, "utf8")).toBe("operator change\n");
});
it("keeps the lifecycle lock until interrupted changelog state is restored", async () => {
const root = makePackageRoot();
const mapPath = path.join(root, "docs", "docs_map.md");
const receiptPath = path.join(root, ".artifacts", "package-docs-map", "receipt.json");
const changelogBackupPath = path.join(
root,
".artifacts",
"package-changelog",
"CHANGELOG.md.prepack-backup",
);
const stub = "# Docs map source\n";
writeFileSync(mapPath, stub);
await preparePackageDocsMap(root);
await preparePackageChangelog(root);
const packagedChangelog = readFileSync(path.join(root, "CHANGELOG.md"), "utf8");
writeFileSync(path.join(root, "CHANGELOG.md"), "operator change\n");
await expect(restorePrepackArtifacts(root)).rejects.toThrow("changed since the backup");
expect(existsSync(receiptPath)).toBe(true);
expect(existsSync(changelogBackupPath)).toBe(true);
expect(readFileSync(mapPath, "utf8")).toBe(renderDocsHeadingMap(path.join(root, "docs")));
writeFileSync(path.join(root, "CHANGELOG.md"), packagedChangelog);
await expect(restorePrepackArtifacts(root)).resolves.toBeUndefined();
expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(sourceChangelog);
expect(readFileSync(mapPath, "utf8")).toBe(stub);
expect(existsSync(changelogBackupPath)).toBe(false);
expect(existsSync(receiptPath)).toBe(false);
});
});