fix(deploy): Mac SSH 배포 지원 + .env.local 자동 로드

- .env.local 파일에서 NAS_SSH_TARGET 등 SSH 설정 자동 로드
- NAS_SSH_TARGET 설정 시 SMB 마운트보다 SSH 우선 사용
- SMB 쓰기 실패(EIO) 시 스택트레이스 대신 SSH 설정 안내 메시지 출력
- .env.local을 .gitignore에 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 09:21:32 +09:00
parent 7cbdbe6e8b
commit f6d95264c3
2 changed files with 46 additions and 12 deletions

View File

@@ -1,5 +1,20 @@
const { execSync } = require("child_process");
const fs = require("fs");
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";
@@ -12,10 +27,6 @@ if (!fs.existsSync(src)) {
console.error("dist not found. Run build first.");
process.exit(1);
}
if (!fs.existsSync(dst)) {
console.error("NAS path not found. Check mount: " + dst);
process.exit(1);
}
if (isWin) {
const cmd =
@@ -26,33 +37,43 @@ if (isWin) {
const sshPath =
process.env.NAS_SSH_PATH || "/volume1/docker/webpage/frontend/";
const sshPort = process.env.NAS_SSH_PORT;
// SSH 경로: NAS_SSH_TARGET이 설정된 경우 항상 우선
if (sshTarget) {
console.log(`Deploying via SSH → ${sshTarget}:${sshPath}`);
const sshCmd = sshPort ? `ssh -p ${sshPort}` : "ssh";
execSync(
`rsync -r --delete --delete-delay -e \"${sshCmd}\" ${src}/ ${sshTarget}:${sshPath}`,
`rsync -r --delete --delete-delay -e "${sshCmd}" ${src}/ ${sshTarget}:${sshPath}`,
{ stdio: "inherit" }
);
process.exit(0);
}
// rsync on macOS + SMB/NAS can be flaky; use ditto after a safe clean.
// 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(
"NAS write test failed. Files may be locked or permissions are read-only."
"macOS SMB → Synology 쓰기 실패는 흔한 이슈입니다. SSH 배포를 사용하세요.\n"
);
console.error(
"Try stopping services using the folder, remounting the share with write access,",
"or set NAS_SSH_TARGET to deploy over SSH instead."
);
throw err;
printSshHint();
process.exit(1);
}
const sleep = (ms) =>
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
const retry = (fn, attempts = 6) => {
@@ -96,3 +117,15 @@ if (isWin) {
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("──────────────────────────────────────────────────");
}