有一次 TCF Canada 的 Backend 已經部署完成。

Railway 顯示:

status = SUCCESS

Backend readiness 也正常:

status = ok
database = reachable

CLI 的 deployment message 裡甚至有我預期的 commit SHA。

照一般流程來看,這次部署應該可以繼續了。但我還是讓 Pipeline 停下來,沒有部署 eval-worker,也沒有發布 Frontend。

原因是 Railway 回傳的:

metadata.commitHash = null

我可以證明某個版本正在正常運作,但不能證明它就是剛剛通過 CI 的版本。

CI 綠不等於正式環境正確

很多 Pipeline 最後只檢查:

  • CI 通過
  • Provider 顯示 SUCCESS
  • Health endpoint 回傳 200
  • 網站可以打開

這些條件都很重要,但還是不夠。因為它們沒有回答:正式環境現在執行的是哪一個 commit?

如果 CI 通過後,main 又多了一個 commit;部署腳本重新 checkout 最新 main,就可能出現:

CI 驗證 A
正式環境部署 B

兩邊都顯示成功,但部署出去的 B 根本沒有通過那次 CI。

Exact-SHA 是什麼

我後來要求整條發布鏈都要對上相同 SHA:

CI SHA
  = Deployment source SHA
  = Railway API SHA
  = Railway Worker SHA
  = Cloudflare marker SHA
  = Release receipt SHA

SpeakUp 的 deployment workflow 不會在發布時重新猜最新 main。它使用觸發該次 CI 的 workflow_run.head_sha,先 checkout 那個 exact SHA,再開始部署。

這樣 CI 驗證的是誰,部署的就是誰。

Health check 證明健康,不證明身份

這是我在 TCF Canada 遇到的狀態:

Railway status       = SUCCESS
Backend /ready       = ok
Database             = reachable
CLI message          = 包含預期 SHA
metadata.commitHash  = null

前四項只能證明服務可以運作,不能取代最後一項的版本身份。

所以 verifier 拒絕通過,流程停在:

provider-verification blocked

我沒有重複部署 Backend、提前部署 eval-worker、用 CLI message 取代 commit metadata,也沒有因為 health 正常就假裝完成。

這次雖然沒有順利上線,但 Pipeline 做了它應該做的事:在證據不足時停下來。

API 和 Worker 可以並行,但不是永遠

Railway API 和 Worker build 很花時間,看起來最直接的方法就是並行。

但第一次從 embedded worker 切換到 external worker 時,順序不能亂:

調整容量
   ↓
Worker verified
   ↓
API 切換 external

如果 API 先切換,而 Worker 還沒準備好,背景工作可能沒有正確接手。

只有系統已經穩定在 external topology 時,下一次替換才可以:

API build    ─┐
              ├→ Paired readback
Worker build ─┘

兩邊都成功後,才可以繼續 Cloudflare publication。任一邊失敗,都不能寫入成功的 release receipt。

Runtime 沒變,不需要重建全部服務

後來我把版本身份拆成兩組:

Runtime digest
- Backend source
- Frontend source
- Schema
- Public runtime configuration

Control digest
- GitHub Actions workflow
- Publisher scripts
- Deployment verifier
- Deployment CLI

如果只改 Publisher 或 verifier,Runtime 沒變,就不用重新 Build Railway 和 Cloudflare。

但 Pipeline 仍然要重新確認 Railway API、Worker、Database readiness、Cloudflare marker、Frontend asset hash、正式網址與 release receipt。

有一次 control-only 驗證在 1 分 32 秒內完成,而且沒有重新發布 Runtime。

這不能寫成完整部署從 13 分鐘降到 1 分鐘。正確說法是:Runtime 沒有變更,Pipeline 在 1 分 32 秒內完成既有部署的嚴格讀回驗證。

Release receipt 是做什麼的

發布證據原本散落在 GitHub Actions、Railway、Cloudflare、Health endpoint、Database readiness 和 Frontend marker。

Release receipt 把這些結果綁在一起:

source SHA
runtime digest
control digest
API deployment
Worker deployment
database readiness
frontend marker
asset hashes
publication result
verification timestamps

最後只有兩邊 Provider 身份、健康狀態和正式站讀回都正確,才會記錄:

paired_verified

它讓之後的人可以回答:哪個 commit 被發布、哪些服務重新 Build、哪些服務沿用既有 Runtime、哪些檢查執行過,以及哪些部分仍然沒有驗證。

我現在怎麼區分發布狀態

我不再只使用完成或沒完成,而是分成:

Implemented
Tested
Merged
CI green
Deploying
Provider verified
Live readback verified
Customer flow verified

因為:

CI 綠        ≠ 已部署
已合併       ≠ 已上線
Railway 成功 ≠ Exact-SHA 正確
Health 200   ≠ Database 正常
網站能開     ≠ Frontend assets 正確
部署成功     ≠ 真實使用者流程完成

我最後學到的事情

第一篇處理速度與成本,第二篇處理部署正確性。

真正可靠的 CI/CD,需要不重複執行沒有必要的工作,也不能因為追求速度而跳過身份驗證。

Pipeline 最重要的功能不只是自動部署,而是在證據不足時,知道自己應該停下來。

回到第一篇: 改一篇文件也跑完整 CI:我的 2,000 分鐘就是這樣沒的

這套寫法整理成可重用的公開規則: Evidence-first CI/CD writing skill