281 lines
11 KiB
Markdown
281 lines
11 KiB
Markdown
# syncbot
|
||
|
||
定时把一个 git 仓库(src)镜像推送到另一个 git 仓库(dst)的小机器人。
|
||
典型用途:把内网 GitLab / Gitea 上的仓库持续同步到 GitHub,dst 侧用一把 deploy key 授权。
|
||
|
||
- **单文件、低占用** — 静态二进制 6.4 MB,空转常驻内存约 11 MB,同步时的重活交给短命的 `git` 子进程
|
||
- **配置热更新** — 改完 `config.toml` 直接保存,几秒内生效;只有被改动的仓库会重启,其它仓库的计时器和进行中的同步不受影响
|
||
- **无状态、自愈** — 不存任何同步进度。每轮都直接问 src 和 dst 各自有哪些引用,只做必要的操作。有人手贱 force push 了 dst、上一次推送推到一半失败、容器被重建 —— 下一轮自动收敛
|
||
- **省流量** — 每轮先用 `ls-remote` 探一下,src 没动就不 fetch,dst 已经一致就不 push
|
||
- **不会误删** — src 突然变空(URL 写错、token 过期长这样)时拒绝把 dst 清空
|
||
- **凭据不落盘** — 带 token 的 URL 只在命令行里传,不写进 `.git/config`;日志和报错里的密码一律脱敏
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### Docker Compose(推荐)
|
||
|
||
```bash
|
||
mkdir -p syncbot/keys && cd syncbot
|
||
curl -O https://raw.githubusercontent.com/OWNER/syncbot/main/docker-compose.yml
|
||
curl -o config.toml https://raw.githubusercontent.com/OWNER/syncbot/main/config.example.toml
|
||
# 编辑 docker-compose.yml 里的 image 和 config.toml 里的仓库地址
|
||
docker compose up -d
|
||
docker compose logs -f
|
||
```
|
||
|
||
### 直接跑二进制
|
||
|
||
```bash
|
||
go build -o syncbot .
|
||
./syncbot -config ./config.toml -check # 先校验配置
|
||
./syncbot -config ./config.toml
|
||
```
|
||
|
||
### 最小配置
|
||
|
||
```toml
|
||
[global]
|
||
work_dir = "/var/lib/syncbot"
|
||
|
||
[[repo]]
|
||
name = "my-project"
|
||
src = "https://gitlab.internal/team/my-project.git"
|
||
dst = "git@github.com:me/my-project.git"
|
||
interval = "5m"
|
||
ssh_key = "/etc/syncbot/keys/my-project"
|
||
```
|
||
|
||
完整示例见 [`config.example.toml`](config.example.toml)。
|
||
|
||
---
|
||
|
||
## 给 bot 配 GitHub deploy key
|
||
|
||
```bash
|
||
# 1. 生成一把不带密码的 key(bot 无人值守,不能有 passphrase)
|
||
ssh-keygen -t ed25519 -N "" -C "syncbot" -f keys/my-project
|
||
|
||
# 2. 把公钥加到 GitHub:仓库 → Settings → Deploy keys → Add deploy key
|
||
cat keys/my-project.pub
|
||
# ⚠️ 必须勾选 "Allow write access",否则只能读不能推
|
||
|
||
# 3. 配置里指向私钥
|
||
# ssh_key = "/etc/syncbot/keys/my-project"
|
||
```
|
||
|
||
几个坑:
|
||
|
||
- **一把 deploy key 只能绑一个仓库。** GitHub 不允许同一把公钥重复添加到多个仓库(会报 key is already in use)。要镜像多个仓库就每个仓库生成一把,或者改用 machine user / GitHub App token 走 HTTPS。
|
||
- **私钥权限不用操心。** 从 Kubernetes Secret 或 `:ro` 挂载进来的 key 常常是 0644,`ssh` 会直接拒绝。syncbot 会自动在私有临时目录做一份 0600 的副本再用,任务结束即删。
|
||
- **主机密钥校验。** 默认 `accept-new`(首次连接自动信任并记到 `work_dir/home/known_hosts`)。想更严格就先固定下来:
|
||
|
||
```bash
|
||
ssh-keyscan github.com > keys/known_hosts
|
||
```
|
||
|
||
```toml
|
||
known_hosts = "/etc/syncbot/keys/known_hosts"
|
||
strict_host_key = "yes"
|
||
```
|
||
|
||
---
|
||
|
||
## 配置
|
||
|
||
### `[global]`
|
||
|
||
| 字段 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `work_dir` | `/var/lib/syncbot` | 本地镜像仓库和 ssh 状态的存放目录,需要可写 |
|
||
| `interval` | `5m` | 默认轮询间隔 |
|
||
| `timeout` | `30m` | 单次同步的超时 |
|
||
| `concurrency` | `min(4, CPU 核数)` | 同时运行的 git 进程数上限,**内存主要靠它控制** |
|
||
| `max_backoff` | `1h` | 连续失败后的退避上限 |
|
||
| `reload_interval` | `5s` | 多久检查一次配置文件有没有变 |
|
||
| `listen` | 空(不监听) | HTTP 端点地址,如 `:8080` |
|
||
| `log_level` | `info` | `debug` / `info` / `warn` / `error` |
|
||
| `log_format` | `text` | `text` / `json` |
|
||
|
||
`[global]` 里还可以写下面所有 `[[repo]]` 字段作为默认值。
|
||
|
||
### `[[repo]]`
|
||
|
||
| 字段 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `name` | 必填 | 唯一标识,同时用作目录名和监控标签,限 `[A-Za-z0-9._-]` |
|
||
| `src` | 必填 | 源仓库,字符串或表(见下) |
|
||
| `dst` | 必填 | 目标仓库 |
|
||
| `enabled` | `true` | 设为 `false` 可临时停掉而不用删配置 |
|
||
| `interval` / `timeout` / `max_backoff` | 继承 global | |
|
||
| `refs` | `["refs/heads/*", "refs/tags/*"]` | 镜像哪些引用,每条最多一个 `*` |
|
||
| `prune` | `true` | src 上删掉的分支/标签,也在 dst 上删掉 |
|
||
| `force` | `true` | 允许非快进推送。上游 rebase 过就必须开 |
|
||
| `atomic` | `false` | `true` 表示所有引用要么全成功要么全不动 |
|
||
| `allow_empty` | `false` | 允许「空的 src 清空 dst」,见下方保护机制 |
|
||
| `ssh_key` | 无 | 私钥绝对路径,同时作用于 src 和 dst |
|
||
| `known_hosts` | 无 | 固定的 known_hosts 文件 |
|
||
| `strict_host_key` | 有 `known_hosts` 时 `yes`,否则 `accept-new` | `yes` / `no` / `accept-new` |
|
||
| `git_config` | 无 | 原样传给 git 的 `-c key=value`,用于压内存等 |
|
||
|
||
### 端点写法
|
||
|
||
src / dst 可以直接写成字符串:
|
||
|
||
```toml
|
||
src = "https://gitlab.internal/team/repo.git"
|
||
dst = "git@github.com:me/repo.git"
|
||
```
|
||
|
||
两边需要各自的凭据时写成表:
|
||
|
||
```toml
|
||
[repo.src]
|
||
url = "git@gitlab.internal:team/repo.git"
|
||
ssh_key = "/etc/syncbot/keys/gitlab"
|
||
|
||
[repo.dst]
|
||
url = "git@github.com:me/repo.git"
|
||
ssh_key = "/etc/syncbot/keys/github"
|
||
known_hosts = "/etc/syncbot/keys/known_hosts"
|
||
strict_host_key = "yes"
|
||
```
|
||
|
||
### 环境变量
|
||
|
||
URL 和路径里的 `${VAR}` 会用环境变量展开,适合放 token:
|
||
|
||
```toml
|
||
src = "https://x-access-token:${SRC_TOKEN}@github.com/upstream/repo.git"
|
||
```
|
||
|
||
- 变量没定义会**直接启动失败并指出名字**,不会静默展开成空串给你一个坏掉的 URL
|
||
- 只认 `${VAR}`,裸写的 `$VAR` 原样保留(密码里 `$` 很常见)
|
||
- 环境变量在进程启动时读取,改环境变量需要重启
|
||
|
||
---
|
||
|
||
## 热更新
|
||
|
||
syncbot 每 `reload_interval` 比对一次配置文件内容的哈希,变了就重新加载;也可以 `kill -HUP` 立即触发。
|
||
|
||
重新加载时会把新配置和正在跑的任务做 diff:
|
||
|
||
- 新增的仓库 → 启动
|
||
- 删掉或 `enabled = false` 的仓库 → 停掉,其正在进行的 git 操作会被取消
|
||
- 配置有改动的仓库 → 重启(新任务会等旧任务完全退出,避免两个进程同时动同一个镜像目录)
|
||
- **没改动的仓库 → 完全不动**,计时器和进行中的同步都不受影响
|
||
|
||
几点说明:
|
||
|
||
- **配置写错了不会挂。** 解析失败会记一条 ERROR,然后继续用上一份能跑的配置。修好保存即可恢复
|
||
- `log_level` 改了立即生效
|
||
- `listen` 改了需要重启进程,日志里会 WARN 提示
|
||
- 累计的 `syncs` / `pushes` 计数在仓库重启后保留,只有仓库被删掉才清零
|
||
|
||
---
|
||
|
||
## 运维
|
||
|
||
### 命令行
|
||
|
||
```
|
||
syncbot -config PATH 配置文件路径(默认 /etc/syncbot/config.toml)
|
||
-check 校验配置并打印解析结果后退出
|
||
-once 所有仓库同步一次就退出(适合 cron / CI)
|
||
-version 打印版本
|
||
```
|
||
|
||
### 信号
|
||
|
||
| 信号 | 行为 |
|
||
| --- | --- |
|
||
| `SIGHUP` | 立即重新加载配置 |
|
||
| `SIGTERM` / `SIGINT` | 优雅退出,最多等 20 秒让进行中的同步收尾 |
|
||
|
||
### HTTP 端点(需要设置 `listen`)
|
||
|
||
| 路径 | 说明 |
|
||
| --- | --- |
|
||
| `/healthz` | 进程活着就返回 200,适合容器 healthcheck |
|
||
| `/readyz` | 所有仓库都至少成功同步过一次才返回 200,否则 503 |
|
||
| `/status` | JSON,每个仓库的最后运行时间、失败次数、引用数、最后一次错误 |
|
||
| `/metrics` | Prometheus 文本格式 |
|
||
|
||
指标:`syncbot_build_info`、`syncbot_uptime_seconds`、`syncbot_sync_total`、
|
||
`syncbot_push_total`、`syncbot_consecutive_failures`、`syncbot_refs`、
|
||
`syncbot_last_success_timestamp_seconds`、`syncbot_last_duration_seconds`。
|
||
|
||
告警建议盯 `syncbot_consecutive_failures > 0`,或者
|
||
`time() - syncbot_last_success_timestamp_seconds` 超过 interval 的若干倍。
|
||
|
||
---
|
||
|
||
## 工作原理
|
||
|
||
每个仓库一个 goroutine、一个独立计时器,所以某个仓库卡住不会拖累别的。
|
||
一个全局信号量限制同时运行的 git 进程数(`concurrency`)。
|
||
|
||
单轮同步:
|
||
|
||
1. `git ls-remote` 问 src 现在有哪些引用
|
||
2. 和本地镜像(`work_dir/mirrors/<name>.git`)比对 → 一样就**跳过 fetch**
|
||
3. 不一样才 `git fetch --prune`
|
||
4. `git ls-remote` 问 dst 现在有哪些引用
|
||
5. 和本地镜像比对 → 一样就**跳过 push**
|
||
6. 不一样才 `git push --prune --force`
|
||
|
||
因为每轮都重新问过两边,所以不需要任何本地状态文件,删掉 `work_dir` 也只是让下次重新克隆一遍而已。
|
||
|
||
失败时按 `interval → 2×→ 4× …` 退避,上限 `max_backoff`;成功后立刻恢复正常节奏。
|
||
|
||
所有 git 调用都在独立进程组里,超时或退出时整组一起收掉,不会漏下 `ssh` 之类的子进程。
|
||
每次调用都用隔离的 `HOME` 并禁用系统/全局 gitconfig,行为不受宿主机环境影响。
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
**推送被拒:`refusing to delete the current branch`**
|
||
dst 的默认分支(HEAD 指向的那个)不允许被删除,GitHub 也一样。
|
||
上游把 `master` 改名成 `main` 之后会撞上这个:先去 GitHub 仓库设置里把默认分支改成新名字,再让 syncbot 同步。
|
||
|
||
**日志出现 `refusing to mirror an empty source`**
|
||
src 一个引用都没返回,但 dst 有内容。绝大多数情况是 URL 写错或者 token 过期,
|
||
所以默认拒绝推送以免把 dst 清空。确实想清空的话给那个仓库加 `allow_empty = true`。
|
||
|
||
**`Permission denied (publickey)`**
|
||
依次检查:deploy key 是否勾了 *Allow write access*;配置里 `ssh_key` 是否指向**私钥**(不是 `.pub`);
|
||
key 是否没有 passphrase;同一把 key 是否被重复用在了多个 GitHub 仓库上。
|
||
`log_level = "debug"` 可以看到每条 git 命令。
|
||
|
||
**只想同步分支,不要标签 / PR 引用**
|
||
`refs = ["refs/heads/*"]`。默认就不会碰 `refs/pull/*` 这类引用。
|
||
|
||
**大仓库把内存吃满了**
|
||
吃内存的是 `git`,不是 syncbot 自己。调小 `concurrency`,并给该仓库加:
|
||
|
||
```toml
|
||
git_config = ["pack.threads=1", "pack.windowMemory=64m", "core.bigFileThreshold=8m"]
|
||
```
|
||
|
||
**想立刻同步一次,不等下个周期**
|
||
`docker compose restart syncbot`,或者用 `-once` 单独跑一次。
|
||
|
||
---
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
go test ./... # 单元测试 + 跑真实 git 的端到端测试
|
||
go test -race ./...
|
||
go vet ./...
|
||
go build .
|
||
```
|
||
|
||
代码结构和设计约定见 [AGENTS.md](AGENTS.md)。
|
||
|
||
镜像由 GitHub Actions 构建并推送到 `ghcr.io/OWNER/syncbot`,
|
||
`main` 分支推 `latest`,打 `v*` 标签推对应语义化版本,支持 amd64 / arm64。
|