187 lines
5.2 KiB
TypeScript
187 lines
5.2 KiB
TypeScript
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);
|
|
const rsyncLog = await readFile(path.join(logDir, "rsync.log"), "utf8");
|
|
const rsyncArgs = rsyncLog
|
|
.split("\0")
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
assert.equal(sshCalls.length, 2);
|
|
assert.match(sshCalls[0] ?? "", /sudo mkdir -p \/opt\/boss/);
|
|
assert.match(rsyncArgs, /--rsync-path=sudo rsync/);
|
|
assert.match(sshCalls[1] ?? "", /bootstrap-server\.sh/);
|
|
assert.match(sshCalls[1] ?? "", /sudo chown -R ubuntu:ubuntu \/opt\/boss\/data \/opt\/boss\/public\/downloads/);
|
|
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 });
|
|
}
|
|
});
|
|
|
|
test("deploy-server falls back to remote build when local build hits ENOSPC", async () => {
|
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "boss-deploy-script-remote-build-"));
|
|
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
|
|
COUNT_FILE="${path.join(logDir, "npm-count")}"
|
|
count=0
|
|
if [ -f "$COUNT_FILE" ]; then
|
|
count="$(cat "$COUNT_FILE")"
|
|
fi
|
|
count=$((count + 1))
|
|
printf '%s' "$count" > "$COUNT_FILE"
|
|
printf '%s\\0' "$*" >> "${path.join(logDir, "npm.log")}"
|
|
if [ "$count" -eq 1 ]; then
|
|
printf 'Error: ENOSPC: no space left on device\\n' >&2
|
|
exit 1
|
|
fi
|
|
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 rsyncLog = await readFile(path.join(logDir, "rsync.log"), "utf8");
|
|
const rsyncArgs = rsyncLog
|
|
.split("\0")
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
assert.match(rsyncArgs, /--exclude \.next/);
|
|
assert.match(rsyncArgs, /--rsync-path=sudo rsync/);
|
|
|
|
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[1] ?? "", /sudo chown -R ubuntu:ubuntu \/opt\/boss\/data \/opt\/boss\/public\/downloads/);
|
|
assert.match(sshCalls[1] ?? "", /npm install && BOSS_RUNTIME_ROOT=\/opt\/boss BOSS_STATE_FILE=\/opt\/boss\/data\/boss-state\.json npm run build/);
|
|
assert.match(sshCalls[1] ?? "", /npm prune --omit=dev/);
|
|
assert.doesNotMatch(sshCalls[1] ?? "", /npm install --omit=dev/);
|
|
} finally {
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|