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