先给判断
自动发布最容易被低估的不是生成文章,而是回答“这一篇是否已经被另一个运行发布过”。把激活状态和队列放在本地 ignored 文件里,只能支持单进程演示;一旦出现两个 worker、进程中断或生产已成功但回执未写入,系统就无法可靠判断下一步。
Omnihex 的 R1 把状态放在不连接 Pages 部署的 automation/content-pipeline-state 分支。它不是第二条发布分支,而是 activation、candidate、run、publication 和精确 public manifest 的唯一状态所有者。
CAS 的含义
Git 本身不是数据库,但一个受限的 fast-forward push 可以充当简单的 compare-and-swap。运行先读取 state_head,在本地计算 claim,再只允许从这个旧 head 快进推送。如果另一个运行先写入,远端 head 改变,旧运行的 push 被拒绝;正确行为是 fetch、重读并安全退出,而不是 force push。
read state_head
-> claim candidate with run_id
-> commit
-> push state_head..new_head
-> if rejected: another worker won; reconcile and stop
这不是分布式锁的完整替代品。它依赖正常 Git 远端、明确的身份字段和失败时不重试写入的纪律,但对“一个候选只能被一个运行领取”已经足够有用。
为什么要拆成多种 receipt
candidate 说明要处理什么:logical ID、source hash、content hash、slug 和 localized routes。run 说明这次执行经历了哪些状态、用了哪些来源、通过了哪些 QA。publication 说明哪个 production SHA、部署和实际路由完成了发布。public-manifest 则回答当前生产到底有哪些公开路由。
把四类对象混成一条日志,会让恢复变得模糊。生产已经成功但 publication receipt 没写入时,reconcile 应比较 live SHA、内容 hash 和路由,确认是同一个候选后补写回执;它不应再次发布。
幂等检查的最小集合
同一运行不得重复创建:
run_idcandidate_idlogical_content_idsource_hash和content_hashslug/locale- 每个 canonical route
- publication ID
这些不是为了让状态文件看起来复杂,而是避免一个“重试”变成第二篇文章、第二套图片或第二次生产部署。
状态分支不能变成部署旁路
状态分支不应触发 Pages Preview 或 production。它只存非敏感 JSON,生产仍沿用 bot worktree → main → validate → publish-production → production。这样内容状态可以耐久化,却不会因为状态提交直接公开草稿。
这条边界也解释了为什么 exact public manifest 很重要。发布前计算 previous_public_set + exactly_one_candidate + deterministic_derived_routes,再与构建产物比较。数量增加并不等于正确;必须知道增加了哪几个 URL,草稿和无关路由都不能出现。
失败与修复
状态 head 漂移:fetch、重读、放弃本次 claim。锁残留:只有超过 stale threshold 且能证明原进程不存在时才恢复。生产已成功、回执缺失:reconcile,不再发布。live hard gate 失败:按现有非破坏性回滚流程保存证据并停止。
适用边界
这种 Git-native 状态适合低频、可审计、候选数量有限的内容发布。它不是高并发消息队列,也不是跨平台营销数据库。若未来接入多个外部草稿箱,应增加 adapter receipt,但仍保持 state branch 为唯一状态 owner。
来源与边界
Git 文档支持 fast-forward push 和 linked worktree 的基础语义;本文关于 CAS、receipt、manifest 和 reconcile 的细节来自 Omnihex 当前公开仓库实现与测试,是工程案例,不是 Git 官方的分布式一致性保证。
