#!/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 # 全局变量统一声明。具体值由各步骤函数写入。 REPO_TOPLEVEL="" LOG_FILE="" CURRENT_BRANCH="" START_PORT="" BUILD_EXIT=0 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) 优雅关闭(SIGTERM)—— 旧进程自己 drain # 2) 等 2s # 3) 强杀(SIGKILL)—— 还在的话 # 4) 等 1s # ============================================================ stop_existing_next() { echo "=== stop existing next on port $START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" PIDS="" if command -v lsof >/dev/null 2>&1; then PIDS="$(lsof -ti tcp:"$START_PORT" 2>/dev/null || true)" elif command -v fuser >/dev/null 2>&1; then PIDS="$(fuser "$START_PORT"/tcp 2>/dev/null || true)" else pkill -f "next start.*$START_PORT" 2>/dev/null || true fi if [ -n "$PIDS" ]; then kill $PIDS 2>/dev/null || true fi sleep 2 if [ -n "$PIDS" ]; then kill -9 $PIDS 2>/dev/null || true fi sleep 1 echo "=== stop DONE @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" } 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 stop_existing_next # build 成功 → 先关旧进程 launch_next # 再启新 } main "$@" exit 0