Files
cozsweet-frontend-nextjs/.githooks/post-receive
T
admin d5ddb3f97f fix(githooks): cd to worktree root before running build
When git invokes post-receive, cwd is GIT_DIR (not worktree root).
`git rev-parse --show-toplevel` refuses to run from a bare context,
so the script would silently cd into .git/. Use `--absolute-git-dir`
and its parent to find the worktree reliably.

Also: receive.denyCurrentBranch=false means push does NOT auto-update
the worktree, so the hook must do `git reset --hard HEAD` to sync
the source the build will use.
2026-06-16 14:18:25 +08:00

157 lines
6.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# Post-receive hook: push 后自动 build + start
#
# 重要:git 调钩子时 cwd=GIT_DIR(不是工作树根),所以必须先把 cwd
# 切到工作树根,下面的所有路径(env-example/、logs/、pnpm)才正确。
# 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 rev-parse --show-toplevel`会拒返回。
# 用 `git rev-parse --absolute-git-dir` 拿到绝对 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)"
else
REPO_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
fi
cd "$REPO_TOPLEVEL" || { echo "FATAL: cannot cd to $REPO_TOPLEVEL" >&2; exit 1; }
echo "=== REPO_TOPLEVEL=$REPO_TOPLEVEL (GIT_DIR=$GIT_DIR_ABS) ===" >&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.productionprod 部署)
# test 分支 → .env.localtest **不**碰 .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