Files
lst_v3/backend/utils/zipper.utils.ts
Blake Matthes 3e8417dcff
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m54s
refactor(builds): refactored the build process to hold in the build info plus version
2026-06-18 13:51:17 -05:00

181 lines
4.8 KiB
TypeScript

import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import archiver from "archiver";
import { format } from "date-fns";
import { createLogger } from "../logger/logger.controller.js";
import { emitBuildLog } from "./build.utils.js";
import { updateAppStats } from "./updateAppStats.utils.js";
const log = createLogger({ module: "utils", subModule: "zip" });
const exists = async (target: string) => {
try {
await fsp.access(target);
return true;
} catch {
return false;
}
};
const getNextBuildNumber = async (versionPath: string) => {
const raw = await fsp.readFile(versionPath, "utf8");
const config = JSON.parse(raw);
const current = Number.parseInt(config.build.trim(), 10);
let nextBuild: string;
if (Number.isNaN(current) || current < 1) {
nextBuild = "1";
} else {
nextBuild = String(current + 1); // Incrementing the build number
}
const updatedConfig = {
...config,
build: nextBuild,
lastBuildDate: format(new Date(Date.now()), "M/d/yyyy HH:mm"),
};
await fsp.writeFile(
versionPath,
JSON.stringify(updatedConfig, null, 4) + "\n",
"utf8",
);
return { version: config.version, build: config.build };
};
const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
const entries = await fsp.readdir(buildFolder, { withFileTypes: true });
const zipFiles: { fullPath: string; name: string; mtimeMs: number }[] = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
//if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
if (!entry.name.includes("LSTV3")) continue;
const fullPath = path.join(buildFolder, entry.name);
const stat = await fsp.stat(fullPath);
zipFiles.push({
fullPath,
name: entry.name,
mtimeMs: stat.mtimeMs,
});
}
zipFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
const toRemove = zipFiles.slice(maxBuilds);
for (const file of toRemove) {
await fsp.rm(file.fullPath, { force: true });
emitBuildLog(`Removed old build: ${file.name}`);
}
};
export const zipBuild = async () => {
const appDir = process.env.DEV_DIR ?? "";
const maxBuilds = Number(process.env.MAX_BUILDS ?? 5);
if (!appDir) {
log.error({ notify: true }, "Forgot to add in the dev dir into the env");
return;
}
const includesFile = path.join(appDir, ".includes");
const version = path.join(appDir, "package.json");
const buildFolder = path.join(appDir, "builds");
const tempFolder = path.join(appDir, "temp", "zip-temp");
if (!(await exists(includesFile))) {
log.error({ notify: true }, "Missing .includes file common");
return;
}
await fsp.mkdir(buildFolder, { recursive: true });
const lstVersion = await getNextBuildNumber(version);
const zipFileName = `LSTV3-${lstVersion.version}.${lstVersion.build}.zip`;
const zipFile = path.join(buildFolder, zipFileName);
// make the folders in case they are not created already
emitBuildLog(
`Using version, build number: ${lstVersion.version}.${lstVersion.build}`,
);
if (await exists(tempFolder)) {
await fsp.rm(tempFolder, { recursive: true, force: true });
}
await fsp.mkdir(tempFolder, { recursive: true });
const includes = (await fsp.readFile(includesFile, "utf8"))
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
emitBuildLog(`Preparing zip from ${includes.length} include entries`);
for (const relPath of includes) {
const source = path.join(appDir, relPath);
const dest = path.join(tempFolder, relPath);
if (!(await exists(source))) {
emitBuildLog(`Skipping missing path: ${relPath}`, "error");
continue;
}
const stat = await fsp.stat(source);
await fsp.mkdir(path.dirname(dest), { recursive: true });
if (stat.isDirectory()) {
emitBuildLog(`Copying folder: ${relPath}`);
await fsp.cp(source, dest, { recursive: true });
} else {
emitBuildLog(`Copying file: ${relPath}`);
await fsp.copyFile(source, dest);
}
}
// if something crazy happens and we get the same build lets just reuse it
// if (await exists(zipFile)) {
// await fsp.rm(zipFile, { force: true });
// }
emitBuildLog(`Creating zip: ${zipFile}`);
await new Promise<void>((resolve, reject) => {
const output = fs.createWriteStream(zipFile);
const archive = archiver("zip", { zlib: { level: 9 } });
output.on("close", () => resolve());
output.on("error", reject);
archive.on("error", reject);
archive.pipe(output);
// zip contents of temp folder, not temp folder itself
archive.directory(tempFolder, false);
archive.finalize();
});
await fsp.rm(tempFolder, { recursive: true, force: true });
emitBuildLog(`Zip completed successfully: ${zipFile}`);
await cleanupOldBuilds(buildFolder, maxBuilds);
await updateAppStats({
lastUpdated: new Date(),
building: false,
currentBuild: lstVersion.build,
});
return {
success: true,
build: lstVersion.build,
zipFile,
zipFileName,
};
};