多人加 AI 维护同一个线上服务:让每次发布都可追溯
当几个人各自用不同的 AI 编程工具开发、并且都能部署同一个线上服务时,最先出问题的往往不是代码,而是"线上到底跑的哪份代码"。整理一套低成本做法:版本号进镜像、按镜像 ID 回滚、先推送再部署、AGENTS.md,以及只能手动触发的 Claude Code skill。
这篇和前几篇 Claude Code 系列不太一样,讲的不是工具配置,而是一个越来越常见的场景:几个人各自用 AI 编程工具开发同一个线上服务,而且每个人(以及每个人的 AI)都可能直接部署。
在这种场景下,最先出问题的往往不是代码本身,而是一个看似简单的问题:线上现在跑的到底是哪份代码? 这篇整理我在这类场景里用过、觉得成本低收益高的一套做法。
一、典型的混乱#
如果没有约定,很快就会出现这些情况:
- 服务器上的源码目录和线上镜像不一致,里面还混着某个人没提交的改动。
- 镜像 tag 用
:latest或功能名,并且会被覆盖:同一个 tag 今天指向 A,明天指向 B。 - 有人(或者有人的 AI)在服务器上直接改代码、构建、部署,提交留在服务器上没推送。
- 你基于 GitHub 主线开发完准备上线,才发现主线落后于线上:直接部署会把别人已经上线的功能覆盖掉。
到了这一步,“回滚到上一个版本”都无从谈起,因为没人说得清上一个版本是什么。
二、版本可追溯:三件事#
1. 镜像带源码版本号,回滚按镜像 ID#
构建时把提交号写进镜像:
docker build \
--build-arg SOURCE_REVISION=$(git rev-parse HEAD) \
-t app-frontend:<功能>-<短sha>-<日期> frontendbash- Dockerfile 里用
LABEL org.opencontainers.image.revision=$SOURCE_REVISION写进镜像元数据,docker inspect就能看到。 - 前端额外暴露一个
/api/version接口,返回构建时的提交号,在浏览器里就能确认线上前端是哪个提交。 - tag 命名为
<功能>-<短sha>-<日期>,新版本用新 tag,绝不复用。
更重要的一条:核对线上版本、记录回滚点,一律用镜像 ID,而不是 tag 名。
docker inspect -f '{{.Image}}' app-frontend-1 # 容器实际运行的镜像 IDbashtag 是可变的指针,镜像 ID 是内容哈希。回滚点记录成”tag + 镜像 ID 前 12 位”,回滚前先核对镜像 ID 对不对得上。
一个顺带的小优化:如果版本 LABEL 写在 Dockerfile 靠前的位置(比如安装 ffmpeg 之前),版本号一变,后面所有层的缓存都会失效,每次构建都要重装一遍。把 LABEL 挪到这些重层之后就好了。会变的东西放在 Dockerfile 的后面。
2. 先推送,再部署#
规则很简单:要部署的提交必须已经推送到 GitHub(推到哪个分支都可以),构建前工作区必须干净。
这样任何时候都能从线上镜像的 revision 反查到 GitHub 上的源码。已经在服务器上直接上线、但没推送的提交,先原样推送到 GitHub(保留作者和提交号,线上的版本号才对得上),再用 merge --no-ff 合进主线——不用 rebase,因为 rebase 会改变提交号,线上镜像就对不上任何一个提交了。
3. 不用 bind mount 发版#
一种常见的”快速修复”是把新编译的二进制通过 bind mount 挂进容器,替换镜像里的文件。这样做之后,docker inspect 看到的镜像根本不是实际在跑的代码。规则是:改代码就提交、推送、构建新镜像,不用 bind mount 代替发版。
三、发布本身的几个坑#
旧标签页打开面板报 ChunkLoadError#
Next.js 每次构建,/_next/static 下的 chunk 文件名都会变。切换前端之后,还开着旧页面的用户一点开某个懒加载的面板,请求的是旧 chunk,新容器里已经没有了,于是加载报错。
解决办法:切换前,把当前线上前端容器里的 .next/static 归档到宿主机的一个目录,再配置 nginx:新容器里找不到的静态文件,回退到归档目录去找。下面是表达这个思路的示意配置:
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
proxy_intercept_errors on;
error_page 404 = @static_archive;
}
location @static_archive {
root /srv/app/static-assets; # 历次归档的 _next/static
}nginx归档只新增、不覆盖。遇到同名但内容不同的文件就停下来检查:chunk 文件名带内容哈希,同名不同内容说明哪里出了问题。
后端没有优雅退出:等”安全窗口”#
如果后端重启会切断进行中的请求、让进程内的任务卡住,那么后端上线前就要等一个安全窗口:最近几分钟没有关键请求提交,也没有在途任务,再执行切换。切换脚本里先检查这一条,不满足就自动放弃,等下一次。高峰期等不到窗口就不强切,改到低峰。
部署留痕#
每次部署后,在服务器上的 DEPLOY_LOG.md 末尾追加一行:
| 时间 | 服务 | 镜像 tag | 镜像 ID 前 12 位 | 完整 sha(已推送) | 谁 | 说明 |plaintext每次开工先看这个文件的最后几行,就知道最近谁部署了什么。
四、把约定写给 AI 看:AGENTS.md#
这些规则如果只存在于某个人的笔记里,其他人的 AI 工具是不知道的。所以要在仓库根目录放一个 AGENTS.md,写给所有在这个仓库里工作的 AI 编程工具。一个精简的模板:
# AGENTS.md — 协作约定(给所有在本仓库工作的 AI 编程工具)
本服务已上线。多位维护者各自用 AI 工具开发和部署,
为了互不覆盖已上线的改动,请遵守以下约定。
## 1. 代码以 GitHub 主线为准
- 开发前先 fetch,基于主线新建分支或 worktree。
- 部署前必须先把要上线的提交推送到 GitHub。
## 2. 构建
- 在单独的 clone 或 worktree 里构建,构建前工作区必须干净。
- 必须带版本号:--build-arg SOURCE_REVISION=$(git rev-parse HEAD)
- 新版本用新 tag,不要复用或覆盖已有 tag。
## 3. 部署
- 只改 compose 里对应服务的 image 行,然后 up -d --no-deps <服务>。
- 不要用 bind mount 替代发版。
- 后端重启前确认没有在途任务,并提前通知其他维护者。
## 4. 留痕
- 每次部署后在 DEPLOY_LOG.md 末尾追加一行。
## 5. 来不及遵守时
- 紧急修复可以先上线,但事后必须提交推送、记录日志、告知其他维护者。markdown第 5 条很重要:规则要给紧急情况留出口。没有出口的规则会被绕过,而且绕过之后不留痕迹。
五、我这边:四个只能手动触发的 Claude Code skill#
我把日常操作固化成了四个 skill,都设置了 disable-model-invocation: true,只能由我手动输入命令触发,模型不能自己决定去执行:
| skill | 做什么 | 关键约束 |
|---|---|---|
/preflight | 开工只读检查 | 全程只读。对比笔记里记录的版本和线上实际镜像 ID;检查近 24 小时的 sshd 登录、构建目录里有没有未推送的提交、近 1 小时 5xx、磁盘 |
/sync | 把已上线但还没进主线的改动收进来 | 先查清每个服务对应的源码;收进单独的 sync/ 分支后停下等我审核,审核通过才合进主线 |
/deploy-frontend | 前端上线 | 构建 → 归档旧静态资源 → 停下等我确认可以切换 → 切换(脚本内检查安全窗口)→ 核对线上版本号 → 写 DEPLOY_LOG |
/wrap-up | 收工整理 | 检查本地有没有遗留的临时补丁,更新状态笔记并提交 |
几个设计原则:
- 只读和写操作分开。预检全程只读,发现异常只报告,由我决定下一步。
- 不可逆操作前必须停下。部署切换、合进主线之前,skill 会明确停住等我确认。
- 不相信 tag,只相信
docker inspect。每次部署前都重新核对实际镜像 ID,因为别人的部署随时可能改动线上。 - 登录审计要精确。统计登录时只看 sshd 的
Accepted行,否则 grep 会匹配到 sudo 日志里记录的自己的命令。 - 一个小细节:skill 里 awk 的字段要写成
$(1)而不是$1,因为 skill 的参数替换会把$1当成传给 skill 的参数替换掉,命令就坏了。
另外我会维护一份 status.md,第一节固定是”三处版本对照”:GitHub 主线、本地、线上镜像,每次部署后更新。它既是给自己看的,也是给 Claude Code 每次开工时读的上下文。
六、收获#
- 多人(多 AI)协作的第一要务是可追溯:任何时候都能回答”线上跑的是哪个提交”。版本号写进镜像、核对用镜像 ID、先推送再部署,这三件事成本很低,收益很大。
- 约定要写在仓库里,写给 AI 看。AI 工具会读
AGENTS.md,但不会读你的脑子。 - 让 AI 做重复的检查,把关键决策留给人。skill 负责把”该查的都查一遍”,部署、合并这类不可逆的决定由人来确认。
- 给规则留紧急出口,并要求事后补齐留痕。
- 流程不能保证所有人都照做,所以预检的核心不是”相信流程”,而是”每次开工都重新核对现实”。