multi-instance-update.js (3366B)
1 const fs = require("fs"); 2 const path = require("path"); 3 const { spawnSync } = require("child_process"); 4 5 const ROOT = path.join(__dirname, ".."); 6 7 function log(msg) { 8 console.log(`[multi-update] ${msg}`); 9 } 10 11 function fail(msg) { 12 console.error(`[multi-update] ERROR: ${msg}`); 13 process.exit(1); 14 } 15 16 function parseArgs() { 17 const args = process.argv.slice(2); 18 const out = {}; 19 for (const raw of args) { 20 const a = String(raw || ""); 21 if (!a.startsWith("--")) continue; 22 const eq = a.indexOf("="); 23 if (eq > -1) { 24 out[a.slice(2, eq)] = a.slice(eq + 1); 25 continue; 26 } 27 out[a.slice(2)] = "1"; 28 } 29 return out; 30 } 31 32 function run(cmd, args, cwd = ROOT, { allowFail = false } = {}) { 33 const result = spawnSync(cmd, args, { cwd, stdio: "inherit", shell: false }); 34 if (result.error) fail(`${cmd} failed to start: ${result.error?.message || result.error}`); 35 const code = result.status || 0; 36 if (code !== 0 && !allowFail) fail(`${cmd} ${args.join(" ")} failed with exit code ${code}`); 37 return code; 38 } 39 40 function main() { 41 const args = parseArgs(); 42 const remote = String(args.remote || "origin").trim(); 43 const branch = String(args.branch || "main").trim(); 44 const configPath = args.config 45 ? path.resolve(process.cwd(), String(args.config)) 46 : path.join(ROOT, "multi_instance", "instances.json"); 47 const composePath = args.compose 48 ? path.resolve(process.cwd(), String(args.compose)) 49 : path.join(path.dirname(configPath), "docker-compose.yml"); 50 51 const skipGit = args["skip-git"] === "1"; 52 const skipBuild = args["skip-build"] === "1"; 53 const routeDns = args["route-dns"] === "1"; 54 const dryRun = args["dry-run"] === "1"; 55 56 if (!skipGit) { 57 log(`Pulling latest source-of-truth from ${remote}/${branch}...`); 58 run("git", ["fetch", remote], ROOT); 59 run("git", ["checkout", branch], ROOT); 60 run("git", ["pull", "--ff-only", remote, branch], ROOT); 61 } else { 62 log("Skipping git fetch/pull (--skip-git)."); 63 } 64 65 if (!fs.existsSync(configPath)) { 66 fail(`Missing config: ${configPath}\nRun: node scripts/multi-instance-init.js --config=${configPath}`); 67 } 68 69 log("Regenerating multi-instance compose/env files..."); 70 const initArgs = [`--config=${configPath}`]; 71 if (routeDns) initArgs.push("--route-dns"); 72 run(process.execPath, [path.join(ROOT, "scripts", "multi-instance-init.js"), ...initArgs], ROOT); 73 74 if (!fs.existsSync(composePath)) fail(`Expected compose file was not generated: ${composePath}`); 75 76 const composeRef = composePath.replace(/\\/g, "/"); 77 const upArgs = ["compose", "-f", composeRef, "up", "-d", "--remove-orphans"]; 78 if (!skipBuild) upArgs.push("--build"); 79 80 if (dryRun) { 81 log("Dry run mode enabled (--dry-run). No docker compose command executed."); 82 console.log(""); 83 console.log("Would run:"); 84 console.log(`docker ${upArgs.join(" ")}`); 85 return; 86 } 87 88 log("Updating all instances..."); 89 run("docker", upArgs, ROOT); 90 91 log("Container status:"); 92 run("docker", ["compose", "-f", composeRef, "ps"], ROOT, { allowFail: true }); 93 94 console.log(""); 95 console.log("Done."); 96 console.log(`- Updated compose stack: ${path.relative(ROOT, composePath).replace(/\\/g, "/")}`); 97 console.log("- Verify health for each hostname and /api/health endpoint."); 98 console.log("- If cloudflared ingress changed, restart tunnel service/process."); 99 } 100 101 main();