2026-10-26 11:02:58 +00:00
#!/usr/bin/env bash
# 배포 — 서버에서 실행 계정으로 실행: sudo -u <APP_USER> env SOURCE_BUNDLE=<번들> <APP_DIR>/repo/deploy/deploy.sh
# 1. 소스 갱신(번들 또는 원격) 2. 테스트 3. 릴리스 복사 4. DB 백업·마이그레이션 5. 서비스 재시작(실패 시 이전 릴리스로 복귀)
# 사이트(서비스 이름·계정·경로·포트)는 <APP_DIR>/site.env(setup.sh 가 둠)에서 읽습니다. 없으면 arihakdang 설정으로 동작합니다(0.8.17 까지의 서버 호환).
set -euo pipefail
. " $( dirname " $0 " ) /lib.sh"
APP = " $( cd " $DEPLOY_DIR /../.." && pwd ) " # <APP_DIR>/repo/deploy → <APP_DIR>
if [ -f " $APP /site.env" ] ; then load_site " $APP /site.env" ; else load_site " $DEPLOY_DIR /sites/arihakdang.env" ; fi
APP = " $APP_DIR " ; REPO = " $APP /repo" ; RELEASES = " $APP /releases"
[ -x " $NODE_BIN " ] || NODE_BIN = " $( command -v node) "
MAJOR = " $( $NODE_BIN --version | sed -E 's/^v([0-9]+).*/\1/' ) " ; [ " $MAJOR " -ge 22 ] || { echo "Node 22.13+ 필요: $( $NODE_BIN --version) " >& 2; exit 1; }
export PATH = " $( dirname " $NODE_BIN " ) : $PATH "
STAMP = " $( date +%Y%m%d-%H%M%S) " ; TARGET = " $RELEASES / $STAMP "
exec 9>" $APP /deploy.lock" ; flock -n 9 || { echo '다른 배포가 진행 중입니다' >& 2; exit 1; }
echo "▶ 소스 갱신 ( $SITE : $MAIL_HOST )" ; cd " $REPO "
# SOURCE_BUNDLE=<git bundle 경로> 를 주면 원격 자격 증명 없이 번들에서 main 을 받습니다(작업 PC 에서 scp 로 전달).
# 번들의 main 이 서버가 아는 origin/main 과 이어지지 않아도(릴리스 역사를 다시 쓴 경우) 받도록 강제(+) 갱신합니다. 어차피 아래에서 origin/main 으로 reset 합니다.
if [ -n " ${ SOURCE_BUNDLE :- } " ] ; then git bundle verify " $SOURCE_BUNDLE " >/dev/null 2>& 1; git fetch --quiet --force " $SOURCE_BUNDLE " +main:refs/remotes/origin/main; else git fetch --quiet origin; fi
git reset --hard --quiet origin/main; echo " $( git log --oneline -1) "
2026-10-26 11:47:45 +00:00
echo "▶ 테스트" ; for t in gateway imap smtp config provision domains webmaker usage ariweb; do " $NODE_BIN " "test/ $t .test.mjs" ; done
2026-10-26 11:02:58 +00:00
echo "▶ 릴리스 배치" ; mkdir -p " $TARGET "
for path in server web lang migrations package.json haraka; do cp -r " $REPO / $path " " $TARGET / $path " ; done
git rev-parse HEAD > " $TARGET /REVISION"
DB = " $APP /data/mail-gateway.sqlite" ; mkdir -p " $APP /data/backups"
if [ -f " $DB " ] ; then python3 - " $DB " " $APP /data/backups/pre- $STAMP .sqlite" <<'PY'
import sqlite3,sys
s=sqlite3.connect('file:'+sys.argv[1]+'?mode=ro',uri=True);b=sqlite3.connect(sys.argv[2]);s.backup(b);b.close();s.close()
PY
fi
DB_FILE = " $DB " " $NODE_BIN " " $TARGET /server/db-migrate.mjs"
# 첫 배포에는 current 가 없습니다. -e 는 존재하는 경로만 돌려주므로 자기 자신을 가리키는 링크를 만들지 않습니다.
PREVIOUS = " $( readlink -e " $APP /current" 2>/dev/null || true ) "
rollback() { trap - ERR; if [ -n " $PREVIOUS " ] && [ -d " $PREVIOUS " ] && [ " $PREVIOUS " != " $TARGET " ] ; then ln -sfn " $PREVIOUS " " $APP /current.rollback" ; mv -Tf " $APP /current.rollback" " $APP /current" ; sudo systemctl restart " $APP_NAME " ; fi ; echo "배포 실패 — 이전 릴리스로 복귀" >& 2; }
trap rollback ERR
ln -sfn " $TARGET " " $APP /current.tmp" ; mv -Tf " $APP /current.tmp" " $APP /current" ; echo " current → $TARGET "
# sync: 릴리스 파일이 디스크에 완전히 쓰인 뒤 재시작합니다(1.0.5 — 0.9.7 배포 때 첫 시작이 빈 package.json 을 읽은 기록).
sync
echo "▶ 서비스 재시작" ; sudo systemctl restart " $APP_NAME " ; sleep 2
systemctl is-active --quiet " $APP_NAME " || { rollback; exit 1; }
curl -fsS "http://127.0.0.1: $PORT /api/health" >/dev/null && echo " health OK"
trap - ERR
ls -1dt " $RELEASES " /*/ 2>/dev/null | tail -n +6 | xargs -r rm -rf
# 내장 Haraka(1.0.0): 관리 서비스가 등록되어 있으면 새 릴리스의 run.mjs 로 다시 띄웁니다(sudoers 에 없으면 건너뜀 — haraka-apply.sh 가 다음 실행에서 맞춤).
# 의존성 설치는 root 가 하는 haraka-apply.sh 의 몫이지만, 이미 설치된 디렉터리가 있고 package.json 이 바뀌었으면 여기서 실행 계정으로 갱신을 시도합니다(실패해도 배포는 성공).
if [ -f " $APP /haraka/package.json" ] && ! cmp -s " $REPO /haraka/package.json" " $APP /haraka/package.json" ; then
NPM_BIN = " $( dirname " $NODE_BIN " ) /npm" ; [ -x " $NPM_BIN " ] || NPM_BIN = " $( command -v npm || true ) "
if [ -x " $NPM_BIN " ] ; then cp " $REPO /haraka/package.json" " $APP /haraka/package.json" ; ( cd " $APP /haraka" && HOME = " $APP /haraka" " $NPM_BIN " install --omit= dev --no-audit --no-fund --no-package-lock --loglevel= error) || echo " ⚠️ Haraka 의존성 갱신 실패(인터넷?) — sudo bash $REPO /deploy/haraka-apply.sh 로 다시 시도" >& 2; fi
fi
if systemctl is-enabled --quiet " $APP_NAME -haraka" 2>/dev/null; then sudo -n systemctl restart " $APP_NAME -haraka" 2>/dev/null && echo " Haraka 관리 서비스 재시작" || echo " (Haraka 관리 서비스는 root 가 재시작해야 합니다: sudo systemctl restart $APP_NAME -haraka)" ; fi
echo "✔ 배포 완료: $STAMP "
# 초기 설정 페이지(1.0.0): 서버가 시작하며 데이터 디렉터리에 접속 주소(토큰 포함)를 남깁니다. 설치 직후 운영 설정(Resend 키 등)을 여기서 채웁니다.
sleep 1; if [ -f " $APP /data/setup-url.txt" ] ; then echo "▶ 초기 설정 페이지(토큰 포함 — 공유하지 마세요):" ; sed 's/^/ /' " $APP /data/setup-url.txt" ; echo " 또는 https:// $MAIL_HOST /admin → 메일 서비스 설정" ; fi