#!/bin/sh # Post-receive hook: push 后自动 build + start # # 触发场景: # - 非 bare repo + receive.denyCurrentBranch=updateInstead:本地推送自动部署 # # 用途:执行 package.json 中的 `build` + `start` 脚本 # - 实际调用 `pnpm run build`(输出丢,仅记退出码) # - 再后台启 `pnpm run start`(nohup,输出丢,hook 立即返回) # # 设计:逻辑块**封**装为独立函数,main 调**度**(shell 风格) # - **全**局变量:LOG_FILE(在 init_log_file **里**定**义**)、CURRENT_BRANCH # - **函**数**间**通过 `$LOG_FILE` / `$BUILD_EXIT` 通信 # - **不**用 set -e —— 各步**自**己处理错**误**(env 不**存**在 / build 失败 / 旧**进**程**不**存**在)→ 让 deploy 错**误****不**挡 git push # # 关键前置(**解**决 push 后 hook **不**触**发** + pnpm **找**不**到**): # 1) cd 到 repo **顶**层 —— git-receive-pack 调 hook 时 CWD **未**必 = repo 顶 # 2) 加**载** nvm —— 非交互 shell **不**会 source .bashrc,pnpm **不**在 PATH # 3) **验**证 PATH —— 写日**志**确认 pnpm 可**用**(**不**可用则 build **必**失**败**) # ---- 前置:切到 repo 顶 + 加 pnpm 到 PATH ---- # 重要:不能**用** `git rev-parse --show-toplevel` 获**取** repo **顶**层 # —— git-receive-pack 调 hook 时 CWD **可**能是 `.git/` 内**部**, # 这种情况下 `--show-toplevel` **报**错 `must be run in a work tree`。 # 解**法**:**直**接**根**据 hook **本**身路径推**算** repo **顶**层。 SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "")" if [ -z "$SCRIPT_DIR" ]; then # 兜底:**用** git rev-parse(如**果**可**用**) REPO_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || true)" if [ -z "$REPO_TOPLEVEL" ]; then # **最**后**兜**底:**直**接**用** PWD REPO_TOPLEVEL="$PWD" fi else # .githooks/post-receive 的**父**目录**的****父**目录 = repo **顶** REPO_TOPLEVEL="$(cd "$SCRIPT_DIR/.." && pwd)" fi cd "$REPO_TOPLEVEL" || { echo "FATAL: cannot cd to $REPO_TOPLEVEL" >&2; exit 1; } # 加**载** nvm(**非**交互 shell **不**自动 source .bashrc) # nvm 是 POSIX **兼**容,但**有**个**问**题:它**会** [ -n "$BASH_VERSION" ] **检**查, # 在 dash **下** nvm 函**数**注**册**失败(pnpm **仍**然**找**不**到**)。 # 所以策略是:尝**试** source nvm,**如**果失**败**则**直**接**找** pnpm **可执**行文件路径。 PNPM_BIN="$(command -v pnpm 2>/dev/null || true)" if [ -z "$PNPM_BIN" ] && [ -s "$HOME/.nvm/nvm.sh" ]; then \. "$HOME/.nvm/nvm.sh" 2>/dev/null || true PNPM_BIN="$(command -v pnpm 2>/dev/null || true)" fi # 兜底:**直**接**搜**索 **常**见 nvm 路**径** if [ -z "$PNPM_BIN" ]; then for candidate in /root/.nvm/versions/node/*/bin/pnpm "$HOME/.local/share/pnpm/pnpm" /usr/local/bin/pnpm; do if [ -x "$candidate" ]; then PNPM_BIN="$candidate" break fi done fi if [ -n "$PNPM_BIN" ]; then export PATH="$(dirname "$PNPM_BIN"):$PATH" fi # ============================================================ # 1. init_log_file —— 定**义** LOG_FILE + 创**建** logs/ 目**录** # ============================================================ init_log_file() { LOG_FILE="$(git rev-parse --show-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() { echo "=== start LAUNCH @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" nohup pnpm run start > /dev/null 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 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