# 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/.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。