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
This commit is contained in:
2026-06-12 19:08:25 +08:00
parent f72d1ce94f
commit 83fdd73923
+115 -62
View File
@@ -7,74 +7,127 @@
# 用途:执行 package.json 中的 `build` + `start` 脚本 # 用途:执行 package.json 中的 `build` + `start` 脚本
# - 实际调用 `pnpm run build`(输出丢,仅记退出码) # - 实际调用 `pnpm run build`(输出丢,仅记退出码)
# - 再后台启 `pnpm run start`nohup,输出丢,hook 立即返回) # - 再后台启 `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" # 1. init_log_file —— 定**义** LOG_FILE + 创**建** logs/ 目**录**
mkdir -p "$(dirname "$LOG_FILE")" # ============================================================
init_log_file() {
LOG_FILE="$(git rev-parse --show-toplevel)/logs/post-receive.log"
mkdir -p "$(dirname "$LOG_FILE")"
}
# **根**据**分**支选**择** env-example → .envtest 服**务**器**只**需 .env.local**不**碰 .env.production # ============================================================
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) # 2. write_metadata_header —— **覆**盖写**入**元信息(**首**次**写** → **覆**盖**让日志**开**头是**这**个)
echo "=== branch: $CURRENT_BRANCH ===" >> "$LOG_FILE" # ============================================================
case "$CURRENT_BRANCH" in write_metadata_header() {
main) {
cp -f env-example/.env.production.example .env.production echo "=== post-receive @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
echo "=== env: copied .env.production (main → prod) ===" >> "$LOG_FILE" echo "branch: $(git rev-parse --abbrev-ref HEAD)"
;; echo "commit: $(git rev-parse --short HEAD)"
test) echo "cwd: $(pwd)"
cp -f env-example/.env.local.example .env.local echo ""
echo "=== env: copied .env.local (test → test) ===" >> "$LOG_FILE" } > "$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
# 元信息头(**首**次写 → 覆盖) # ============================================================
{ # 3. copy_env_by_branch —— 根**据**分支选 env-example → .env
echo "=== post-receive @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" # main 分支 → .env.productionprod 部署)
echo "branch: $(git rev-parse --abbrev-ref HEAD)" # test 分支 → .env.localtest **不**碰 .env.production**避**免**读**到 prod 域名)
echo "commit: $(git rev-parse --short HEAD)" # dev 分支 → .env.local**未**配远端**用**途,**但**保**留**
echo "cwd: $(pwd)" # ============================================================
echo "" copy_env_by_branch() {
} > "$LOG_FILE" 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" # 4. run_build —— 跑 `pnpm run build`,输出**丢****捕**获退出码写日志
pnpm run build > /dev/null 2>&1 # 返回:build 退出码(main **用** if ! run_build 决**定**是否**继**续)
BUILD_EXIT=$? # ============================================================
echo "=== build EXIT_CODE=$BUILD_EXIT @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" run_build() {
echo "" >> "$LOG_FILE" 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 # 5. stop_existing_next —— 两阶段 kill + 等 1s 确**保**端口**完**全释放
echo "=== ABORTED @ $(date -u +%Y-%m-%dT%H:%M:%SZ) (build failed, start skipped) ===" >> "$LOG_FILE" # 1) 优**雅**关**闭**SIGTERM)—— 旧**进**程**自**己 drain
exit $BUILD_EXIT # 2) 等 2s
fi # 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" # 6. launch_next —— 后**台**启 `pnpm run start`,捕 PID
# 1) **优**雅**关**闭(SIGTERM)—— 旧进程**自**己 drain 现有请求 # ============================================================
pkill -f "next start" 2>/dev/null launch_next() {
pkill -f "next-server" 2>/dev/null echo "=== start LAUNCH @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE"
# 2) 等 2s 让**优**雅**关**闭**完**成 nohup pnpm run start > /dev/null 2>&1 &
sleep 2 START_PID=$!
# 3) 强杀(SIGKILL)—— **还**在**的**话 echo "=== start PID=$START_PID @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE"
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"
# pnpm run start → **后台**跑(nohup ... &),long-running 进程 # ============================================================
# 输出**丢**> /dev/null 2>&1)—— 真实 next 日志要看 next 自身 stdout/stderr # 7. log_abort —— build 失败**时**写**结**束**标**记(**不**输**出** START LAUNCH
echo "=== start LAUNCH @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE" # ============================================================
nohup pnpm run start > /dev/null 2>&1 & log_abort() {
START_PID=$! echo "=== ABORTED @ $(date -u +%Y-%m-%dT%H:%M:%SZ) (build failed, start skipped) ===" >> "$LOG_FILE"
echo "=== start PID=$START_PID @ $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$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 exit 0