Files
web-page/scripts/deploy-nas.cjs
gahusb 11e4f00ae6 fix(deploy): rsync SSH에 -i 키파일 명시 (macOS Keychain 우회)
macOS에서 rsync 서브프로세스는 Keychain 키를 자동 로드하지 못해
비밀번호 프롬프트로 fallback됨. -i ~/.ssh/id_rsa 명시로 해결.
- BatchMode=yes: 비밀번호 프롬프트 차단 (명확한 에러 반환)
- StrictHostKeyChecking=accept-new: 최초 연결 host key 자동 수락
- NAS_SSH_KEY 환경변수로 다른 키 파일 지정 가능

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:35:56 +09:00

153 lines
5.9 KiB
JavaScript

const { execSync } = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
// Load .env.local from project root if present (persists NAS_SSH_TARGET etc.)
const envLocalPath = path.join(__dirname, "..", ".env.local");
if (fs.existsSync(envLocalPath)) {
for (const line of fs.readFileSync(envLocalPath, "utf8").split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const idx = trimmed.indexOf("=");
if (idx < 0) continue;
const k = trimmed.slice(0, idx).trim();
const v = trimmed.slice(idx + 1).trim();
if (!(k in process.env)) process.env[k] = v;
}
}
const isWin = process.platform === "win32";
const isMac = process.platform === "darwin";
const src = "dist";
const dstWin = "Z:\\docker\\webpage\\frontend\\";
const dstMac = "/Volumes/gahusb.synology.me/docker/webpage/frontend/";
const dst = isWin ? dstWin : dstMac;
if (!fs.existsSync(src)) {
console.error("dist not found. Run build first.");
process.exit(1);
}
if (isWin) {
const cmd =
'powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference=\\"Stop\\"; $src=\\"dist\\"; $dst=\\"Z:\\\\docker\\\\webpage\\\\frontend\\\\\\"; if(!(Test-Path $src)){ throw \\"dist not found. Run build first.\\" }; if(!(Test-Path $dst)){ throw \\"NAS drive not found. Check Z: mapping.\\" }; $log = Join-Path (Get-Location) \\"robocopy.log\\"; robocopy $src $dst /MIR /R:1 /W:1 /E /NFL /NDL /NP /V /TEE /LOG:$log; $rc = $LASTEXITCODE; if($rc -ge 8){ Write-Host \\"robocopy failed with code $rc. See $log\\"; exit $rc } else { exit 0 }"';
execSync(cmd, { stdio: "inherit" });
} else if (isMac) {
const sshTarget = process.env.NAS_SSH_TARGET;
const sshPath =
process.env.NAS_SSH_PATH || "/volume1/docker/webpage/frontend/";
const sshPort = process.env.NAS_SSH_PORT;
// SSH 경로: NAS_SSH_TARGET이 설정된 경우 항상 우선
if (sshTarget) {
// 제어문자·줄바꿈 제거 (잘못된 export/copy-paste 대비)
const cleanTarget = sshTarget.replace(/[\r\n\t]/g, "").trim();
const cleanPath = sshPath.replace(/[\r\n\t]/g, "").trim();
const cleanPort = sshPort ? sshPort.replace(/\D/g, "").trim() : "";
if (!cleanTarget) {
console.error("NAS_SSH_TARGET 값이 비어있습니다. .env.local 또는 환경변수를 확인하세요.");
printSshHint();
process.exit(1);
}
if (cleanPort && !/^\d{1,5}$/.test(cleanPort)) {
console.error(`NAS_SSH_PORT 값이 잘못됐습니다: "${sshPort}" → 숫자만 입력하세요.`);
process.exit(1);
}
// macOS Keychain은 서브프로세스(rsync)에서 SSH 키를 자동 로드하지 못함 → -i 명시
const keyFile = process.env.NAS_SSH_KEY || path.join(os.homedir(), ".ssh", "id_rsa");
const portOpt = cleanPort ? `-p ${cleanPort}` : "";
const sshCmd = `ssh ${portOpt} -i "${keyFile}" -o StrictHostKeyChecking=accept-new -o BatchMode=yes`.trim();
console.log(`Deploying via SSH → ${cleanTarget}:${cleanPath}`);
console.log(`SSH command: ${sshCmd}`);
execSync(
`rsync -r --delete --delete-delay -e "${sshCmd}" ${src}/ ${cleanTarget}:${cleanPath}`,
{ stdio: "inherit" }
);
process.exit(0);
}
// SMB 마운트 경로 fallback
if (!fs.existsSync(dst)) {
console.error("NAS path not found: " + dst);
printSshHint();
process.exit(1);
}
if (!dst.includes("docker/webpage/frontend")) {
console.error("Safety check failed: unexpected dst path: " + dst);
process.exit(1);
}
try {
const testPath = `${dst}.deploy-write-test`;
fs.writeFileSync(testPath, "ok");
fs.unlinkSync(testPath);
} catch (err) {
console.error("NAS write test failed (EIO / permission error).");
console.error(
"macOS SMB → Synology 쓰기 실패는 흔한 이슈입니다. SSH 배포를 사용하세요.\n"
);
printSshHint();
process.exit(1);
}
const sleep = (ms) =>
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
const retry = (fn, attempts = 6) => {
for (let i = 0; i < attempts; i += 1) {
try {
fn();
return;
} catch (err) {
if (i === attempts - 1) throw err;
sleep(150);
}
}
};
const removeTree = (target) => {
const stat = fs.lstatSync(target);
if (stat.isDirectory()) {
retry(() => {
const entries = fs.readdirSync(target);
for (const entry of entries) {
removeTree(`${target}/${entry}`);
}
});
retry(() => fs.rmdirSync(target));
} else {
retry(() => fs.unlinkSync(target));
}
};
if (process.env.NAS_CLEAN === "1") {
try {
const entries = fs.readdirSync(dst);
for (const entry of entries) {
removeTree(`${dst}${entry}`);
}
} catch (err) {
console.warn("Clean skipped due to NAS lock:", err?.message ?? err);
}
}
execSync(`ditto ${src} ${dst}`, { stdio: "inherit" });
} else {
const baseArgs = ["rsync", "-r", "--delete", "--delete-delay", "-t"];
const cmd = `${baseArgs.join(" ")} ${src}/ ${dst}`;
execSync(cmd, { stdio: "inherit" });
}
function printSshHint() {
console.error("──────────────────────────────────────────────────");
console.error("SSH 배포 설정 방법:");
console.error(" 프로젝트 루트에 .env.local 파일을 만들고 아래 내용을 입력하세요:");
console.error("");
console.error(" NAS_SSH_TARGET=<NAS_유저명>@gahusb.synology.me");
console.error(" NAS_SSH_PORT=<SSH_포트> # 기본 22, DSM에서 확인");
console.error("");
console.error(" 이후 npm run release:nas 를 다시 실행하면 rsync over SSH로 배포됩니다.");
console.error("──────────────────────────────────────────────────");
}