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

11 KiB
Raw Blame History

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 没动就不 fetch,dst 已经一致就不 push
  • 不会误删 — src 突然变空(URL 写错、token 过期长这样)时拒绝把 dst 清空
  • 凭据不落盘 — 带 token 的 URL 只在命令行里传,不写进 .git/config;日志和报错里的密码一律脱敏

快速开始

Docker Compose(推荐)

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

直接跑二进制

go build -o syncbot .
./syncbot -config ./config.toml -check   # 先校验配置
./syncbot -config ./config.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


给 bot 配 GitHub deploy key

# 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)。想更严格就先固定下来:

    ssh-keyscan github.com > keys/known_hosts
    
    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_hostsyes,否则 accept-new yes / no / accept-new
git_config 原样传给 git 的 -c key=value,用于压内存等

端点写法

src / dst 可以直接写成字符串:

src = "https://gitlab.internal/team/repo.git"
dst = "git@github.com:me/repo.git"

两边需要各自的凭据时写成表:

[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:

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_infosyncbot_uptime_secondssyncbot_sync_totalsyncbot_push_totalsyncbot_consecutive_failuressyncbot_refssyncbot_last_success_timestamp_secondssyncbot_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,并给该仓库加:

git_config = ["pack.threads=1", "pack.windowMemory=64m", "core.bigFileThreshold=8m"]

想立刻同步一次,不等下个周期 docker compose restart syncbot,或者用 -once 单独跑一次。


开发

go test ./...          # 单元测试 + 跑真实 git 的端到端测试
go test -race ./...
go vet ./...
go build .

代码结构和设计约定见 AGENTS.md

镜像由 GitHub Actions 构建并推送到 ghcr.io/OWNER/syncbot main 分支推 latest,打 v* 标签推对应语义化版本,支持 amd64 / arm64。