repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/build/release.ts
Rawimport { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
cpSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readlinkSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { join, sep } from "node:path";
import {
allExtensionNames,
buildExtension,
ROOT,
rootVersion,
} from "./build.ts";
import { listFiles } from "./publish.ts";
import { readBuildSpec } from "./spec.ts";
import { verifyCollection, verifyPackage } from "./verify.ts";
export function treeDigest(dir: string): string {
const hash = createHash("sha256");
for (const file of listFiles(dir)) {
const path = join(dir, file);
const stat = lstatSync(path);
if (stat.isSymbolicLink()) {
if (!realpathSync(path).startsWith(realpathSync(dir) + sep))
throw new Error(`Symlink escapes package: ${path}`);
hash.update(`${file}\0link:${readlinkSync(path)}\n`);
} else {
if (!stat.isFile()) throw new Error(`Not a regular file: ${path}`);
const digest = createHash("sha256")
.update(readFileSync(path))
.digest("hex");
hash.update(`${file}\0file:${digest}\n`);
}
}
return hash.digest("hex");
}
export function collectionManifest(
version: string,
packages: Array<{ name: string; pi: Record<string, string[]> }>,
) {
const pi: Record<string, string[]> = {};
for (const pkg of packages) {
for (const [kind, paths] of Object.entries(pkg.pi)) {
pi[kind] ??= [];
pi[kind].push(...paths.map((path) => `packages/${pkg.name}/${path}`));
}
}
return { name: "pi-ext", version, private: true, type: "module", pi };
}
function tar(args: string[]): void {
const result = spawnSync("tar", args, { encoding: "utf8" });
if (result.status !== 0) throw new Error(`tar failed: ${result.stderr}`);
}
/** Validate the actual archived bytes, not just the pre-pack directory. */
export function archivePackage(
dir: string,
archive: string,
scratch: string,
): string {
const digest = treeDigest(dir);
tar(["-czf", archive, "-C", dir, "."]);
const extracted = mkdtempSync(join(scratch, "extract-"));
try {
tar(["-xzf", archive, "-C", extracted]);
if (treeDigest(extracted) !== digest)
throw new Error(`Archive differs from package: ${archive}`);
} finally {
rmSync(extracted, { recursive: true, force: true });
}
return createHash("sha256").update(readFileSync(archive)).digest("hex");
}
export async function releaseAll(): Promise<void> {
const version = rootVersion();
const scratch = join(ROOT, ".pi", "tmp");
mkdirSync(scratch, { recursive: true });
const stage = mkdtempSync(join(scratch, "release-"));
const output = join(scratch, "release-artifacts");
// This task owns only its disposable artifact output, never dist or source.
rmSync(output, { recursive: true, force: true });
mkdirSync(output, { recursive: true });
const packages = [];
const receipts = [];
try {
for (const name of allExtensionNames()) {
const built = await buildExtension(name, {
distRoot: join(stage, "packages"),
release: true,
});
const report = await verifyPackage(built.dir, {
externals: readBuildSpec(join(ROOT, "extensions", name)).externals,
});
if (!report.ok) throw new Error(`${name}: ${JSON.stringify(report)}`);
const pkg = JSON.parse(
readFileSync(join(built.dir, "package.json"), "utf8"),
);
packages.push({ name, pi: pkg.pi });
const file = `${name}-${version}.tgz`;
const digest = treeDigest(built.dir);
const sha256 = archivePackage(built.dir, join(output, file), scratch);
receipts.push({ extension: name, version, file, sha256, digest });
console.log(
`OK release ${name}@${version}: relocated load + archive bytes`,
);
}
writeFileSync(
join(stage, "package.json"),
`${JSON.stringify(collectionManifest(version, packages), null, "\t")}\n`,
);
cpSync(join(ROOT, "README.md"), join(stage, "README.md"));
const collection = await verifyCollection([stage]);
if (!collection.ok)
throw new Error(`Collection: ${JSON.stringify(collection)}`);
const file = `pi-ext-${version}.tgz`;
const sha256 = archivePackage(stage, join(output, file), scratch);
writeFileSync(
join(output, "manifest.json"),
`${JSON.stringify({ version, collection: { file, sha256, digest: treeDigest(stage) }, extensions: receipts }, null, "\t")}\n`,
);
console.log(
`OK release pi-ext@${version}: ${packages.length} extensions; ${output}`,
);
} catch (error) {
rmSync(output, { recursive: true, force: true });
throw error;
} finally {
rmSync(stage, { recursive: true, force: true });
}
}
if (import.meta.main) await releaseAll();