这次对 A Tour of Go 多语言项目的 Production 流程做进一步优化,起因其实很简单:Go 官方终于把我维护的 4 个翻译站链接加入了官方网站。
法国、德国、韩国和简体中文这 4 个站点进入官方 A Tour of Go 的其他语言列表之后,我自然需要再次同步 golang/website 上游。
原本我以为这只是一次普通的 upstream sync。
真正开始执行之后才发现,随着目前已经上线的语言站增加到 10 个,“同步一次上游,然后把所有站点重新发布上线”这件事情,本身已经值得继续标准化。
最终,这次维护把原本分散的 Production 操作进一步收敛到了一个命令。
起点:Go 官方加入了 4 个翻译站链接
Go 官方网站仓库合并了 _content/tour: add translations,新增了 French、German、Korean 和 Simplified Chinese 四个 A Tour of Go 翻译入口。

golang/website commit,新增 4 个翻译站链接】对我来说,这次变化还有另一层意义。
之前这些语言站只是我自己维护和推广的社区翻译站。进入 Go 官方页面之后,官方 upstream 本身开始包含指向这些站点的链接。
因此,我自己维护的 fork 也必须重新同步上游,否则项目中的 source baseline 就会落后于官方。
随后,我的仓库完成了两步:
59b8b55:
chore: 同步官方 Go Tour 上游基线
以及:
852f2cf:
chore: 完成官方上游同步后的多语言发布准备

从图里也能看到,这次同步并不只是改一行链接。
除了 upstream metadata、Tour 页面之外,还涉及不同语言的 Locale Surface Review evidence、Production 相关测试和发布准备。
这也是多语言项目越来越大以后很明显的一个变化:一个很小的 upstream diff,最终可能需要传播到所有正式语言站。
问题很快从“怎么同步”变成了“怎么重新发布全部站点”
同步 upstream 本身并不是这次最麻烦的部分。
真正让我开始继续优化流程的,是后面的 Production 发布。
目前项目已经有 10 个正式 live locale:
简体中文、日语、德语、法语、韩语、西班牙语、意大利语、荷兰语、巴西葡萄牙语和土耳其语。
如果每次共享源码发生变化之后,都要手工逐个完成 publish、deploy、CDN purge、machine acceptance 和 browser acceptance,那么随着 locale 数量继续增长,维护成本会越来越明显。
而且这种工作有一个特点:绝大多数步骤并不需要重新“思考”,它们本质上都是确定性的 Production 操作。
真正需要解决的问题因此变成了:
怎样把已经验证过的 Production gate 保留下来,同时把重复的机械执行收敛成一个正式工作流?
这几天的 Git 历史正好记录了这个演进过程。

从时间线上可以看到:
先同步官方 Go Tour upstream,然后完成多语言发布准备;随后开始自动化 Production CDN purge 和批量维护;再把 Production 发布与 shared assets 纳入批量流程;最后继续完善批量恢复和 shared assets verification。
这不是一开始就设计出来的一套“大而全”自动化。
相反,它是在真实 Production 使用过程中,一层一层收敛出来的。
最终只需要一个正式入口
现在,对于已经 live 的全部语言站,一次完整的 maintenance release 可以从下面这个命令启动:
scripts/production-release-batch.sh \
--all-live \
--output-root /tmp
--all-live 命令启动完整批量发布】今天这次真实运行中,命令首先导出了 shared assets:
files=14 bytes=971973随后进行 assets validation,然后自动进入 publish。
10 个 locale 会依次完成 projection/build、production binary build、Chrome prerender、runtime validation 和 manifest/checksum。
例如每个正式语言版本都会再次确认:
ready=122
pending=0
blocked=0
pages=103
articles=7整个 publish 阶段本身就持续了相当长时间。日志中最终记录:
stage=publish PASS elapsed=1288.3s这也说明,所谓“一键发布”并不是把原来的严格流程删掉。
恰恰相反,是把原本需要维护者反复手工调用的正式步骤组织到了同一个 orchestration 里面。
一个命令,不代表跳过 Production gate
批量发布之后,流程还会继续做正式 Production maintenance。
其中 Phase A 负责 deploy 和 CDN purge。
以中间的韩语站为例,部署过程中仍然会检查 release SHA-256、远端 staging、权限、远端 SHA-256、systemd service health、连续 HTTP 200,以及公网访问。
部署完成后再进行对应 hostname 的 CDN purge。
然后才进入下一个 locale。

这里我后来专门继续优化了一件看起来很小、实际却非常影响使用体验的事情:进度输出。
长时间 Production workflow 如果只不断打印 curl、SSH 或部署细节,维护者很容易不知道:
现在到底在第几个语言?
上一站有没有完成?
还有几个?
当前是 deploy、purge,还是后面的 machine acceptance?
因此现在会明确输出:
[phase A] locale=ko-KR 5/10 stage=purge PASS
[batch] phase=A completed=5/10 remaining=5
[phase A] locale=es-ES 6/10 stage=deploy START这类输出本身不会改变 Production 结果,但对于一个可能持续几十分钟的正式流程来说,可观测性非常重要。
Phase B 仍然逐站进行 machine 和 browser acceptance
完成 deploy 和 purge 并不意味着发布结束。
后面的 Phase B 还要继续执行 machine acceptance 和 browser acceptance。
Machine acceptance 中会检查 release identity、remote identity、正式路由、CDN cache observation、HTML identity、sitemap、socket boundary 等。
Browser acceptance 则继续检查 desktop routes、mobile 页面,以及 Run、Format、Reset、SPA 和广告相关行为。
也就是说,现在的“一键”只是减少维护者重复输入命令,并没有把验收标准简化成一句:
“首页 HTTP 200 就算上线成功。”
这也是我希望这套自动化一直保持的边界。
自动化的目标是减少机械劳动,而不是降低 Production 标准。
一次真实的 curl exit 28,反而证明了重试机制的价值
今天这轮 10 语言正式 Production 还有一个很有意思的小插曲。
轮到荷兰语 nl-NL 执行 sitemap verification 时,其中一个 URL 第一次请求出现:
curl-exit-28
HTTP-000日志中可以看到:
attempt=1/5
reason=curl-exit-28
next=retry
backoff=1s随后第二次请求恢复:
attempt=2/5 PASS最终结果仍然是:
sitemap URLs: 105/105
host mismatch: 0
HTTP failure: 0
PRODUCTION MACHINE ACCEPTANCE: PASS
这次 failure 不是为了测试而人为制造的,而是在真实 Production 中自然出现的网络瞬态问题。
这也是我前面一直在完善 bounded retry 的原因。
公网、CDN、跨境网络和第三方 API 都不可能保证每一个请求永远第一次成功。
如果一个短暂超时就立即让整个十语言批次失败,会产生大量误报;但如果脚本无条件吞掉错误继续往下走,又会破坏 fail-closed 原则。
比较合理的做法是区分:
短暂网络异常可以在有限次数内 retry;
恢复成功需要明确记录 recovered;
如果最终仍然失败,则正式 gate 依然不能通过。
这次真实的 curl exit 28 → recovered → PASS,至少说明当前这个方向是有实际价值的。
最终结果:10 个语言全部完成
这次完整 maintenance release 最后的结果是:

10 个 locale 的:
deploy
purge
machine
browser
全部 PASS。
最终汇总为:
PASS=10
FAILED=0
SKIPPED=0
PENDING=0Shared assets 同样完成:
export PASS
deploy DEPLOYED
purge PASS
verify PASS最后得到:
PRODUCTION RELEASE BATCH: PASS
locale maintenance: COMPLETE日志中的最终汇总还把每个 locale 的 publish、deploy、purge、machine、browser 状态分别列了出来,因此即使未来某一个语言失败,也能够明确知道 failure 位于哪个阶段,而不是只得到一个笼统的“批量发布失败”。
这次优化真正解决的不是“少敲几个命令”
如果只是为了少输入几行 shell,其实没有必要花这么多时间完善这一套流程。
我更看重的是另外几件事。
随着语言站数量不断增加,一个操作能不能线性扩展,会越来越重要。
现在是 10 个 locale。
如果以后变成 20 个、30 个甚至更多,那么任何需要维护者“逐站手工执行”的步骤都会逐渐成为瓶颈。
而且,手工执行越多,越容易出现另一类问题:
不是代码有 bug,而是某个语言忘记 deploy、某个 CDN 忘记 purge、某个 acceptance 没有执行。
把这些步骤收敛成正式 workflow 之后,维护者的职责开始发生变化。
以前更像是:
“记住所有命令,并且一条一条正确执行。”
现在则更像是:
“启动正式 workflow,观察明确的阶段、进度和 failure evidence,在真正需要人工判断的时候介入。”
我认为这才是这次优化更重要的价值。
自动化越多,反而越需要保留清楚的人工边界
这段时间做 A Tour of Go 多语言项目,我越来越倾向于把工作分成两类。
一类是机器非常适合做的事情:
build、checksum、publish、deploy、CDN purge、HTTP verification、browser automation、状态汇总。
另一类是不能因为自动化方便,就让机器替代的判断:
翻译质量审核、Production HUMAN visual gate,以及遇到真正异常之后对 failure evidence 的判断。
所以我现在并不追求“所有事情一个命令全部做完”。
我更希望实现的是:
能机械化的部分尽可能可靠地机械化;
需要人工判断的 gate 则明确保留下来。
这两者并不冲突。
反而只有机械操作越来越稳定之后,我才可以把更多时间放在真正值得人工投入的地方。
从一次 upstream sync,继续推动整个 Production 流程成熟
回过头看,这次事情的起点其实只是 Go 官方新增了 4 条翻译链接。
我同步了一次 upstream。
然后因为需要重新发布全部语言站,开始重新审视现有 Production workflow。
接下来又陆续完善了 CDN purge、批量 maintenance、shared assets、恢复机制、实时进度和 failure diagnostics。
最终,10 个正式语言站可以从一个命令开始,自动完成整套 maintenance release,并在最后给出明确的逐 locale 验收结果。
这应该不会是这套流程最后一次演进。
以后随着新的 locale 继续增加,应该还会出现新的规模化问题。
不过至少现在,“官方 upstream 又更新了,需要重新发布所有正式语言站”这件事,已经不再意味着我要手工重复操作十遍。
对于一个计划长期扩展到更多语言的项目来说,这一步很重要。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ Brazilian Portuguese — Português (Brasil)
✅ Dutch — Nederlands
✅ French — Français
✅ German — Deutsch
✅ Italian — Italiano
✅ Japanese — 日本語
✅ Korean — 한국어
✅ Simplified Chinese — 简体中文
✅ Spanish — Español
✅ Turkish — Türkçe
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
项目正在持续扩展更多语言版本,并长期维护翻译质量、生产发布与后续更新。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。
