import { createHash } from "node:crypto"; import { createReadStream, createWriteStream } from "node:fs"; import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import * as tar from "tar"; import { ZipFile } from "yazl"; const root = process.cwd(); const distDir = path.join(root, "dist"); const releaseDir = path.join(distDir, "release"); function releaseTag() { const value = process.env.TAG ?? process.env.FORGEJO_REF_NAME ?? process.env.GITHUB_REF_NAME ?? ""; return value.replace(/^refs\/tags\//, "") || "dev"; } function cleanTag(tag) { return tag.replace(/[^A-Za-z0-9._-]+/g, "-"); } async function ensureBuilt() { const index = path.join(distDir, "index.html"); try { const info = await stat(index); if (!info.isFile()) throw new Error(); } catch { throw new Error("dist/index.html is missing. Run `pnpm run build` before `pnpm run release:pack`."); } } async function distEntries() { const entries = await readdir(distDir); return entries.filter((entry) => entry !== "release"); } async function addZipPath(zip, source, name) { const info = await stat(source); if (info.isDirectory()) { const children = await readdir(source); if (children.length === 0) { zip.addEmptyDirectory(name); return; } for (const child of children) { await addZipPath(zip, path.join(source, child), `${name}/${child}`); } return; } zip.addFile(source, name); } async function writeZip(file, entries, bundleName) { await new Promise((resolve, reject) => { const output = createWriteStream(file); const zip = new ZipFile(); output.on("close", resolve); output.on("error", reject); zip.outputStream.on("error", reject); zip.outputStream.pipe(output); Promise.all(entries.map((entry) => addZipPath(zip, path.join(distDir, entry), `${bundleName}/${entry}`))) .then(() => zip.end()) .catch(reject); }); } async function sha256(file) { const hash = createHash("sha256"); await new Promise((resolve, reject) => { createReadStream(file) .on("data", (chunk) => hash.update(chunk)) .on("error", reject) .on("end", resolve); }); return hash.digest("hex"); } async function readBuildInfo() { try { return JSON.parse(await readFile(path.join(distDir, "spec", "build-info.json"), "utf8")); } catch { return {}; } } async function main() { await ensureBuilt(); const tag = cleanTag(releaseTag()); const bundleName = `wry-docs-${tag}`; const entries = await distEntries(); await rm(releaseDir, { recursive: true, force: true }); await mkdir(releaseDir, { recursive: true }); const tarball = path.join(releaseDir, `${bundleName}.tar.gz`); const zipfile = path.join(releaseDir, `${bundleName}.zip`); await tar.c( { cwd: distDir, file: tarball, gzip: true, portable: true, prefix: `${bundleName}/` }, entries ); await writeZip(zipfile, entries, bundleName); const assets = [tarball, zipfile]; const sums = []; for (const file of assets) { sums.push(`${await sha256(file)} ${path.basename(file)}`); } await writeFile(path.join(releaseDir, "SHA256SUMS"), `${sums.join("\n")}\n`); const info = await readBuildInfo(); const notes = [ `# Wry Docs ${tag}`, "", "Static documentation bundle generated from Wry's TOML configuration specs.", "", "## Source", "", `- Wry branch: \`${info.sourceBranch ?? "unknown"}\``, `- Wry commit: \`${info.sourceCommit ?? "unknown"}\``, `- Generated at: \`${info.generatedAt ?? "unknown"}\``, "", "## Assets", "", `- \`${path.basename(tarball)}\``, `- \`${path.basename(zipfile)}\``, "- `SHA256SUMS`", "" ].join("\n"); await writeFile(path.join(releaseDir, "RELEASE_NOTES.md"), notes); console.log(`Wrote release assets to ${releaseDir}`); } main().catch((error) => { console.error(error); process.exit(1); });