代码能跑 ≠ 代码能上线。从 main 分支合到生产环境,中间这一段路才是事故的高发区。前面五天我们讲了 DEFINE、PLAN、BUILD、VERIFY、REVIEW,今天是六阶段流水线的最后一站:SHIP。它关心的是:怎么把代码安全、可逆、可观察地送到用户手里。

agent-skills 在 SHIP 阶段放了四个技能:ci-cd-and-automation、observability-and-instrumentation、shipping-and-launch、deprecation-and-migration。它们四个刚好对应上线的四件事——自动化流水线、埋点观测、上线动作本身、以及下线。

一句话定位

ci-cd-and-automation 把”每条规则都要人记”变成”每条规则流水线强制”;
observability-and-instrumentation 把”上线后才知道有没有问题”变成”上线那一刻就知道在哪看”;
shipping-and-launch 把”一键部署”变成”可逆、可观察、分阶段的上线流程”;
deprecation-and-migration 把”代码只会越写越多”变成”主动把不赚钱的代码清掉”。

四个技能串起来,就是一条完整的”上线 → 观测 → 演进 → 退役”链路。

ci-cd-and-automation:把检查前移

这个技能的核心原则只有两条,但都是反直觉的:

第一条叫 Shift Left。把检查尽量往前推,能在写代码那一秒发现的,就不要等到联调再发现。一个 lint 错误在编辑器里 5 秒能改掉,到 CI 里要 30 秒,到生产环境里可能就是一次回滚事故。

第二条叫 Faster is Safer。小批量、高频次发布反而更安全。这条原则来自 DORA(DevOps Research and Assessment)的多年研究:发布频率越高的团队,变更失败率越低,事故恢复时间越短。一次发布 30 个改动的版本,比一次发布 3 个改动的版本难回滚得多。

整条流水线画出来长这样:

flowchart LR
    PR["PR 打开"] --> L["Lint
eslint / prettier"] L --> T["Type Check
tsc --noEmit"] T --> U["Unit Tests
jest / vitest"] U --> B["Build
npm run build"] B --> I["Integration
API / DB"] I --> E["E2E
Playwright"] E --> S["Security Audit
npm audit"] S --> BS["Bundle Size
预算检查"] BS --> R["Ready for Review"]

每一关都不可跳过。SKILL.md 里专门写了一行:

No gate can be skipped. If lint fails, fix lint — don’t disable the rule. If a test fails, fix the code — don’t skip the test.

意思是:CI 红了,唯一正确的处理方式是修代码,不是关规则、不是 skip 测试。任何”为了过 CI 所以我注释掉了这个检查”的做法,都是在给未来的事故埋种子。

我们本博客系统就有一个真实的例子——deploy.sh 这个脚本做的事情非常朴素:

1
2
3
4
5
6
7
8
#!/usr/bin/env bash
set -euo pipefail
cd /Users/dongshan/webwork/dong_blog
git pull --rebase
npm install
hexo clean
hexo generate
hexo deploy

它对应的就是”自动化部署”这四个字。每次发布前人肉 ssh 上来敲命令,不仅慢,还会让”发布当天是不是网络挂了”这种问题纠缠不清。脚本一上,同样的事变成幂等、可重试、可回放的。这就是 CI/CD 的最小闭环。

CI 跑挂了怎么办

技能里专门讲了一种反馈循环:当 CI 失败时,把报错粘回给 AI 让它修:

1
2
3
4
5
6
7
8
9
10
11
12
13
CI fails


Copy the failure output


Feed it to the agent:
"The CI pipeline failed with this error:
[paste specific error]
Fix the issue and verify locally before pushing again."


Agent fixes → pushes → CI runs again

这个模式把”AI 写代码”和”CI 守住质量”两件事串成了一个回路。AI 负责生成,CI 负责挑刺,CI 的反馈又喂回给 AI 让它修正。这条反馈链跑顺了,AI 编码才能真正进入”自校正”模式。

CI 优化优先级

流水线超过 10 分钟就会被人开始嫌弃。技能给了一份优化清单,按投入产出排序:

1
2
3
4
5
6
7
Slow CI pipeline?
├── Cache dependencies(缓存 node_modules)
├── Run jobs in parallel(lint、typecheck、test 分并发)
├── Only run what changed(按路径过滤跳过无关 job)
├── Use matrix builds(分片测试)
├── Optimize the test suite(慢测试挪出关键路径)
└── Use larger runners(换更快的 runner)

最快的收益是前两条——缓存和并行。其它都是后话。

observability-and-instrumentation:代码不可观察 = 代码不可运维

这个技能开篇就一句值得背下来的话:

Code you can’t observe is code you can’t operate.

如果一个功能上线后没人看得见它在干嘛,那它和没上线没区别——只是多了几行占内存的代码。

埋点不是上线以后再补的,是和功能一起写的。这一点和测试是平级的:你会写完代码不写测试就提交吗?理论上不该,埋点也一样。

三件套:日志、指标、追踪

信号 回答的问题 成本特征
结构化日志 “这个 case 具体发生了什么?” 按事件数增长
指标 “整体上多快、多频繁?” 每条 series 成本固定,便宜
追踪 “跨服务时间花在哪?” 每请求一份,可采样

一句话记忆:metrics 告诉你有问题,traces 告诉你在哪,logs 告诉你为什么

写日志时最常见的反模式是字符串拼接:

1
2
3
4
5
6
7
8
9
10
11
// BAD:拼出来的字符串没法过滤、没法聚合
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);

// GOOD:稳定的事件名 + 结构化字段
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'payment failed');

前者在生产里就是一坨文本,后者在 ELK、Loki、Datadog 里能直接按字段聚合。”支付失败率突增 3 倍,按错误码分组”这种查询,前者做不了,后者一行 SQL。

关联 ID 是必须的

没有 requestId,每一行日志都是孤儿。微服务里一个请求穿五六个服务,等出问题时你拿着 6 个服务的日志对时间戳——这是考古,不是调试。

1
2
3
4
5
6
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});

边界处生成或接受 ID,子 logger 挂上,所有下游调用都带上。看起来很基础,但 80% 的生产系统没做对。

告警要告”症状”不要告”原因”

这是一条非常容易踩坑的规则:

1
2
3
4
SYMPTOM(值得叫醒人):      CAUSE(放 dashboard,不叫人):
error rate > 1% for 5 min CPU at 85%
p99 latency > 2s one pod restarted
queue age > 10 min disk at 70%

CPU 85% 不一定意味着用户受害,但错误率 > 1% 一定意味着用户受害。前者叫醒人可能是误报,后者叫醒人几乎一定是真问题。

每条告警还要满足四个硬条件:

  1. 必须 actionable。如果处理方式是”忽略它,它自愈”,直接删告警。
  2. 必须带 runbook 链接。哪怕只有三行:这是啥、第一手查什么、谁接。
  3. 必须有阈值和持续时间,且这个阈值要能回溯到 SLO 或历史数据,不能拍脑袋。
  4. 只分两档:page(用户受影响,现在动)和 ticket(性能下降,本周内动)。三档以上就开始有人”反正先 ack 再说”了。

与 debugging 的区别

这个技能专门写了一行 NOT for:

NOT for: Diagnosing a failure happening right now — use the debugging-and-error-recovery skill (observability is what makes that skill fast next time).

区别很清晰:debugging 是当下救火,observability 是为下次救火铺路。两者不是替代关系,是前后关系。

shipping-and-launch:可逆、可观察、分阶段

CI 跑过了、可观测性也埋好了,接下来才是上线动作本身。shipping-and-launch 这个技能的核心是三件事:上线清单、灰度发布、回滚预案。

上线清单

清单很长,分六块:代码质量、安全、性能、可访问性、基础设施、文档。重点是这块不能省——很多团队上线靠的是”看着差不多就上”。技能里给的最小集是:

  • 所有测试通过(unit / integration / e2e)
  • 构建成功,无警告
  • 无 TODO 注释遗留
  • console.log 残留
  • 错误处理覆盖预期失败模式
  • 关键依赖审计无 high/critical 漏洞
  • 健康检查端点存在并响应

看着啰嗦,但这是用事故换出来的。漏掉”健康检查端点”那一行,生产环境里 service 进程死了没人知道,直到用户报 bug。

分阶段灰度

灰度的核心是”小流量先验证,再逐步放大”。整个序列画出来:

sequenceDiagram
    participant S as 调度系统
    participant F as Feature Flag 服务
    participant U1 as 1% 用户
    participant U2 as 10% 用户
    participant U3 as 50% 用户
    participant U4 as 100% 用户

    Note over S,F: 步骤 1: 代码部署,flag=OFF
    S->>F: 设置 rollout = 1%
    F->>U1: 命中 flag,启用新功能
    Note over S,F: 监控 15-30 分钟
    S->>F: 设置 rollout = 10%
    F->>U2: 命中 flag,启用新功能
    Note over S,F: 监控 1-2 小时
    S->>F: 设置 rollout = 50%
    F->>U3: 命中 flag,启用新功能
    Note over S,F: 监控 4-8 小时
    S->>F: 设置 rollout = 100%
    F->>U4: 全部启用

每一步之间都要等数据说话,不能急着推。技能给了一份”什么时候可以往前推”的判断表:

指标 推进(绿) 暂停排查(黄) 回滚(红)
错误率 与基线差距 < 10% 高于基线 10%-100% > 2 倍基线
P95 延迟 与基线差距 < 20% 高于基线 20%-50% > 50%
客户端 JS 错误 无新错误类型 新错误 < 0.1% 会话 > 0.1%
业务指标 中性或正向 下滑 < 5% 下滑 > 5%

这张表的好处是把”要不要回滚”从主观判断变成了对照表。开会时大家对着表看一眼,不用吵。

回滚策略

回滚是上线动作的一部分,不是补救措施。每一次上线前,回滚方案必须就位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## Rollback Plan for [Feature/Release]

### Trigger Conditions
- Error rate > 2x baseline
- P95 latency > [X]ms

### Rollback Steps
1. Disable feature flag (if applicable)
OR
1. Deploy previous version: `git revert <commit> && git push`
2. Verify rollback: health check, error monitoring

### Time to Rollback
- Feature flag: < 1 minute
- Redeploy previous version: < 5 minutes
- Database rollback: < 15 minutes

三种回滚方式,按响应时间排序:

  • Feature flag 关闭:秒级,最快。适合功能层面可分离的改动。
  • 前一版本镜像/部署:分钟级。适合 flag 兜不住的情况。
  • 数据库迁移回滚:最慢,需要预先测试好 down 路径。

技能里专门强调了一句很重的话:

Rolling back is responsible engineering. Shipping a broken feature is the failure.

回滚不是认怂,是负责任。硬撑着不回的代价是 P0 事故 + 用户损失 + 团队士气打击。

启动日监控清单

上线后第一个小时是黄金一小时。技能给的 checklist:

1
2
3
4
5
6
1. Check health endpoint returns 200
2. Check error monitoring dashboard (no new error types)
3. Check latency dashboard (no regression)
4. Test the critical user flow manually
5. Verify logs are flowing and readable
6. Confirm rollback mechanism works (dry run if possible)

最后一条特别容易忘:dry-run 一次回滚。真到回滚的时候你才发现按钮点了没反应、镜像拉不下来、脚本权限不对——这就是事故叠加事故。

deprecation-and-migration:顺带说一下

这一篇不是 SHIP 阶段的主菜,但同属 SHIP 阶段,必须提一下。这个技能的核心论点只有一句:

Code is a liability, not an asset.

代码是负债,不是资产。每一行代码都有持续维护成本——bug 要修、依赖要升级、安全补丁要打、新人 onboarding 要看懂。”我们写了好多代码”从来不是成绩,”我们少维护多少代码”才是。

弃用策略

弃用分两档:

类型 何时用 机制
Advisory(劝导式) 迁移可选、旧系统稳定 警告、文档、引导
Compulsory(强制式) 安全风险、阻塞进展 硬截止日期 + 迁移工具

默认用 Advisory。只有当维护成本或风险已经超过迁移成本时,才用 Compulsory。而且 Compulsory 必须配迁移工具——你不能只发个”将于 X 月 X 日下线”,那叫通知,不叫弃用。

数据库迁移:Expand → Migrate → Contract

最危险的迁移是数据库 schema,因为数据是唯一不能靠”回滚部署”恢复的东西。规则是永远不要原地改列,分三步走:

1
2
EXPAND ─────────→ MIGRATE ─────────→ CONTRACT
加新列(nullable) 双写新老列 确认没人读旧列后再删

举例:要把 name 改名为 full_name

  1. Expand:加 full_name 为 nullable,部署。旧代码无视它,没事。
  2. Migrate:应用双写 namefull_name,部署。
  3. Backfill:分批把 name → full_name 复制过去,分批是为了不锁表
  4. Switch reads:应用切到读 full_name,但仍双写,部署并观察。
  5. Contract单独的、靠后的部署,停写 name 然后删列。

每一步都是独立可部署、可回滚的。如果第 4 步出问题,回滚代码,full_name 还在被写入,没有数据丢失。

反合理化表里的金句

技能里这张表非常值得背:

借口 现实
“它还能跑,干嘛删” 能跑但没人维护的代码会堆积安全债和复杂度
“也许以后会用到” 以后要用就重建,”以防万一”的成本比重建还贵
“改一列名就一行代码” 灰度期间新老代码一起跑,一个会读到不存在的列
“我们会写回滚脚本的” 一个没有 down 路径的迁移是一个不能回滚的部署

最后一行是金句——“我们会写回滚脚本的”永远等于”我们不会写的”。

写在最后

SHIP 阶段看起来是”部署 → 验证 → 完事”,但它实际上是六阶段里最容易出事的。前面五个阶段做好了,SHIP 阶段只是收尾;前面五个阶段有缝,SHIP 阶段就是替它们兜底。

把今天四个技能串起来:

  • ci-cd-and-automation 保证”上生产前”的门。
  • observability-and-instrumentation 保证”上生产后”的眼。
  • shipping-and-launch 保证”上”的姿势。
  • deprecation-and-migration 保证”下”的姿势。

四件事缺一不可。一个团队如果只有 CI 没有可观测性,那是有门没眼;如果只有可观测性没有灰度,那是有眼没刹车。

明天开始我们换个方向:怎么把这一整套 agent-skills 装到 Claude Code / Cursor / Gemini / Codex 里。踩过的坑、安装顺序、配置技巧——一篇实操指南。


参考资料