Files
syncbot/README.md
T
iceBear67 a006483bbc
ci / test (push) Canceled after 0s
docker / build (push) Canceled after 0s
init
2026-08-14 07:13:22 +00:00

281 lines
11 KiB
Markdown
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.
# syncbot
定时把一个 git 仓库(src)镜像推送到另一个 git 仓库(dst)的小机器人。
典型用途:把内网 GitLab / Gitea 上的仓库持续同步到 GitHubdst 侧用一把 deploy key 授权。
- **单文件、低占用** — 静态二进制 6.4 MB,空转常驻内存约 11 MB,同步时的重活交给短命的 `git` 子进程
- **配置热更新** — 改完 `config.toml` 直接保存,几秒内生效;只有被改动的仓库会重启,其它仓库的计时器和进行中的同步不受影响
- **无状态、自愈** — 不存任何同步进度。每轮都直接问 src 和 dst 各自有哪些引用,只做必要的操作。有人手贱 force push 了 dst、上一次推送推到一半失败、容器被重建 —— 下一轮自动收敛
- **省流量** — 每轮先用 `ls-remote` 探一下,src 没动就不 fetchdst 已经一致就不 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。