#!/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 # git 调钩子时 cwd=GIT_DIR 并设了 GIT_DIR=.(相对)。此时: # 1. `git rev-parse --show-toplevel` 返回 `/.../repo/.git`(不报错), # 会让原版 `|| echo $PWD` 退路不触发、cd 误进 .git。 # 2. cd 到工作树后,GIT_DIR=. 不再指向 .git,下面 `git reset` 之类的 # 会报 `fatal: not a git repository: '.'`。 # 修正:拿绝对 git-dir,算出 worktree 根,并改用绝对 GIT_DIR。 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 # ============================================================ # 0. sync_worktree —— 将工作树同步到新 push 的 HEAD # 背景:服务器 .git/config 设了 receive.denyCurrentBranch=updateInstead 时 # git 自动更新工作树但会跳过此钩子;改为 false 后 # git 不会更新工作树,手动 reset --hard 同步才能让下面 # 的 build 用上新代码。 # ============================================================ sync_worktree() { echo "=== sync_worktree: git reset --hard HEAD @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" git reset --hard HEAD } # ============================================================ # 1. init_log_file —— 定义 LOG_FILE + 创建 logs/ 目录 # ============================================================ init_log_file() { LOG_FILE="$REPO_TOPLEVEL/logs/post-receive.log" mkdir -p "$(dirname "$LOG_FILE")" } # ============================================================ # 2. write_metadata_header —— 覆盖写入元信息(首次写 → 覆盖让日志开头是这个) # ============================================================ write_metadata_header() { { echo "=== post-receive @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" echo "branch: $(git rev-parse --abbrev-ref HEAD)" echo "commit: $(git rev-parse --short HEAD)" echo "cwd: $(pwd)" echo "" } > "$LOG_FILE" } # ============================================================ # 3. copy_env_by_branch —— 根据分支选 env-example → .env # main 分支 → .env.production(prod 部署) # test 分支 → .env.local(test 不碰 .env.production,避免读到 prod 域名) # dev 分支 → .env.local(未配远端用途,但保留) # ============================================================ 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. run_build —— 跑 `pnpm run build`,输出丢,捕获退出码写日志 # 返回:build 退出码(main 用 if ! run_build 决定是否继续) # ============================================================ run_build() { echo "=== build START @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" pnpm run build > /dev/null 2>&1 BUILD_EXIT=$? echo "=== build EXIT_CODE=$BUILD_EXIT @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" return $BUILD_EXIT } # ============================================================ # 5. stop_existing_next —— 两阶段 kill + 等 1s 确保端口完全释放 # 1) 优雅关闭(SIGTERM)—— 旧进程自己 drain # 2) 等 2s # 3) 强杀(SIGKILL)—— 还在的话 # 4) 等 1s # ============================================================ stop_existing_next() { echo "=== stop existing next @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" pkill -f "next start" 2>/dev/null pkill -f "next-server" 2>/dev/null sleep 2 pkill -9 -f "next start" 2>/dev/null pkill -9 -f "next-server" 2>/dev/null sleep 1 echo "=== stop DONE @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" } # ============================================================ # 6. launch_next —— 后台启 `pnpm run start`,捕 PID # ============================================================ launch_next() { # next stdout/stderr 接到 logs/next-server.log(覆盖模式,每次 deploy 重新开始) NEXT_LOG_FILE="$REPO_TOPLEVEL/logs/next-server.log" echo "=== start LAUNCH @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" nohup pnpm run start > "$NEXT_LOG_FILE" 2>&1 & START_PID=$! echo "=== start PID=$START_PID @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" } # ============================================================ # 7. log_abort —— build 失败时写结束标记(不输出 START LAUNCH) # ============================================================ log_abort() { echo "=== ABORTED @ $(date -u +%Y-%m-%dT%H:%M:%SZ) (build failed, start skipped) ===" >> "$LOG_FILE" } # ============================================================ # main —— 调度所有步骤 # ============================================================ main() { init_log_file sync_worktree # 先同步工作树到新 HEAD(与 git config denyCurrentBranch 配合) write_metadata_header # 先覆盖写元信息(这样 env copy 日志能保留在后面) copy_env_by_branch # 追加 env copy 日志 if ! run_build; then log_abort exit $BUILD_EXIT # build 失败 → 不启 start(避免用旧 .next 跑新代码) fi stop_existing_next # build 成功 → 先关旧进程 launch_next # 再启新 } main "$@" exit 0