im_wower
·
2026-03-22
nginx-sync-plan.mjs
1#!/usr/bin/env node
2
3import {
4 chmodSync,
5 mkdirSync,
6 readFileSync,
7 writeFileSync,
8} from "node:fs";
9import { basename, resolve } from "node:path";
10import {
11 buildDesiredDnsRecords,
12 buildHostInventory,
13 buildRenderedNginxArtifacts,
14 loadOpsConfig,
15 parseCliArgs,
16 repoRoot,
17 resolvePath,
18} from "./lib/ops-config.mjs";
19
20const usage = `Usage:
21 node scripts/ops/nginx-sync-plan.mjs [--env PATH] [--bundle-dir PATH] [--check-repo]
22
23Behavior:
24 - Render the committed Nginx template from the inventory file.
25 - Stage a deploy bundle with deploy-on-vps.sh.
26 - --check-repo compares the rendered output against ops/nginx/baa-conductor.conf in the repo.`;
27
28function main() {
29 const args = parseCliArgs(process.argv.slice(2));
30
31 if (args.help) {
32 console.log(usage);
33 return;
34 }
35
36 const config = loadOpsConfig(args.env);
37 const bundleDir = resolvePath(args.bundleDir ?? config.nginx.bundleDir);
38 const templates = loadTemplates();
39 const rendered = buildRenderedNginxArtifacts(config, templates);
40 const repoCheck = compareWithRepo(rendered);
41
42 if (args.checkRepo && !repoCheck.clean) {
43 console.error("nginx-sync-plan failed: rendered config drifted from committed ops/nginx artifacts.");
44 for (const mismatch of repoCheck.mismatches) {
45 console.error(`- ${mismatch}`);
46 }
47 process.exitCode = 1;
48 return;
49 }
50
51 writeBundle(bundleDir, config, rendered);
52 printSummary(config, bundleDir, repoCheck);
53}
54
55function loadTemplates() {
56 return {
57 siteConf: readFileSync(resolve(repoRoot, "ops/nginx/templates/baa-conductor.conf.template"), "utf8"),
58 commonProxy: readFileSync(resolve(repoRoot, "ops/nginx/includes/common-proxy.conf"), "utf8"),
59 };
60}
61
62function compareWithRepo(rendered) {
63 const mismatches = [];
64 const repoSiteConf = readFileSync(resolve(repoRoot, "ops/nginx/baa-conductor.conf"), "utf8");
65
66 if (repoSiteConf !== rendered.siteConf) {
67 mismatches.push("ops/nginx/baa-conductor.conf");
68 }
69
70 return {
71 clean: mismatches.length === 0,
72 mismatches,
73 };
74}
75
76function writeBundle(bundleDir, config, rendered) {
77 const siteTargetPath = resolve(bundleDir, stripLeadingSlash(config.nginx.siteInstallDir), config.nginx.siteName);
78 const includeRoot = resolve(bundleDir, stripLeadingSlash(config.nginx.includeDir));
79 const commonProxyPath = resolve(includeRoot, "common-proxy.conf");
80 const deployScriptPath = resolve(bundleDir, "deploy-on-vps.sh");
81 const summaryPath = resolve(bundleDir, "inventory-summary.json");
82 const deployCommandsPath = resolve(bundleDir, "DEPLOY_COMMANDS.txt");
83
84 mkdirSync(resolve(siteTargetPath, ".."), { recursive: true });
85 mkdirSync(includeRoot, { recursive: true });
86
87 writeFileSync(siteTargetPath, rendered.siteConf, "utf8");
88 writeFileSync(commonProxyPath, rendered.commonProxy, "utf8");
89 writeFileSync(summaryPath, `${JSON.stringify(buildSummary(config), null, 2)}\n`, "utf8");
90 writeFileSync(deployCommandsPath, `${buildDeployCommands(bundleDir)}\n`, "utf8");
91 writeFileSync(deployScriptPath, buildDeployScript(config), "utf8");
92 chmodSync(deployScriptPath, 0o755);
93}
94
95function buildSummary(config) {
96 let desiredDnsRecords = [];
97
98 try {
99 desiredDnsRecords = buildDesiredDnsRecords(config);
100 } catch (error) {
101 desiredDnsRecords = [{ warning: error.message }];
102 }
103
104 return {
105 env_path: config.envPath,
106 public_hosts: buildHostInventory(config),
107 desired_dns_records: desiredDnsRecords,
108 tailscale: {
109 mini: `${config.tailscale.mini}:${config.tailscale.port}`,
110 },
111 nginx: config.nginx,
112 };
113}
114
115function buildDeployCommands(bundleDir) {
116 const bundleName = basename(bundleDir);
117 return [
118 "Copy the bundle to the VPS, then run:",
119 ` rsync -av ${bundleDir}/ root@YOUR_VPS:/tmp/${bundleName}/`,
120 ` ssh root@YOUR_VPS 'cd /tmp/${bundleName} && sudo ./deploy-on-vps.sh'`,
121 ` ssh root@YOUR_VPS 'cd /tmp/${bundleName} && sudo ./deploy-on-vps.sh --reload'`,
122 "",
123 "The first command installs files and runs nginx -t, but skips reload.",
124 "The second command is the explicit reload step.",
125 ].join("\n");
126}
127
128function buildDeployScript(config) {
129 const siteSource = `"$BUNDLE_ROOT/${stripLeadingSlash(config.nginx.siteInstallDir)}/${config.nginx.siteName}"`;
130 const commonProxySource = `"$BUNDLE_ROOT/${stripLeadingSlash(config.nginx.includeDir)}/common-proxy.conf"`;
131 const siteTarget = `${config.nginx.siteInstallDir}/${config.nginx.siteName}`;
132 const siteEnabledTarget = `${config.nginx.siteEnabledDir}/${config.nginx.siteName}`;
133 const commonProxyTarget = `${config.nginx.includeDir}/common-proxy.conf`;
134
135 return `#!/usr/bin/env bash
136set -euo pipefail
137
138BUNDLE_ROOT="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
139DO_RELOAD=0
140
141if [[ "\${1:-}" == "--reload" ]]; then
142 DO_RELOAD=1
143fi
144
145install -d -m 0755 ${config.nginx.siteInstallDir}
146install -d -m 0755 ${config.nginx.siteEnabledDir}
147install -d -m 0755 ${config.nginx.includeDir}
148
149install -m 0644 ${siteSource} "${siteTarget}"
150install -m 0644 ${commonProxySource} "${commonProxyTarget}"
151ln -sfn "${siteTarget}" "${siteEnabledTarget}"
152
153nginx -t
154
155if [[ "$DO_RELOAD" -eq 1 ]]; then
156 systemctl reload nginx
157else
158 echo "Installed bundle and nginx -t passed. Reload skipped."
159 echo "Run again with: sudo ./deploy-on-vps.sh --reload"
160fi
161`;
162}
163
164function printSummary(config, bundleDir, repoCheck) {
165 const lines = [];
166
167 lines.push(`Inventory: ${config.envPath}`);
168 lines.push(`Bundle: ${bundleDir}`);
169 lines.push("");
170 lines.push("Public host mapping:");
171
172 for (const host of buildHostInventory(config)) {
173 lines.push(`- ${host.hostname}: ${host.description}`);
174 }
175
176 lines.push("");
177 lines.push("Repo drift:");
178 lines.push(repoCheck.clean ? "- none" : `- mismatches: ${repoCheck.mismatches.join(", ")}`);
179 lines.push("");
180 lines.push("Next:");
181 lines.push(`- Review ${resolve(bundleDir, "inventory-summary.json")}`);
182 lines.push(`- Copy the bundle to the VPS and run ${resolve(bundleDir, "deploy-on-vps.sh")} there`);
183 lines.push("- deploy-on-vps.sh only reloads nginx when passed --reload");
184
185 console.log(lines.join("\n"));
186}
187
188function stripLeadingSlash(value) {
189 return value.replace(/^\/+/u, "");
190}
191
192try {
193 main();
194} catch (error) {
195 console.error(`nginx-sync-plan failed: ${error.message}`);
196 process.exitCode = 1;
197}