From 83fdd739230eadf685521552e73c8b1c2605e902 Mon Sep 17 00:00:00 2001 From: chenhang Date: Fri, 12 Jun 2026 19:08:25 +0800 Subject: [PATCH] refactor(githooks): restructure post-receive hook into modular functions Refactor the post-receive deployment hook from a linear script into discrete functions (init_log_file, write_metadata_header, copy_env_by_branch, run_build, stop_existing_next, launch_next) for better readability, maintainability, and error isolation. Key changes: - Remove `set -e` so deploy errors don't block git push; each step handles its own failures (missing env, build failure --- .githooks/post-receive | 177 ++++++++++++++++++++++++++--------------- 1 file changed, 115 insertions(+), 62 deletions(-) diff --git a/.githooks/post-receive b/.githooks/post-receive index 11d33965..e0a4eb04 100755 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -7,74 +7,127 @@ # 用途:执行 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 -# 日志文件路径(**覆盖**模式:每次推送都生成全新日志)—— **先**定**义**给后续 echo 用 -LOG_FILE="$(git rev-parse --show-toplevel)/logs/post-receive.log" -mkdir -p "$(dirname "$LOG_FILE")" +# ============================================================ +# 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")" +} -# **根**据**分**支选**择** env-example → .env(test 服**务**器**只**需 .env.local,**不**碰 .env.production) -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 +# ============================================================ +# 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" +} -# 元信息头(**首**次写 → 覆盖) -{ - 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 +} -# pnpm run build → 输出**丢**(>/dev/null 2>&1),仅捕获退出码写日志 -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" +# ============================================================ +# 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 +} -# build 失败 → **不**启 start(避免用旧 .next 跑新代码),立即退出 -if [ $BUILD_EXIT -ne 0 ]; then - echo "=== ABORTED @ $(date -u +%Y-%m-%dT%H:%M:%SZ) (build failed, start skipped) ===" >> "$LOG_FILE" - exit $BUILD_EXIT -fi +# ============================================================ +# 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" +} -# build 成功 → **先**关闭旧 next 进程(**避**免端口 EADDRINUSE 冲突,新代码**能**生效) -echo "=== stop existing next @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" -# 1) **优**雅**关**闭(SIGTERM)—— 旧进程**自**己 drain 现有请求 -pkill -f "next start" 2>/dev/null -pkill -f "next-server" 2>/dev/null -# 2) 等 2s 让**优**雅**关**闭**完**成 -sleep 2 -# 3) 强杀(SIGKILL)—— **还**在**的**话 -pkill -9 -f "next start" 2>/dev/null -pkill -9 -f "next-server" 2>/dev/null -# 4) **最**后等 1s 确**保**端口**完**全释放 -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" +} -# pnpm run start → **后台**跑(nohup ... &),long-running 进程 -# 输出**丢**(> /dev/null 2>&1)—— 真实 next 日志要看 next 自身 stdout/stderr -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