feat(admin): moved server build/update to full app
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
This commit is contained in:
91
backend/utils/build.utils.ts
Normal file
91
backend/utils/build.utils.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { updateAppStats } from "./updateAppStats.utils.js";
|
||||
import { zipBuild } from "./zipper.utils.js";
|
||||
|
||||
export const emitBuildLog = (message: string, level = "info") => {
|
||||
const payload = {
|
||||
type: "build",
|
||||
level,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
//console.log(`[BUILD][${level.toUpperCase()}] ${message}`);
|
||||
|
||||
emitToRoom("admin:build", payload as any);
|
||||
if (payload.level === "info") {
|
||||
log.info({ stack: payload }, payload.message);
|
||||
}
|
||||
|
||||
// if (log) {
|
||||
// log(payload);
|
||||
// }
|
||||
};
|
||||
|
||||
export let building = false;
|
||||
const log = createLogger({ module: "utils", subModule: "builds" });
|
||||
export const build = async () => {
|
||||
const appDir = process.env.DEV_DIR ?? "";
|
||||
return new Promise((resolve) => {
|
||||
building = true;
|
||||
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: true,
|
||||
});
|
||||
|
||||
emitBuildLog(`Starting build in: ${appDir}`);
|
||||
|
||||
const child = spawn("npm", ["run", "build"], {
|
||||
cwd: appDir,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim() !== "") {
|
||||
emitBuildLog(line, "info");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim() !== "") {
|
||||
emitBuildLog(line, "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
emitBuildLog("Build completed successfully.", "info");
|
||||
building = false;
|
||||
zipBuild();
|
||||
resolve(true);
|
||||
} else {
|
||||
building = false;
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
});
|
||||
emitBuildLog(`Build failed with code ${code}`, "error");
|
||||
//reject(new Error(`Build failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
building = false;
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
});
|
||||
emitBuildLog(`Process error: ${err.message}`, "error");
|
||||
// reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
123
backend/utils/deployApp.ts
Normal file
123
backend/utils/deployApp.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { serverData } from "../db/schema/serverData.schema.js";
|
||||
import { appStats } from "../db/schema/stats.schema.js";
|
||||
//import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitBuildLog } from "./build.utils.js";
|
||||
import { returnFunc } from "./returnHelper.utils.js";
|
||||
|
||||
// const log = createLogger({ module: "utils", subModule: "deploy" });
|
||||
export let updating = false;
|
||||
|
||||
const updateServerBuildNumber = async (token: string) => {
|
||||
// get the current build
|
||||
const buildNum = await db.select().from(appStats);
|
||||
|
||||
// update the build now
|
||||
|
||||
await db
|
||||
.update(serverData)
|
||||
.set({ buildNumber: buildNum[0]?.currentBuild, lastUpdated: sql`NOW()` })
|
||||
.where(eq(serverData.plantToken, token));
|
||||
};
|
||||
export const runUpdate = ({
|
||||
server,
|
||||
destination,
|
||||
token,
|
||||
}: {
|
||||
server: string;
|
||||
destination: string;
|
||||
token: string;
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
updating = true;
|
||||
const scriptPath = process.env.UPDATE_SCRIPT_PATH;
|
||||
if (!scriptPath) {
|
||||
return returnFunc({
|
||||
success: true,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "deploy",
|
||||
message: "UPDATE_SCRIPT_PATH please make sure you have this set.",
|
||||
data: [],
|
||||
notify: true,
|
||||
room: "admin",
|
||||
});
|
||||
}
|
||||
|
||||
const args = [
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
scriptPath,
|
||||
"-Server",
|
||||
server,
|
||||
"-Destination",
|
||||
destination,
|
||||
"-Token",
|
||||
token,
|
||||
"-ADM_USER",
|
||||
process.env.DEV_USER ?? "",
|
||||
"-ADM_PASSWORD",
|
||||
process.env.DEV_PASSWORD ?? "",
|
||||
"-AppDir",
|
||||
process.env.DEV_DIR ?? "",
|
||||
];
|
||||
|
||||
emitBuildLog(`Starting update for ${server}`);
|
||||
|
||||
const child = spawn("powershell.exe", args, {
|
||||
shell: false,
|
||||
});
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
emitBuildLog(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
emitBuildLog(line, "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
emitBuildLog(`Update completed for ${server}`);
|
||||
updating = false;
|
||||
updateServerBuildNumber(token);
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Update completed for ${server}`,
|
||||
data: [],
|
||||
});
|
||||
} else {
|
||||
emitBuildLog(`Update failed for ${server} (code ${code})`, "error");
|
||||
updating = false;
|
||||
reject({
|
||||
success: false,
|
||||
message: `Update failed for ${server} (code ${code})`,
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
emitBuildLog(`Process error: ${err.message}`, "error");
|
||||
updating = false;
|
||||
reject({
|
||||
success: false,
|
||||
message: `${server}: Encountered an error while processing: ${err.message} `,
|
||||
data: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -14,7 +14,8 @@ export interface ReturnHelper<T = unknown[]> {
|
||||
| "email"
|
||||
| "purchase"
|
||||
| "tcp"
|
||||
| "logistics";
|
||||
| "logistics"
|
||||
| "admin";
|
||||
subModule: string;
|
||||
|
||||
level: "info" | "error" | "debug" | "fatal" | "warn";
|
||||
|
||||
17
backend/utils/updateAppStats.utils.ts
Normal file
17
backend/utils/updateAppStats.utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { appStats } from "../db/schema/stats.schema.js";
|
||||
|
||||
export const updateAppStats = async (
|
||||
data: Partial<typeof appStats.$inferInsert>,
|
||||
) => {
|
||||
await db
|
||||
.insert(appStats)
|
||||
.values({
|
||||
id: "primary",
|
||||
...data,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: appStats.id,
|
||||
set: data,
|
||||
});
|
||||
};
|
||||
177
backend/utils/zipper.utils.ts
Normal file
177
backend/utils/zipper.utils.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import archiver from "archiver";
|
||||
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 (buildNumberFile: string) => {
|
||||
if (!(await exists(buildNumberFile))) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const raw = await fsp.readFile(buildNumberFile, "utf8");
|
||||
const current = Number.parseInt(raw.trim(), 10);
|
||||
|
||||
if (Number.isNaN(current) || current < 1) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const next = current + 1;
|
||||
|
||||
await fsp.writeFile(buildNumberFile, String(next), "utf8");
|
||||
|
||||
// update the server with the next build number
|
||||
|
||||
await updateAppStats({
|
||||
currentBuild: next,
|
||||
lastBuildAt: new Date(),
|
||||
building: true,
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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 buildNumberFile = path.join(appDir, ".buildNumber");
|
||||
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 buildNumber = await getNextBuildNumber(buildNumberFile);
|
||||
const zipFileName = `LSTV3-${buildNumber}.zip`;
|
||||
const zipFile = path.join(buildFolder, zipFileName);
|
||||
// make the folders in case they are not created already
|
||||
emitBuildLog(`Using build number: ${buildNumber}`);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
buildNumber,
|
||||
zipFile,
|
||||
zipFileName,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user