Skip to content

Git原生Hook结合脚本自动化部署

约 2562 字大约 9 分钟

2026-03-19

小的项目使用git actrun部署都要去创建一个容器太吃资源 也慢,所以使用git原生钩子来做自动化部署

使用的docker部署的gitea

1.映射路径

  • 需要把gitea的数据仓库映射出来:- ./gitea:/data:rw
  • 把代码和部署的文件夹也映射到容器内 - /volume2/Site:/deploy:rw 这样可以在容器内git pull,也可以打包完了复制到部署的站点下

2.找到仓库内的钩子文件

gitea/gitea/git/repositories/{用户名}/{仓库名}.git/hooks/post-receive 可以看到这个脚本作用是去扫描与钩子脚本相同的.d文件(post-receive.d)中的脚本。所以我们只需要在这个文件中放入部署的脚本就可以了

3.容器内预设

# 将仓库设置为git的安全目录
git config --global --add safe.directory /data/git/repositories/yuchen

4.编写部署脚本

如果是从当前容器中拉取,当前容器中有这个仓库:可以直接从仓库文件中克隆

deploy.sh
#!/bin/bash
unset GIT_DIR GIT_WORK_TREE

echo "================= 开始部署 ================="

REPO="file:///data/git/repositories/yuchen/vuepress.git"
WORK_REPO_DIR="/deploy/repo/vuepress"
BLOG_DIR="/deploy/blog"

# 目录不存在克隆仓库
if [ ! -d "$WORK_REPO_DIR" ]; then
  echo "仓库不存在开始克隆..."
  mkdir -p "$WORK_REPO_DIR"
  git clone -b release "$REPO" "$WORK_REPO_DIR"
  echo "仓库克隆完成"
fi

#设置git hook的git环境
#GIT_GLOBAL="git --work-tree=$WORK_REPO_DIR --git-dir=$WORK_REPO_DIR/.git"
#echo "GIT_GLOBAL: $GIT_GLOBAL"
# 克隆代码
#git clone -b release "$REPO" "$WORK_REPO_DIR" 

cd "$WORK_REPO_DIR" || exit 1
echo "==> 工作路径: $(pwd)"

# ==================== 拉代码前后,先记录新旧的 package.json hash ====================
old_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')

echo "==== 切换到release并pull ===="
git switch release --force &>/dev/null
git pull --force &>/dev/null

new_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')

echo "==> 当前分支: $(git branch)"
#echo "==> 当前仓库地址: $(git remote -v)"

# ==================== 判断是否存在pnpm 不存在就安装 ====================
if ! pnpm --version >/dev/null 2>&1; then
  echo "=== pnpm 不存在,开始安装 Node.js + pnpm ==="
  sudo apk update
  sudo apk add nodejs npm
  sudo npm install -g pnpm --registry=https://registry.npmmirror.com

  pnpm --version
  pnpm i &>/dev/null
else
  echo "=== pnpm 已存在,版本:$(pnpm --version) 跳过安装 ==="
fi

# 变化了才重新安装依赖!否则跳过!
if [ "$old_hash" != "$new_hash" ]; then
    echo "======== 依赖已变化,重新安装 ========"
    pnpm i &>/dev/null
fi

# 限制NODE使用的最大内存(防止内存溢出被kill) && 构建
#export NODE_OPTIONS="--max-old-space-size=512"
echo "run build"

if ! pnpm run build; then
  echo "❌ 构建失败,停止部署!"
  exit 1
fi

# 发布
rm -rf "$BLOG_DIR"/*
cp -r ./docs/.vuepress/dist/* "$BLOG_DIR"/

echo "================= success! 部署完成 ================="

从其他地方拉取:注意钩子环境是非交互式的拉取是不能输入密码需要部署密钥或者git@的方式

deploy.sh
#!/bin/bash
echo "====================  开始部署 ===================="
WORK_DIR="/deploy/repo/vuepress"
cd "$WORK_DIR" || {
  echo "❌ 进入项目目录失败"
  exit 1
}
echo -n "工作目录:1"
pwd

chmod -R 777 /deploy/repo/vuepress &>/dev/null
rm -rf ./docs/.vuepress/dist &>/dev/null

# ==================== 设置git hook的git环境 ====================
GIT_GLOBAL="git --work-tree=$WORK_DIR --git-dir=$WORK_DIR/.git"
echo "GIT_GLOBAL: $GIT_GLOBAL"
$GIT_GLOBAL switch release &>/dev/null

# ==================== 拉代码前后,先记录新旧的 package.json hash ====================
old_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')
$GIT_GLOBAL pull origin release
new_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')

# ==================== 判断是否存在pnpm 不存在就安装 ====================
if ! pnpm --version >/dev/null 2>&1; then
  echo "=== pnpm 不存在,开始安装 Node.js + pnpm ==="
  apk update
  apk add nodejs npm
  npm install -g pnpm --registry=https://registry.npmmirror.com
  pnpm --version
  #第一次默认安装一次依赖
  pnpm i 
else
  echo "=== pnpm 已存在,版本:$(pnpm --version) 跳过安装 ==="
fi

# 变化了才重新安装依赖!否则跳过!
if [ "$old_hash" != "$new_hash" ]; then
    echo "==================== 依赖已变化,重新安装 ===================="
    pnpm i
fi

pnpm run build
#ls -lha ./docs/.vuepress/dist

#删除以前的站点文件
rm -rf /deploy/blog/*
mv ./docs/.vuepress/dist/* /deploy/blog/
echo "==================== ✅✅✅部署完成✅✅✅ ===================="

git原生钩子

服务端:

名称触发时机主要作用/使用场景
pre-receivegit push操作到达服务器后,所有引用(refs)被更新之前触发一次。服务端第一个执行的钩子。用于执行全局性的强制策略,例如禁止非快进式推送、进行复杂的访问控制。若脚本非零退出,拒绝所有推送。
updatepre-receive之后,为每一个即将被更新的引用(分支或标签)各触发一次。用于按分支实施精细化的权限控制。例如,允许特定用户推送 master 分支,但禁止推送 develop 分支。拒绝一个引用不影响其他引用的更新。
post-receivegit push操作成功完成,所有引用都被更新之后触发一次。主要用于部署和通知。例如,将代码部署到生产环境、触发 CI/CD 流水线、给邮件列表发送通知。此钩子无法中止推送流程。
post-updatepost-receive之后触发,功能与之类似,但通常用于通知。该钩子会收到一个参数,列出所有被更新的引用名。主要用于向外部服务(如网站构建服务)发出通知,告知仓库已更新。
push-to-checkoutgit receive-pack更新了分支,但该分支当前被检出且 receive.denyCurrentBranch 设置为 updateInstead 时触发。用于自定义如何更新当前被检出的分支的工作目录,以便安全地在非裸仓库接收推送。
reference-transaction当 Git 更新某个引用(ref)时,在事务的特定阶段(提交、中止或准备)触发。一个较底层的钩子,用于高级的引用监听和审计,可以捕获引用的所有更改。

客户端:

名称触发时机主要作用/使用场景
applypatch-msggit am命令调用。它接收一个参数,即存放提交信息的临时文件路径。用于检查和规范化补丁提交的提交信息。如果脚本以非零状态退出,git am 将中止。
pre-applypatchgit am命令调用。在补丁被应用之后、但提交尚未生成之前执行。用于检查应用补丁后的工作区状态,例如运行测试。若脚本非零退出,则补丁不会提交。
post-applypatchgit am命令调用。在补丁被应用且提交生成之后运行。主要用于通知,例如通知团队成员或补丁作者。此钩子无法影响 git am 命令的执行结果。
pre-commit在执行git commit时,在获取提交信息并生成提交对象之前触发。可通过--no-verify绕过。最常用的钩子之一。用于检查即将提交的快照,例如运行代码风格检查(Lint)、单元测试、检查调试代码或尾随空白字符等。
pre-merge-commit在执行git merge时,在合并成功完成后、生成提交信息并提交之前触发。可通过--no-verify绕过。用于在合并提交前对合并结果进行检查,类似于 pre-commit 在合并场景下的应用。
prepare-commit-msg在执行git commit时,在默认提交信息准备完成后、但编辑器启动之前运行。用于动态编辑提交信息模板。例如,根据分支名自动填充缺陷单号,或从合并信息中提取说明。此钩子不能被 --no-verify 绕过。
commit-msg在执行git commit或 git merge 时,在用户输入提交信息之后、提交生成之前触发。可通过 --no-verify 绕过。用于验证提交信息是否符合项目规范,例如检查信息格式、长度或是否包含 Signed-off-by 签名。
post-commitgit commit整个流程成功完成后立即触发。主要用于事后通知,如邮件提醒、更新工单系统。此钩子无法影响 git commit 的结果。
pre-rebase在执行git rebase之前触发。用于阻止某些分支被变基,例如禁止对已推送到公共仓库的分支进行变基操作。
post-checkoutgit checkout 或 git switch 成功更新工作目录后触发。在 git clone 之后也会执行。用于根据切换后的分支调整工作目录,例如自动切换文档版本、修改环境变量或设置 Git 配置。
post-mergegit mergegit pull 操作成功完成后触发。用于处理 Git 无法追踪的元数据,例如恢复文件权限、重新生成依赖(如 npm install)。
pre-pushgit push执行期间,远程引用被更新但对象尚未传输时触发。用于在推送前进行最后检查,例如运行尚未提交的本地测试,防止将损坏的或不规范的代码推送到远程仓库。
pre-auto-gc在执行git gc --auto进行自动垃圾回收之前触发。用于在自动垃圾回收前进行检查或提醒,可以根据情况决定是否中止回收过程。
post-rewrite被那些会替换已有提交的命令调用,如git commit --amendgit rebase(不包括git filter-branch)。用于处理由重写历史产生的提交变更,其用途类似于 post-checkout 和 post-merge,例如同步相关的元数据。

很蠢,改为自定义容器

  • 使用dockerfile将依赖(node、npm、pnpm)打包打镜像里去
FROM gitea/gitea:latest                         

RUN apk add --no-cache nodejs npm  && npm install -g pnpm --registry=https://registry.npmmirror.com  && rm -rf /root/.npm /tmp/*
# 有权限问题  无法用
FROM gitea/gitea:latest-rootless

USER root # 以 root 权限安装依赖(只执行一次) 
RUN apk add --no-cache nodejs npm  && npm install -g pnpm --registry=https://registry.npmmirror.com  && rm -rf /root/.npm /tmp/*
USER git # 切回 git 用户(Gitea 强制要求)
#!/bin/bash
unset GIT_DIR GIT_WORK_TREE

echo "================= 开始部署 ================="

REPO="file:///data/git/repositories/yuchen/vuepress.git"
WORK_REPO_DIR="/deploy/repo/vuepress"
BLOG_DIR="/deploy/blog"

# 目录不存在克隆仓库
if [ ! -d "$WORK_REPO_DIR" ]; then
  echo "仓库不存在开始克隆..."
  mkdir -p "$WORK_REPO_DIR"
  git clone -b release "$REPO" "$WORK_REPO_DIR"
  echo "仓库克隆完成"
fi

cd "$WORK_REPO_DIR" || exit 1
echo "==> 工作路径: $(pwd)"

# ==================== 拉代码前后,先记录新旧的 package.json hash ====================
old_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')

echo "==== 切换到release并pull ===="
git switch release --force &>/dev/null
git pull --force &>/dev/null

new_hash=$(md5sum package.json pnpm-lock.yaml 2>/dev/null | head -1 | awk '{print $1}')

echo "==> 当前分支: $(git branch)"
#echo "==> 当前仓库地址: $(git remote -v)"

echo "pnpm 版本:$(pnpm --version)"
# 变化了才重新安装依赖!否则跳过!
if [ "$old_hash" != "$new_hash" ]; then
    echo "======== 依赖已变化,重新安装 ========"
    pnpm i &>/dev/null
fi

echo "run build"

if ! pnpm run build; then
  echo "❌ 构建失败,停止部署!"
  exit 1
fi

# 发布
rm -rf "$BLOG_DIR"/*
cp -r ./docs/.vuepress/dist/* "$BLOG_DIR"/

echo "================= success! 部署完成 ================="