#!/usr/bin/env bash # 배포 — 서버에서 실행 계정으로 실행: sudo -u env SOURCE_BUNDLE=<번들> /repo/deploy/deploy.sh # 1. 소스 갱신(번들 또는 원격) 2. 테스트 3. 릴리스 복사 4. DB 백업·마이그레이션 5. 서비스 재시작(실패 시 이전 릴리스로 복귀) # 사이트(서비스 이름·계정·경로·포트)는 /site.env(setup.sh 가 둠)에서 읽습니다. 없으면 arihakdang 설정으로 동작합니다(0.8.17 까지의 서버 호환). set -euo pipefail . "$(dirname "$0")/lib.sh" APP="$(cd "$DEPLOY_DIR/../.." && pwd)" # /repo/deploy → 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= 를 주면 원격 자격 증명 없이 번들에서 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)" echo "▶ 테스트"; for t in gateway imap smtp config provision domains webmaker usage ariweb; do "$NODE_BIN" "test/$t.test.mjs"; done 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