Files
cozsweet-frontend-nextjs/.githooks/post-receive
T
2026-06-18 17:36:45 +08:00

232 lines
7.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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=""
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
;;
test|dev)
START_PORT=9135
;;
*)
START_PORT=9135
;;
esac
export START_PORT
echo "=== start port: $START_PORT ===" >> "$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
}
launch_next() {
echo "=== start LAUNCH port=$START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
PORT="$START_PORT" nohup pnpm run start >> "$LOG_FILE" 2>&1 &
START_PID=$!
echo "=== start PID=$START_PID @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
}
# ============================================================
# 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_next; then
exit 1 # 端口未释放 → 不启动新服务,避免 EADDRINUSE
fi
launch_next # 再启新
}
main "$@"
exit 0