275 lines
8.7 KiB
Bash
Executable File
275 lines
8.7 KiB
Bash
Executable File
#!/bin/sh
|
||
# Post-receive hook: push 后自动 build + start
|
||
|
||
# pnpm 环境变量
|
||
export NVM_DIR="$HOME/.nvm"
|
||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||
# pnpm
|
||
export PNPM_HOME="/root/.local/share/pnpm"
|
||
case ":$PATH:" in
|
||
*":$PNPM_HOME:"*) ;;
|
||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||
esac
|
||
# pnpm end
|
||
|
||
# 全局变量统一声明。具体值由各步骤函数写入。
|
||
REPO_TOPLEVEL=""
|
||
LOG_FILE=""
|
||
CURRENT_BRANCH=""
|
||
START_PORT=""
|
||
PM2_APP_NAME=""
|
||
BUILD_EXIT=0
|
||
STOP_WAIT_SECONDS=3
|
||
STOP_VERIFY_RETRIES=3
|
||
|
||
resolve_repo_context() {
|
||
GIT_DIR_ABS="$(git rev-parse --absolute-git-dir 2>/dev/null)"
|
||
if [ -n "$GIT_DIR_ABS" ] && [ -d "$GIT_DIR_ABS" ]; then
|
||
REPO_TOPLEVEL="$(cd "$GIT_DIR_ABS/.." && pwd)"
|
||
export GIT_DIR="$GIT_DIR_ABS"
|
||
else
|
||
REPO_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
|
||
fi
|
||
unset GIT_WORK_TREE
|
||
cd "$REPO_TOPLEVEL" || { echo "FATAL: cannot cd to $REPO_TOPLEVEL" >&2; exit 1; }
|
||
echo "=== REPO_TOPLEVEL=$REPO_TOPLEVEL (GIT_DIR=$GIT_DIR) ===" >&2
|
||
}
|
||
|
||
init_log_file() {
|
||
LOG_FILE="$REPO_TOPLEVEL/logs/post-receive.log"
|
||
mkdir -p "$(dirname "$LOG_FILE")"
|
||
}
|
||
|
||
write_metadata_header() {
|
||
{
|
||
echo "=== post-receive @ $(date +%Y-%m-%dT%H:%M:%S%z) ==="
|
||
echo "branch: $(git rev-parse --abbrev-ref HEAD)"
|
||
echo "commit: $(git rev-parse --short HEAD)"
|
||
echo "cwd: $(pwd)"
|
||
echo ""
|
||
} > "$LOG_FILE"
|
||
}
|
||
|
||
sync_worktree() {
|
||
echo "=== sync_worktree: git reset --hard HEAD @ $(date +%Y-%m-%dT%H:%M:%S%z) ==="
|
||
git reset --hard HEAD
|
||
}
|
||
|
||
copy_env_by_branch() {
|
||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||
echo "=== branch: $CURRENT_BRANCH ===" >> "$LOG_FILE"
|
||
case "$CURRENT_BRANCH" in
|
||
main)
|
||
cp -f env-example/.env.production.example .env.production
|
||
echo "=== env: copied .env.production (main → prod) ===" >> "$LOG_FILE"
|
||
;;
|
||
test)
|
||
cp -f env-example/.env.local.example .env.local
|
||
echo "=== env: copied .env.local (test → test) ===" >> "$LOG_FILE"
|
||
;;
|
||
dev)
|
||
cp -f env-example/.env.development.example .env.local
|
||
echo "=== env: copied .env.local (dev → dev) ===" >> "$LOG_FILE"
|
||
;;
|
||
*)
|
||
echo "=== env: UNKNOWN branch '$CURRENT_BRANCH', skip env copy ===" >> "$LOG_FILE"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================
|
||
# 4. select_port_by_branch —— 根据分支选择 next start 端口
|
||
# main 分支 → 9185
|
||
# test 分支 → 9135
|
||
# dev 分支 → 9135
|
||
# ============================================================
|
||
select_port_by_branch() {
|
||
case "$CURRENT_BRANCH" in
|
||
main)
|
||
START_PORT=9185
|
||
PM2_APP_NAME="cozsweet-main"
|
||
;;
|
||
test|dev)
|
||
START_PORT=9135
|
||
PM2_APP_NAME="cozsweet-$CURRENT_BRANCH"
|
||
;;
|
||
*)
|
||
START_PORT=9135
|
||
PM2_APP_NAME="cozsweet-$CURRENT_BRANCH"
|
||
;;
|
||
esac
|
||
export START_PORT
|
||
export PM2_APP_NAME
|
||
echo "=== start port: $START_PORT ===" >> "$LOG_FILE"
|
||
echo "=== pm2 app: $PM2_APP_NAME ===" >> "$LOG_FILE"
|
||
}
|
||
|
||
# ============================================================
|
||
# 5. run_build —— 跑 `pnpm install` + `pnpm run build`,输出丢,捕获退出码写日志
|
||
# 返回:build 退出码(main 用 if ! run_build 决定是否继续)
|
||
# ============================================================
|
||
run_build() {
|
||
echo "=== build START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
pnpm run build:deploy > /dev/null 2>&1
|
||
BUILD_EXIT=$?
|
||
echo "=== build EXIT_CODE=$BUILD_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
echo "" >> "$LOG_FILE"
|
||
return $BUILD_EXIT
|
||
}
|
||
|
||
# ============================================================
|
||
# 6. stop_existing_next —— 强杀当前分支端口上的旧进程
|
||
# 1) 使用 ss 查找占用 START_PORT 的进程
|
||
# 2) 直接 SIGKILL,避免旧服务 drain 太久导致新服务无法绑定端口
|
||
# 3) 等待 STOP_WAIT_SECONDS,让系统释放端口
|
||
# 4) 循环复查端口占用并再次强杀残留
|
||
# ============================================================
|
||
find_port_pids() {
|
||
if ! command -v ss >/dev/null 2>&1; then
|
||
echo "=== ss command not found; cannot inspect port $START_PORT ===" >> "$LOG_FILE"
|
||
return 0
|
||
fi
|
||
|
||
ss -tulnp 2>/dev/null \
|
||
| awk -v port=":$START_PORT" '$5 ~ port "$" { print }' \
|
||
| sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' \
|
||
| sort -u
|
||
}
|
||
|
||
force_kill_pids() {
|
||
PIDS_TO_KILL="$1"
|
||
if [ -z "$PIDS_TO_KILL" ]; then
|
||
return 0
|
||
fi
|
||
echo "=== force kill PIDS: $PIDS_TO_KILL ===" >> "$LOG_FILE"
|
||
kill -9 $PIDS_TO_KILL 2>/dev/null || true
|
||
}
|
||
|
||
wait_for_port_release() {
|
||
RETRY=0
|
||
while [ "$RETRY" -lt "$STOP_VERIFY_RETRIES" ]; do
|
||
REMAINING_PIDS="$(find_port_pids)"
|
||
if [ -z "$REMAINING_PIDS" ]; then
|
||
echo "=== port $START_PORT released ===" >> "$LOG_FILE"
|
||
return 0
|
||
fi
|
||
|
||
echo "=== port $START_PORT still busy, retry=$RETRY, pids=$REMAINING_PIDS ===" >> "$LOG_FILE"
|
||
force_kill_pids "$REMAINING_PIDS"
|
||
sleep "$STOP_WAIT_SECONDS"
|
||
RETRY=$((RETRY + 1))
|
||
done
|
||
|
||
REMAINING_PIDS="$(find_port_pids)"
|
||
if [ -n "$REMAINING_PIDS" ]; then
|
||
echo "=== stop FAILED: port $START_PORT still occupied by PIDS: $REMAINING_PIDS ===" >> "$LOG_FILE"
|
||
return 1
|
||
fi
|
||
|
||
echo "=== port $START_PORT released ===" >> "$LOG_FILE"
|
||
return 0
|
||
}
|
||
|
||
stop_existing_next() {
|
||
echo "=== stop existing next on port $START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
PIDS="$(find_port_pids)"
|
||
|
||
if [ -n "$PIDS" ]; then
|
||
force_kill_pids "$PIDS"
|
||
else
|
||
echo "=== no process found on port $START_PORT ===" >> "$LOG_FILE"
|
||
fi
|
||
|
||
sleep "$STOP_WAIT_SECONDS"
|
||
|
||
if ! wait_for_port_release; then
|
||
echo "=== stop ABORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
echo "" >> "$LOG_FILE"
|
||
return 1
|
||
fi
|
||
|
||
echo "=== stop DONE @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
echo "" >> "$LOG_FILE"
|
||
return 0
|
||
}
|
||
|
||
stop_existing_pm2() {
|
||
echo "=== stop existing pm2 app $PM2_APP_NAME @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
|
||
if ! command -v pm2 >/dev/null 2>&1; then
|
||
echo "=== pm2 command not found; install pm2 before deployment ===" >> "$LOG_FILE"
|
||
return 1
|
||
fi
|
||
|
||
if pm2 describe "$PM2_APP_NAME" >/dev/null 2>&1; then
|
||
pm2 delete "$PM2_APP_NAME" >> "$LOG_FILE" 2>&1 || true
|
||
echo "=== pm2 app deleted: $PM2_APP_NAME ===" >> "$LOG_FILE"
|
||
else
|
||
echo "=== no pm2 app found: $PM2_APP_NAME ===" >> "$LOG_FILE"
|
||
fi
|
||
|
||
echo "" >> "$LOG_FILE"
|
||
return 0
|
||
}
|
||
|
||
launch_next() {
|
||
echo "=== pm2 LAUNCH app=$PM2_APP_NAME port=$START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
|
||
export PORT="$START_PORT"
|
||
pm2 start pnpm --name "$PM2_APP_NAME" -- run start >> "$LOG_FILE" 2>&1
|
||
PM2_EXIT=$?
|
||
echo "=== pm2 START EXIT_CODE=$PM2_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
|
||
|
||
if [ "$PM2_EXIT" -ne 0 ]; then
|
||
echo "=== pm2 START FAILED ===" >> "$LOG_FILE"
|
||
return "$PM2_EXIT"
|
||
fi
|
||
|
||
pm2 save >> "$LOG_FILE" 2>&1 || true
|
||
echo "=== realtime logs: pm2 logs $PM2_APP_NAME ===" >> "$LOG_FILE"
|
||
return 0
|
||
}
|
||
|
||
# ============================================================
|
||
# 8. log_abort —— build 失败时写结束标记(不输出 START LAUNCH)
|
||
# ============================================================
|
||
log_abort() {
|
||
echo "=== ABORTED @ $(date +%Y-%m-%dT%H:%M:%S%z) (build failed, start skipped) ===" >> "$LOG_FILE"
|
||
}
|
||
|
||
# ============================================================
|
||
# main —— 调度所有步骤
|
||
# ============================================================
|
||
main() {
|
||
resolve_repo_context
|
||
init_log_file
|
||
write_metadata_header # 先覆盖写元信息(这样 env copy 日志能保留在后面)
|
||
|
||
sync_worktree # 先同步工作树到新 HEAD(与 git config denyCurrentBranch 配合)
|
||
copy_env_by_branch # 追加 env copy 日志
|
||
|
||
select_port_by_branch # 根据当前分支决定 next start 端口
|
||
|
||
if ! run_build; then
|
||
log_abort
|
||
exit $BUILD_EXIT # build 失败 → 不启 start(避免用旧 .next 跑新代码)
|
||
fi
|
||
|
||
if ! stop_existing_pm2; then
|
||
exit 1 # pm2 不可用 → 不启动新服务
|
||
fi
|
||
|
||
# if ! stop_existing_next; then
|
||
# exit 1 # 端口未释放 → 不启动新服务,避免 EADDRINUSE
|
||
# fi
|
||
|
||
if ! launch_next; then
|
||
exit 1 # pm2 启动失败
|
||
fi
|
||
}
|
||
|
||
main "$@"
|
||
|
||
exit 0
|