baa-conductor


baa-conductor / scripts / ops
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}