社交媒体

Git提交代码自动同步宝塔

Git提交代码自动同步宝塔
改一行代码上线要几步?

打包、登录宝塔、找到站点目录、上传覆盖、清缓存——周末改个错别字也得走一遍。更惨的是某次改完急着走,本地推了,服务器没拉,线上跑着三天前的版本,用户反馈的 bug 你在这边怎么都复现不出来。

这篇把这个环节彻底砍掉:**本地 `git push` 之后,服务器自己把代码拉下来,全程你不用再登面板。**

![番外-Git同步链路图.png](/upload/%E7%95%AA%E5%A4%96-Git%E5%90%8C%E6%AD%A5%E9%93%BE%E8%B7%AF%E5%9B%BE.png)

## 01 先搞清它在干什么

原理一句话:**代码仓库被 push 时,会往你留的一个网址发一条通知(HTTP 请求),宝塔收到通知就去执行一句脚本,脚本做的事就是 `git pull`。**

三个角色:

| 角色                    | 干什么                                 |
| ----------------------- | -------------------------------------- |
| Gitee / GitHub / GitLab | 事件源:你 push 了,它就发通知         |
| 宝塔 WebHook 插件       | 接收器:守着一个网址,收到请求就跑脚本 |
| 同步脚本(shell)       | 执行者:`git pull` + 修权限 + 写日志 |

所以整套东西没有任何黑科技,难的全在细节:**脚本放哪、谁来跑、权限怎么给、失败了去哪看日志。**

> **顺带说个安全提醒**:宝塔给的 WebHook 地址里带一串密钥(access_key),**谁拿到谁就能让你的服务器拉代码**。别把它贴到公开仓库、群聊、截图里;服务器上有防火墙的,建议只放行码云/GitHub 的回调 IP。

## 02 服务器上先把 Git 和钥匙准备好

进宝塔 → 终端,先看有没有 git(宝塔机器一般自带):

```bash
git --version
```

![image-olkH.png](/upload/image-olkH.png)

没有就装:

```bash
# CentOS / 宝塔常见
yum install -y git
# Ubuntu / Debian
apt-get install -y git
```

生成 SSH 钥匙(一路回车三下,不要设密码,否则脚本拉代码会卡住):

```bash
ssh-keygen -t rsa -C "bt-deploy"
cat ~/.ssh/id_rsa.pub
```

把 `cat` 出来的那一整串复制下来(以 `ssh-rsa` 开头、以你的主机名结尾),粘到 **Gitee → 设置 → SSH 公钥**,标题会自动生成。

![2.png](/upload/2.png)

验证服务器和码云是否握手成功(这一步不能跳过):

```bash
ssh -T git@gitee.com
# 看到 Hi XXX! You've successfully authenticated 就成了
```

这一步的意义:让服务器有了"免密拉代码"的身份。后面脚本用 SSH 地址拉代码,就不会在深夜因为要输密码而静默失败。

## 03 站点目录:先把仓库 clone 下来

进 `/www/wwwroot`(宝塔站点默认都在这),用 **SSH 地址** clone:

```bash
cd /www/wwwroot
git clone git@gitee.com:你的用户名/你的仓库名.git my-site
```

> ⚠️ 目录名记住它——**它就是后面 WebHook 地址里要传的参数**。仓库名和站点目录名保持一致,整套配置最省事。

clone 完务必把归属改回 `www`(宝塔站点的运行用户,不改的话 Web 访问可能 403):

```bash
chown -R www:www /www/wwwroot/my-site
```

再放行一次 git 的目录安全检查(避免后面脚本报错 `detected dubious ownership`):

```bash
git config --global --add safe.directory /www/wwwroot/my-site
```

## 04 装宝塔 WebHook,填脚本

宝塔面板 → 软件商店 → 搜 **WebHook** → 安装。装完打开它,**添加 Hook**,名称随便写(比如 `sync-site`),执行脚本贴下面这份:

```bash
#!/bin/bash
##############################################################################
# 橙码酱 · 宝塔 WebHook 自动同步脚本
#
# v2 相比网上流传的版本修了三件事,都是真踩过的坑:
#   1. 同一秒被触发多次 → 加并发锁,只跑一个,其余打印 [SKIP] 直接退出
#   2. SSH 认证失败时 git 会卡在"等你输密码",脚本永远不返回 → BatchMode,失败立刻报错
#   3. git 的报错走 stderr,不进宝塔日志 → exec 2>&1,报错一并可见
##############################################################################

exec 2>&1                        # 让 git 的报错也进宝塔日志,否则你只能看到一半

# ====================== 只需改这三行 ======================
GIT_USER="你的码云用户名"        # gitee.com 后面的用户名
BRANCH="master"                  # 要同步的分支(main / master / dev …)
WWW_ROOT="/www/wwwroot"          # 站点根目录
# =========================================================

LOG_DIR="/www/wwwlogs/deploy"    # 部署日志目录,排错全靠它

# 防卡死:SSH 不弹密码框、连不上就 10 秒超时报错;https 地址也不弹凭据框
export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10"
export GIT_TERMINAL_PROMPT=0

echo ""
echo "===== $(date "+%Y-%m-%d %H:%M:%S") Start ====="

# ---------- 0. 取参数:param 传的就是站点目录名 ----------
if [ -z "$1" ]; then
    echo "[ERROR] param 为空:WebHook 地址请带 &param=站点目录名"
    echo "===== End ====="
    exit 1
fi

SITE="$1"
gitPath="$WWW_ROOT/$SITE"
gitHttp="git@gitee.com:${GIT_USER}/${SITE}.git"   # SSH 地址;注释用 #,不能用 //
echo "[STEP1] 参数 OK:站点目录 = $gitPath"

# ---------- 1. 并发锁:同一秒被触发多次时只跑一个 ----------
LOCK="/tmp/bt_sync_${SITE}.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
    echo "[SKIP] 上一次同步还没跑完,本次跳过(避免 git 抢锁卡死)"
    echo "===== End ====="
    exit 0
fi
trap 'rm -rf "$LOCK"' EXIT

echo "[STEP2] 拿到锁,开始同步"

# ---------- 2. 目录不存在就创建并克隆 ----------
if [ ! -d "$gitPath" ]; then
    echo "[STEP3] 目录不存在,创建并克隆"
    mkdir -p "$gitPath"
    cd "$gitPath" || exit 1
    git clone -b "$BRANCH" "$gitHttp" tmp_clone
    mv tmp_clone/.git ./
    rm -rf tmp_clone
fi

cd "$gitPath" || exit 1

# ---------- 3. 放行 git 安全目录检查(避免 dubious ownership 报错)----------
git config --global --add safe.directory "$gitPath"
echo "[STEP4] 进入站点目录,开始拉取"

# ---------- 4. 强制对齐远端 ----------
# 部署目录不该有人手改,所以用 reset --hard 而不是 pull:
# pull 在本地有改动/换行符差异时会因冲突失败,reset 则是"和仓库一模一样"
git fetch --all
git reset --hard "origin/$BRANCH"
echo "[STEP5] 代码已对齐"

# ---------- 5. 归属还给 www(宝塔站点运行用户)----------
chown -R www:www "$gitPath"

# ---------- 6. 记一笔日志,方便事后查 ----------
mkdir -p "$LOG_DIR"
{
    echo "----- $(date "+%Y-%m-%d %H:%M:%S") -----"
    echo "站点目录:$gitPath"
    echo "当前版本:$(git rev-parse --short HEAD)"
    git log -1 --pretty=format:"%h %an %ad %s" --date=format:"%Y-%m-%d %H:%M"
    echo ""
} >> "$LOG_DIR/${SITE}.log" 2>&1

echo "[STEP6] 已更新到:$(git rev-parse --short HEAD)"
echo "===== End ====="
exit 0
```

![番外-Git同步三坑.png](/upload/%E7%95%AA%E5%A4%96-Git%E5%90%8C%E6%AD%A5%E4%B8%89%E5%9D%91.png)

保存之后,**再点一次「编辑」,确认脚本原样还在**。宝塔有时候会过滤掉部分字符,脚本残缺时它照样返回 200,但服务器什么都没做——这个坑能让你排查一晚上。

## 05 拿到地址,回填到仓库

![4.png](/upload/4.png)

保存后,WebHook 界面会给你一条地址,长这样:

```text
http://你的服务器IP:8888/hook?access_key=一串密钥&param=aaa
```

把最后的 **`aaa` 改成你的站点目录名**(也就是 03 步 clone 出来的那个名字),然后整条复制。

打开 **Gitee 仓库 → 管理 → WebHooks → 添加**,把地址粘进去,事件勾 **Push**(有需要再勾 Tag Push),保存。

改完面板记得重启一次宝塔,让权限和环境变量彻底生效:

```bash
/etc/init.d/bt restart
```

**GitHub / GitLab 同款操作**:都是把这条 URL 填到仓库的 Webhooks → Payload URL 里,Content type 选默认的即可——宝塔那边不解析请求体,只看 URL 里的 param。

## 06 验证:别急着改代码

最快的验证不需要 push——直接浏览器访问那条 WebHook 地址(或用 `curl`):

```bash
curl "http://你的服务器IP:8888/hook?access_key=一串密钥&param=my-site"
```

回来看三样东西:

| 看哪                          | 期望结果                                         |
| ----------------------------- | ------------------------------------------------ |
| Gitee → WebHooks → 查看更多 | 状态码 **200**                             |
| 宝塔 WebHook → 日志          | 出现你写的时间戳、`Reset` 结果、最新 commit 号 |
| 站点目录文件                  | 修改时间和刚才一致                               |

三样都对上了,再在本地改一行、commit、push,亲眼看着服务器目录自己变。那一刻的爽感,值这一篇的收藏。

## 07 日志只有 Start,永远等不到 End

这是我自己在服务器上真遇到的一幕——日志长这样,整整九行 `Start`,一行 `End` 都没有:

```text
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:19 Start =====
===== 2026-10-04 22:02:12 Start =====
===== 2026-10-04 22:03:15 Start =====
===== 2026-10-04 22:31:15 Start =====
```

先读这段日志:**同一个「21:45:13」触发了五次**。这本身就是最大的线索。

`Start` 是脚本第一行打印的,它出现了,说明「宝塔收到请求 → 拉起脚本 → 脚本开始跑」这条路是通的,问题**不在 WebHook 配置,而在脚本中途卡住/静默死掉**。卡住的位置按概率排:

| 排名 | 卡在哪                                  | 为什么会卡                                                                                                |
| ---- | --------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| 1    | `git fetch`(等你输密码)             | 用了 HTTPS 地址,或 SSH key 没配对。git 想问你要账号密码,可 WebHook 没有终端,它就一直等——等到天荒地老 |
| 2    | `git fetch`(抢 `.git/index.lock`) | 五条同一秒触发,五个进程同时摸同一个仓库,git 互相等锁                                                    |
| 3    | 报错静默吞掉                            | git 的报错走 stderr,宝塔日志只接 stdout,你看到的就是「啥也没有」                                        |

三条里前两条有一个共同点:**脚本一直在往 `End` 走,但永远走不到**。

所以加固版做了三件事(就是上面脚本里那三行看着"多余"的配置):

- `export GIT_TERMINAL_PROMPT=0` + `GIT_SSH_COMMAND="ssh -o BatchMode=yes"` → 认证失败**立刻报错返回**,绝不弹框等待
- `mkdir "$LOCK"` 原子并发锁 → 同一秒触发 N 次,只跑一个,其余打印 `[SKIP]` 直接 `End` 退出
- `exec 2>&1` → git 的报错并进日志,再也不会「一片空白」

外加 `[STEP1]`~`[STEP6]` 六步面包屑:**日志停在 STEP 几,就是卡在第几步**,不用再猜。

真排查时,别先改业务代码,先跑一遍探针脚本(`bt-webhook-probe.sh`,打印 `[P0]`~`[P6]` 的环境信息,跑到 `Probe End` 说明执行链路没问题)。另外记得清掉已经卡住的僵尸进程:

```bash
ps -ef | grep -E "git|hook" | grep -v grep
kill -9 上面查出来的进程号
rm -f /www/wwwroot/你的站点目录/.git/index.lock
```

## 排错清单:报错基本就这 6 种

| 现象                                            | 原因                                               | 一句解决                                                |
| ----------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------- |
| WebHook 日志只有 `[ERROR] param 为空`         | 地址里没带 `param`,或拼错了参数名               | 在 URL 末尾补 `¶m=站点目录名`                        |
| `Permission denied (publickey)`               | SSH 公钥没配对,或用了 HTTPS 地址                  | 回到 02 步 `ssh -T git@gitee.com` 验一次              |
| `detected dubious ownership`                  | 仓库属主和执行者不是一个人                         | `git config --global --add safe.directory 目录`       |
| `sudo: command not found` / `sudo 需要 tty` | 网上教程习惯性加 sudo,面板里本来就是 root         | **把 sudo 全删掉**                                |
| 返回 200 但代码没变                             | 脚本被面板过滤,或者改的不是你拉的分支             | 重新编辑脚本确认;检查 `BRANCH` 变量                  |
| 只有 `Start` 没有 `End`                     | git 在等你输密码、或多个进程抢 `.git/index.lock` | 换加固版脚本(BatchMode + 并发锁),再清 `index.lock` |

## 速查表

| 环节         | 关键命令 / 位置                                     |
| ------------ | --------------------------------------------------- |
| 生成钥匙     | `ssh-keygen -t rsa` → `cat ~/.ssh/id_rsa.pub`  |
| 配公钥       | Gitee → 设置 → SSH 公钥                           |
| 握手验证     | `ssh -T git@gitee.com`                            |
| 首次 clone   | `/www/wwwroot` 下 `git clone git@…`            |
| 权限         | `chown -R www:www 目录`                           |
| 放行安全目录 | `git config --global --add safe.directory 目录`   |
| WebHook 地址 | `http://IP:8888/hook?access_key=…¶m=站点目录名` |
| 触发方式     | Gitee/GitHub 的 Push 事件,或手动 `curl` 那条地址 |
| 看日志       | 宝塔 → WebHook → 日志                             |

一句话总结:**公钥打通身份,WebHook 打通通知,脚本负责最后那一下 `reset --hard`。** 三样都通了,push 就是上线。

橙码酱,分享更有用的技术知识,助你成为更优秀的开发者。