From 2c47df702e2bf5a6b508dae0dc62a92dc6d8a08b Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 6 Apr 2026 11:19:51 +0800 Subject: [PATCH] Harden deploy script SSH session flow --- scripts/deploy-server.sh | 15 ++++-- tests/deploy-server-script.test.ts | 86 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/deploy-server-script.test.ts diff --git a/scripts/deploy-server.sh b/scripts/deploy-server.sh index 39a0ff5..0c18b25 100755 --- a/scripts/deploy-server.sh +++ b/scripts/deploy-server.sh @@ -48,6 +48,15 @@ rsync -az --delete \ -e "$RSYNC_RSH" \ "$ROOT_DIR/" "$REMOTE_HOST:$REMOTE_DIR/" -"${SSH_PREFIX[@]}" "$REMOTE_HOST" "sudo bash $REMOTE_DIR/scripts/bootstrap-server.sh" -"${SSH_PREFIX[@]}" "$REMOTE_HOST" "sudo chown -R ${REMOTE_USER}:${REMOTE_USER} $REMOTE_DIR" -"${SSH_PREFIX[@]}" "$REMOTE_HOST" "cd $REMOTE_DIR && npm install --omit=dev && sudo systemctl restart boss-web && sudo systemctl restart caddy && sleep 2 && curl -fsS http://127.0.0.1:3000/api/health" +POST_SYNC_REMOTE_CMD=" +sudo bash $REMOTE_DIR/scripts/bootstrap-server.sh && +sudo chown -R ${REMOTE_USER}:${REMOTE_USER} $REMOTE_DIR && +cd $REMOTE_DIR && +npm install --omit=dev && +sudo systemctl restart boss-web && +sudo systemctl restart caddy && +sleep 2 && +curl -fsS http://127.0.0.1:3000/api/health +" + +"${SSH_PREFIX[@]}" "$REMOTE_HOST" "$POST_SYNC_REMOTE_CMD" diff --git a/tests/deploy-server-script.test.ts b/tests/deploy-server-script.test.ts new file mode 100644 index 0000000..378c572 --- /dev/null +++ b/tests/deploy-server-script.test.ts @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +async function writeExecutable(filePath: string, contents: string) { + await writeFile(filePath, contents, "utf8"); + await chmod(filePath, 0o755); +} + +test("deploy-server merges post-rsync remote work into a single ssh session", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "boss-deploy-script-")); + const binDir = path.join(tempRoot, "bin"); + const logDir = path.join(tempRoot, "logs"); + const repoRoot = "/Users/kris/code/boss"; + + await mkdir(binDir, { recursive: true }); + await mkdir(logDir, { recursive: true }); + + await writeExecutable( + path.join(binDir, "security"), + `#!/bin/sh +printf 'Asd123456.' +`, + ); + await writeExecutable( + path.join(binDir, "npm"), + `#!/bin/sh +printf '%s\\0' "$*" >> "${path.join(logDir, "npm.log")}" +exit 0 +`, + ); + await writeExecutable( + path.join(binDir, "rsync"), + `#!/bin/sh +printf '%s\\0' "$*" >> "${path.join(logDir, "rsync.log")}" +exit 0 +`, + ); + await writeExecutable( + path.join(binDir, "ssh"), + `#!/bin/sh +printf '%s\\0' "$*" >> "${path.join(logDir, "ssh.log")}" +exit 0 +`, + ); + await writeExecutable( + path.join(binDir, "sshpass"), + `#!/bin/sh +if [ "$1" = "-e" ]; then + shift +fi +exec "$@" +`, + ); + + try { + await execFileAsync("zsh", [path.join(repoRoot, "scripts/deploy-server.sh")], { + cwd: repoRoot, + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH ?? ""}`, + }, + }); + + const sshLog = await readFile(path.join(logDir, "ssh.log"), "utf8"); + const sshCalls = sshLog + .split("\0") + .map((line) => line.trim()) + .filter(Boolean); + + assert.equal(sshCalls.length, 2); + assert.match(sshCalls[0] ?? "", /sudo mkdir -p \/opt\/boss/); + assert.match(sshCalls[1] ?? "", /bootstrap-server\.sh/); + assert.match(sshCalls[1] ?? "", /npm install --omit=dev/); + assert.match(sshCalls[1] ?? "", /systemctl restart boss-web/); + assert.match(sshCalls[1] ?? "", /curl -fsS http:\/\/127\.0\.0\.1:3000\/api\/health/); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +});