<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ci on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ci/</link><description>Recent content in Ci on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 29 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ci/index.xml" rel="self" type="application/rss+xml"/><item><title>App 簽章、商店審核與分批發布流程</title><link>https://tarrragon.github.io/blog/ci/app-deploy/signing-store-rollout-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/app-deploy/signing-store-rollout-flow/</guid><description>&lt;p>App 發布流程的核心責任是把可安裝 artifact 送進受控發行通道。App 與 web 最大差異是使用者裝置會長期保留舊版本；CI/CD 需要把 build number、簽章、審核、分批發布與服務端相容性一起管理。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>App 部署的風險集中在不可變 artifact 與外部 gate。IPA、APK、AAB 或桌面安裝包一旦被使用者安裝，團隊需要靠 hotfix、remote config、kill switch 或服務端相容性止血；store review、簽章憑證與 phased rollout 會決定錯誤版本能否快速收斂。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Version&lt;/td>
 &lt;td>管理 version 與 build number&lt;/td>
 &lt;td>每次上傳是否可唯一追溯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App signing&lt;/a>&lt;/td>
 &lt;td>產生可信 artifact&lt;/td>
 &lt;td>certificate / keystore 是否安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>驗證裝置與 OS matrix&lt;/td>
 &lt;td>高風險裝置、權限與離線情境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Store review&lt;/td>
 &lt;td>通過商店或企業發行 gate&lt;/td>
 &lt;td>審核時間、拒審理由、metadata&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a>&lt;/td>
 &lt;td>控制使用者取得比例&lt;/td>
 &lt;td>crash-free rate、conversion、回報&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>hotfix、remote config、kill switch&lt;/td>
 &lt;td>是否能處理已安裝版本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Version 階段負責讓 artifact 可追溯。App crash report、客服回報與 store console 都依賴 version / build number；版本號對應 commit 與 workflow run 時，事故定位可以直接回到發布紀錄。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App signing&lt;/a> 階段負責維持發布信任鏈。簽章憑證、provisioning profile、keystore 與 notarization credential 都是發布能力；它們要用 secret 管理、權限隔離、輪替與備援流程保護。&lt;/p>
&lt;p>Test 階段負責覆蓋目標裝置條件。App 測試要依實際使用者分佈選擇 OS、裝置、權限狀態、網路條件與升級路徑；只跑 emulator smoke test，通常抓不到真機權限、背景限制或升級資料遷移問題。&lt;/p>
&lt;p>Store review 階段負責處理外部 gate。審核可能因 metadata、隱私揭露、權限使用、付款政策或 crash 被拒；CI/CD 文件要記錄誰能處理審核回覆、哪些變更需要重新提交。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a> 階段負責控制新版本擴散速度。分批發布的觀察指標包含 crash rate、登入、購買、同步、推播與核心流程完成率；達到停損條件時應暫停 rollout，先讓已受影響範圍維持可控。&lt;/p>
&lt;p>Recovery 階段負責處理已安裝版本。App 常見止血工具是 remote config、feature flag、kill switch、server-side compatibility、hotfix build 與要求使用者升級；每個工具都要在事故前實作，事故時才有路可走。&lt;/p>
&lt;h2 id="多版本共存契約">多版本共存契約&lt;/h2>
&lt;p>多版本共存是 App 發布的基本前提。後端 API、資料格式、推播 payload 與 remote config 都要支援一段時間的新舊 client，因為使用者更新節奏不受團隊完全控制。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>契約&lt;/th>
 &lt;th>判讀問題&lt;/th>
 &lt;th>常見風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>API response&lt;/td>
 &lt;td>舊 app 看到新增欄位是否能正常處理&lt;/td>
 &lt;td>刪欄位或改語意造成舊版 crash&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auth / session&lt;/td>
 &lt;td>更新前後 token 是否仍可使用&lt;/td>
 &lt;td>強制登出或登入狀態破壞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local storage&lt;/td>
 &lt;td>app upgrade 是否能遷移本機資料&lt;/td>
 &lt;td>新版寫入後舊版讀取契約失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Push payload&lt;/td>
 &lt;td>舊版是否能忽略未知 action&lt;/td>
 &lt;td>推播點擊進入不存在頁面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Remote config&lt;/td>
 &lt;td>config key 是否有預設值與版本條件&lt;/td>
 &lt;td>未支援版本收到新功能開關&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些契約要在 CI 或 release checklist 裡被驗證。若只靠後端「盡量相容」，App 發布失敗會在使用者更新後才暴露，回復成本會比 web 或後端高。&lt;/p>
&lt;h2 id="release-checklist">Release checklist&lt;/h2>
&lt;p>Release checklist 的責任是把外部 gate 與內部 gate 接起來。App 發布牽涉商店、憑證、客服、行銷與後端相容，因此 checklist 應該是流程契約，不只是提醒清單。&lt;/p></description><content:encoded><![CDATA[<p>App 發布流程的核心責任是把可安裝 artifact 送進受控發行通道。App 與 web 最大差異是使用者裝置會長期保留舊版本；CI/CD 需要把 build number、簽章、審核、分批發布與服務端相容性一起管理。</p>
<h2 id="流程定位">流程定位</h2>
<p>App 部署的風險集中在不可變 artifact 與外部 gate。IPA、APK、AAB 或桌面安裝包一旦被使用者安裝，團隊需要靠 hotfix、remote config、kill switch 或服務端相容性止血；store review、簽章憑證與 phased rollout 會決定錯誤版本能否快速收斂。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Version</td>
          <td>管理 version 與 build number</td>
          <td>每次上傳是否可唯一追溯</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App signing</a></td>
          <td>產生可信 artifact</td>
          <td>certificate / keystore 是否安全</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>驗證裝置與 OS matrix</td>
          <td>高風險裝置、權限與離線情境</td>
      </tr>
      <tr>
          <td>Store review</td>
          <td>通過商店或企業發行 gate</td>
          <td>審核時間、拒審理由、metadata</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a></td>
          <td>控制使用者取得比例</td>
          <td>crash-free rate、conversion、回報</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>hotfix、remote config、kill switch</td>
          <td>是否能處理已安裝版本</td>
      </tr>
  </tbody>
</table>
<p>Version 階段負責讓 artifact 可追溯。App crash report、客服回報與 store console 都依賴 version / build number；版本號對應 commit 與 workflow run 時，事故定位可以直接回到發布紀錄。</p>
<p><a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App signing</a> 階段負責維持發布信任鏈。簽章憑證、provisioning profile、keystore 與 notarization credential 都是發布能力；它們要用 secret 管理、權限隔離、輪替與備援流程保護。</p>
<p>Test 階段負責覆蓋目標裝置條件。App 測試要依實際使用者分佈選擇 OS、裝置、權限狀態、網路條件與升級路徑；只跑 emulator smoke test，通常抓不到真機權限、背景限制或升級資料遷移問題。</p>
<p>Store review 階段負責處理外部 gate。審核可能因 metadata、隱私揭露、權限使用、付款政策或 crash 被拒；CI/CD 文件要記錄誰能處理審核回覆、哪些變更需要重新提交。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a> 階段負責控制新版本擴散速度。分批發布的觀察指標包含 crash rate、登入、購買、同步、推播與核心流程完成率；達到停損條件時應暫停 rollout，先讓已受影響範圍維持可控。</p>
<p>Recovery 階段負責處理已安裝版本。App 常見止血工具是 remote config、feature flag、kill switch、server-side compatibility、hotfix build 與要求使用者升級；每個工具都要在事故前實作，事故時才有路可走。</p>
<h2 id="多版本共存契約">多版本共存契約</h2>
<p>多版本共存是 App 發布的基本前提。後端 API、資料格式、推播 payload 與 remote config 都要支援一段時間的新舊 client，因為使用者更新節奏不受團隊完全控制。</p>
<table>
  <thead>
      <tr>
          <th>契約</th>
          <th>判讀問題</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API response</td>
          <td>舊 app 看到新增欄位是否能正常處理</td>
          <td>刪欄位或改語意造成舊版 crash</td>
      </tr>
      <tr>
          <td>Auth / session</td>
          <td>更新前後 token 是否仍可使用</td>
          <td>強制登出或登入狀態破壞</td>
      </tr>
      <tr>
          <td>Local storage</td>
          <td>app upgrade 是否能遷移本機資料</td>
          <td>新版寫入後舊版讀取契約失效</td>
      </tr>
      <tr>
          <td>Push payload</td>
          <td>舊版是否能忽略未知 action</td>
          <td>推播點擊進入不存在頁面</td>
      </tr>
      <tr>
          <td>Remote config</td>
          <td>config key 是否有預設值與版本條件</td>
          <td>未支援版本收到新功能開關</td>
      </tr>
  </tbody>
</table>
<p>這些契約要在 CI 或 release checklist 裡被驗證。若只靠後端「盡量相容」，App 發布失敗會在使用者更新後才暴露，回復成本會比 web 或後端高。</p>
<h2 id="release-checklist">Release checklist</h2>
<p>Release checklist 的責任是把外部 gate 與內部 gate 接起來。App 發布牽涉商店、憑證、客服、行銷與後端相容，因此 checklist 應該是流程契約，不只是提醒清單。</p>
<ol>
<li>確認 version、build number、commit 與 artifact 對應。</li>
<li>確認 signing secret、profile 或 keystore 仍有效。</li>
<li>跑 unit、UI、device matrix 與 upgrade test。</li>
<li>檢查 API / remote config / push payload 多版本相容。</li>
<li>上傳 internal / beta track，跑 smoke test。</li>
<li>提交 store review，記錄審核狀態。</li>
<li>用 phased rollout 推進，觀察 crash-free rate 與核心指標。</li>
<li>觸發停損條件時暫停 rollout、關閉功能或準備 hotfix。</li>
</ol>
<p>這個順序讓 App 發布從「把包丟上去」變成可觀測流程。每一步都對應一個失敗路由，事故時能知道下一個可執行動作。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>App 部署總覽：回 <a href="../">App 部署 CI/CD</a>。</li>
<li>簽章概念：讀 <a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Data pipeline backfill、checkpoint 與 rerun 流程</title><link>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/backfill-checkpoint-rerun-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/backfill-checkpoint-rerun-flow/</guid><description>&lt;p>Data pipeline 發布流程的核心責任是讓資料處理邏輯變更可驗證、可重跑、可修補。資料任務部署成功不等於資料正確；CI/CD 要同時檢查輸入 schema、輸出契約、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a> 與異常資料修復路徑。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>Data pipeline 的風險集中在資料副作用。API 發布錯誤通常會表現成 request failure；資料任務錯誤可能把錯誤結果寫進 warehouse、feature store、報表或下游模型，並在很久之後才被看見。發布流程要把 correctness check 放到 deploy 前後。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>產生 transform code、DAG、query&lt;/td>
 &lt;td>版本是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation&lt;/td>
 &lt;td>驗證 input / output schema&lt;/td>
 &lt;td>新舊欄位、型別、nullability 是否相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>推進 job、DAG、schedule、trigger&lt;/td>
 &lt;td>新版本是否正確接管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>&lt;/td>
 &lt;td>受控補算歷史資料&lt;/td>
 &lt;td>範圍、節流、checkpoint 是否明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a>&lt;/td>
 &lt;td>修復失敗區間或錯誤輸出&lt;/td>
 &lt;td>idempotency、覆寫規則、對帳是否存在&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>rollback、forward fix、資料修補&lt;/td>
 &lt;td>下游是否已消費錯誤資料&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責固定執行邏輯。dbt model、Spark job、Flink processor、Airflow DAG 或 SQL transform 都需要能追到 commit 與 dependency，讓歷史資料重跑時能確認使用哪一版邏輯。&lt;/p>
&lt;p>Validation 階段負責檢查資料契約。Schema check、sample run、contract test、row count、null ratio、distinct count 與 business invariant 都可以作為 gate；重點是讓輸出變更在下游消費前被看見。&lt;/p>
&lt;p>Deploy 階段負責切換任務版本。Scheduler、trigger、checkpoint location 與 credential 都會影響新版本是否真正接管；部署後要確認下一次 run 用的是新版本，並保留舊版本停止或恢復路徑。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a> 階段負責補算歷史資料。Backfill 應有時間範圍、節流、checkpoint、停損條件與對帳策略，避免一次掃完整個歷史區間壓垮上游或把錯誤大規模寫入下游。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a> 階段負責修復失敗 run 或錯誤區間。Rerun 要定義輸出覆寫、去重、idempotency 與下游通知；同一段資料被跑兩次時，結果應可預期。&lt;/p>
&lt;p>Recovery 階段負責處理錯誤資料已被消費的情況。資料 pipeline 的 rollback 常常採用 forward fix、重新計算、標記污染區間與通知下游重新讀取。&lt;/p>
&lt;h2 id="backfill-控制面">Backfill 控制面&lt;/h2>
&lt;p>Backfill 控制面的責任是限制歷史補算的影響範圍。歷史資料量通常遠大於日常增量；沒有控制面的 backfill 會同時衝擊計算成本、上游讀取、下游寫入與資料正確性。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>控制項&lt;/th>
 &lt;th>判讀問題&lt;/th>
 &lt;th>常見做法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Range&lt;/td>
 &lt;td>補算哪個時間或 partition 區間&lt;/td>
 &lt;td>先小範圍驗證，再擴大區間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Throttle&lt;/td>
 &lt;td>每批處理多少資料&lt;/td>
 &lt;td>限制 concurrency、batch size&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a>&lt;/td>
 &lt;td>失敗後從哪裡接續&lt;/td>
 &lt;td>記錄 partition、offset、run id&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stop loss&lt;/td>
 &lt;td>哪些訊號要暫停&lt;/td>
 &lt;td>error rate、成本、row count 異常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reconcile&lt;/td>
 &lt;td>補算結果如何確認&lt;/td>
 &lt;td>新舊輸出比對、抽樣、business check&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些控制項要寫進 workflow 或 runbook。若 backfill 只能靠工程師現場下 SQL，事故時很難保證每次操作都有相同邏輯。&lt;/p>
&lt;h2 id="rerun-判讀">Rerun 判讀&lt;/h2>
&lt;p>Rerun 判讀的責任是確認重跑是否會造成二次傷害。資料任務失敗後，最危險的動作是未確認輸出語意就直接重跑。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>任務失敗但沒有輸出&lt;/td>
 &lt;td>可用同版本重跑&lt;/td>
 &lt;td>確認輸入仍可取得&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部分 partition 已寫入&lt;/td>
 &lt;td>需要去重或覆寫策略&lt;/td>
 &lt;td>檢查 output mode&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>下游已消費錯誤輸出&lt;/td>
 &lt;td>需要通知下游或重算衍生資料&lt;/td>
 &lt;td>標記污染區間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>input schema 已改&lt;/td>
 &lt;td>舊版本重跑條件可能失效&lt;/td>
 &lt;td>用相容版本或轉換層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>streaming checkpoint 壞&lt;/td>
 &lt;td>重跑可能重複消費或漏資料&lt;/td>
 &lt;td>評估 checkpoint repair / replay&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表讓 rerun 從「再跑一次」變成有條件的恢復策略。資料正確性比任務綠燈更重要；綠燈只代表 job 完成，不代表輸出可信。&lt;/p></description><content:encoded><![CDATA[<p>Data pipeline 發布流程的核心責任是讓資料處理邏輯變更可驗證、可重跑、可修補。資料任務部署成功不等於資料正確；CI/CD 要同時檢查輸入 schema、輸出契約、<a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a>、<a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a> 與異常資料修復路徑。</p>
<h2 id="流程定位">流程定位</h2>
<p>Data pipeline 的風險集中在資料副作用。API 發布錯誤通常會表現成 request failure；資料任務錯誤可能把錯誤結果寫進 warehouse、feature store、報表或下游模型，並在很久之後才被看見。發布流程要把 correctness check 放到 deploy 前後。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 transform code、DAG、query</td>
          <td>版本是否可重現</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>驗證 input / output schema</td>
          <td>新舊欄位、型別、nullability 是否相容</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>推進 job、DAG、schedule、trigger</td>
          <td>新版本是否正確接管</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a></td>
          <td>受控補算歷史資料</td>
          <td>範圍、節流、checkpoint 是否明確</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a></td>
          <td>修復失敗區間或錯誤輸出</td>
          <td>idempotency、覆寫規則、對帳是否存在</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>rollback、forward fix、資料修補</td>
          <td>下游是否已消費錯誤資料</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責固定執行邏輯。dbt model、Spark job、Flink processor、Airflow DAG 或 SQL transform 都需要能追到 commit 與 dependency，讓歷史資料重跑時能確認使用哪一版邏輯。</p>
<p>Validation 階段負責檢查資料契約。Schema check、sample run、contract test、row count、null ratio、distinct count 與 business invariant 都可以作為 gate；重點是讓輸出變更在下游消費前被看見。</p>
<p>Deploy 階段負責切換任務版本。Scheduler、trigger、checkpoint location 與 credential 都會影響新版本是否真正接管；部署後要確認下一次 run 用的是新版本，並保留舊版本停止或恢復路徑。</p>
<p><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a> 階段負責補算歷史資料。Backfill 應有時間範圍、節流、checkpoint、停損條件與對帳策略，避免一次掃完整個歷史區間壓垮上游或把錯誤大規模寫入下游。</p>
<p><a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a> 階段負責修復失敗 run 或錯誤區間。Rerun 要定義輸出覆寫、去重、idempotency 與下游通知；同一段資料被跑兩次時，結果應可預期。</p>
<p>Recovery 階段負責處理錯誤資料已被消費的情況。資料 pipeline 的 rollback 常常採用 forward fix、重新計算、標記污染區間與通知下游重新讀取。</p>
<h2 id="backfill-控制面">Backfill 控制面</h2>
<p>Backfill 控制面的責任是限制歷史補算的影響範圍。歷史資料量通常遠大於日常增量；沒有控制面的 backfill 會同時衝擊計算成本、上游讀取、下游寫入與資料正確性。</p>
<table>
  <thead>
      <tr>
          <th>控制項</th>
          <th>判讀問題</th>
          <th>常見做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Range</td>
          <td>補算哪個時間或 partition 區間</td>
          <td>先小範圍驗證，再擴大區間</td>
      </tr>
      <tr>
          <td>Throttle</td>
          <td>每批處理多少資料</td>
          <td>限制 concurrency、batch size</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a></td>
          <td>失敗後從哪裡接續</td>
          <td>記錄 partition、offset、run id</td>
      </tr>
      <tr>
          <td>Stop loss</td>
          <td>哪些訊號要暫停</td>
          <td>error rate、成本、row count 異常</td>
      </tr>
      <tr>
          <td>Reconcile</td>
          <td>補算結果如何確認</td>
          <td>新舊輸出比對、抽樣、business check</td>
      </tr>
  </tbody>
</table>
<p>這些控制項要寫進 workflow 或 runbook。若 backfill 只能靠工程師現場下 SQL，事故時很難保證每次操作都有相同邏輯。</p>
<h2 id="rerun-判讀">Rerun 判讀</h2>
<p>Rerun 判讀的責任是確認重跑是否會造成二次傷害。資料任務失敗後，最危險的動作是未確認輸出語意就直接重跑。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任務失敗但沒有輸出</td>
          <td>可用同版本重跑</td>
          <td>確認輸入仍可取得</td>
      </tr>
      <tr>
          <td>部分 partition 已寫入</td>
          <td>需要去重或覆寫策略</td>
          <td>檢查 output mode</td>
      </tr>
      <tr>
          <td>下游已消費錯誤輸出</td>
          <td>需要通知下游或重算衍生資料</td>
          <td>標記污染區間</td>
      </tr>
      <tr>
          <td>input schema 已改</td>
          <td>舊版本重跑條件可能失效</td>
          <td>用相容版本或轉換層</td>
      </tr>
      <tr>
          <td>streaming checkpoint 壞</td>
          <td>重跑可能重複消費或漏資料</td>
          <td>評估 checkpoint repair / replay</td>
      </tr>
  </tbody>
</table>
<p>這張表讓 rerun 從「再跑一次」變成有條件的恢復策略。資料正確性比任務綠燈更重要；綠燈只代表 job 完成，不代表輸出可信。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Data pipeline 部署總覽：回 <a href="../">Data Pipeline 部署 CI/CD</a>。</li>
<li>Migration 概念：讀 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Desktop client 簽章、公證與自動更新流程</title><link>https://tarrragon.github.io/blog/ci/desktop-client-deploy/signing-notarization-update-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/desktop-client-deploy/signing-notarization-update-flow/</guid><description>&lt;p>Desktop client 發布流程的核心責任是讓多平台安裝包可信、可更新、可回復。桌面應用和 web 不同，使用者會下載 installer 或 package 到本機；CI/CD 需要處理平台差異、code signing、notarization、auto-update feed、delta package 與多版本共存。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>Desktop client 的風險集中在作業系統信任鏈與更新通道。macOS、Windows、Linux 對簽章、安裝包格式與安全提示的要求不同；同一份 source 通常會產生多個平台 artifact，因此 workflow 要把平台 matrix、簽章 secret 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel&lt;/a> 拆清楚。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>產生 &lt;code>.dmg&lt;/code>、&lt;code>.pkg&lt;/code>、&lt;code>.msi&lt;/code>、AppImage 等&lt;/td>
 &lt;td>平台 matrix 是否完整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Signing&lt;/td>
 &lt;td>建立 OS 信任&lt;/td>
 &lt;td>certificate、timestamp、keychain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Notarize&lt;/td>
 &lt;td>通過 macOS 公證或平台審查&lt;/td>
 &lt;td>staple、gatekeeper 是否通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release&lt;/td>
 &lt;td>發布到 channel 或 download page&lt;/td>
 &lt;td>stable / beta / internal 分流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Update&lt;/td>
 &lt;td>推送 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed&lt;/a> 或 delta package&lt;/td>
 &lt;td>feed 簽章、版本相容、回退策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>hotfix、rollback channel、停用更新&lt;/td>
 &lt;td>是否能阻止錯誤版本擴散&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生平台專屬 artifact。Flutter Desktop、Electron 與 Tauri 的輸出格式不同，但共同要求是每個 artifact 都能追到 commit、workflow run 與 dependency lock。&lt;/p>
&lt;p>Signing 階段負責讓 OS 信任安裝包。Windows code signing certificate、macOS Developer ID、timestamp server 與 Linux package signing key 都是發布能力；secret 應放在受控環境，並限制能觸發 signing job 的分支與 reviewer。&lt;/p>
&lt;p>Notarize 階段負責處理 macOS 信任 gate。macOS app 即使完成簽章，也常需要 notarization 與 stapling；CI 要把 notarization log 保存下來，否則使用者看到 Gatekeeper 警告時很難回溯。&lt;/p>
&lt;p>Release 階段負責把 artifact 放到正確 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel&lt;/a>。Internal、beta、stable 與 enterprise channel 的 gate 不同；CI/CD 要避免未審核的 beta artifact 被 stable feed 取用。&lt;/p>
&lt;p>Update 階段負責維持升級路徑。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed&lt;/a>、delta package、signature、minimum supported version 與 rollback channel 要一起設計；更新壞掉時，使用者可能卡在需要人工修復的版本。&lt;/p>
&lt;p>Recovery 階段負責止血。桌面客戶端常用方式是撤下 update feed、發布 hotfix、切換 rollback channel、停用 remote feature 或要求最低版本；每種方式都依賴 app 內建相容支援。&lt;/p>
&lt;h2 id="平台差異判讀">平台差異判讀&lt;/h2>
&lt;p>平台差異判讀的責任是讓 CI matrix 對應真實發布風險。桌面發布除了確認「三平台都 build 成功」，還要確認每個平台的安裝、啟動、更新與卸載行為。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>平台&lt;/th>
 &lt;th>高風險點&lt;/th>
 &lt;th>驗證方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>macOS&lt;/td>
 &lt;td>Developer ID、notarization、universal binary&lt;/td>
 &lt;td>Gatekeeper、arm64 / x64 啟動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Windows&lt;/td>
 &lt;td>Authenticode、SmartScreen、installer 權限&lt;/td>
 &lt;td>安裝、更新、卸載、權限提示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Linux&lt;/td>
 &lt;td>AppImage、deb、rpm、repository key&lt;/td>
 &lt;td>dependency、desktop entry、sandbox&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的用途是避免平台細節被單一「desktop build」欄位抹平。每個 OS 的失敗代價不同，CI 應保留平台專屬 gate。&lt;/p></description><content:encoded><![CDATA[<p>Desktop client 發布流程的核心責任是讓多平台安裝包可信、可更新、可回復。桌面應用和 web 不同，使用者會下載 installer 或 package 到本機；CI/CD 需要處理平台差異、code signing、notarization、auto-update feed、delta package 與多版本共存。</p>
<h2 id="流程定位">流程定位</h2>
<p>Desktop client 的風險集中在作業系統信任鏈與更新通道。macOS、Windows、Linux 對簽章、安裝包格式與安全提示的要求不同；同一份 source 通常會產生多個平台 artifact，因此 workflow 要把平台 matrix、簽章 secret 與 <a href="/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel</a> 拆清楚。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 <code>.dmg</code>、<code>.pkg</code>、<code>.msi</code>、AppImage 等</td>
          <td>平台 matrix 是否完整</td>
      </tr>
      <tr>
          <td>Signing</td>
          <td>建立 OS 信任</td>
          <td>certificate、timestamp、keychain</td>
      </tr>
      <tr>
          <td>Notarize</td>
          <td>通過 macOS 公證或平台審查</td>
          <td>staple、gatekeeper 是否通過</td>
      </tr>
      <tr>
          <td>Release</td>
          <td>發布到 channel 或 download page</td>
          <td>stable / beta / internal 分流</td>
      </tr>
      <tr>
          <td>Update</td>
          <td>推送 <a href="/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed</a> 或 delta package</td>
          <td>feed 簽章、版本相容、回退策略</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>hotfix、rollback channel、停用更新</td>
          <td>是否能阻止錯誤版本擴散</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生平台專屬 artifact。Flutter Desktop、Electron 與 Tauri 的輸出格式不同，但共同要求是每個 artifact 都能追到 commit、workflow run 與 dependency lock。</p>
<p>Signing 階段負責讓 OS 信任安裝包。Windows code signing certificate、macOS Developer ID、timestamp server 與 Linux package signing key 都是發布能力；secret 應放在受控環境，並限制能觸發 signing job 的分支與 reviewer。</p>
<p>Notarize 階段負責處理 macOS 信任 gate。macOS app 即使完成簽章，也常需要 notarization 與 stapling；CI 要把 notarization log 保存下來，否則使用者看到 Gatekeeper 警告時很難回溯。</p>
<p>Release 階段負責把 artifact 放到正確 <a href="/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel</a>。Internal、beta、stable 與 enterprise channel 的 gate 不同；CI/CD 要避免未審核的 beta artifact 被 stable feed 取用。</p>
<p>Update 階段負責維持升級路徑。<a href="/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed</a>、delta package、signature、minimum supported version 與 rollback channel 要一起設計；更新壞掉時，使用者可能卡在需要人工修復的版本。</p>
<p>Recovery 階段負責止血。桌面客戶端常用方式是撤下 update feed、發布 hotfix、切換 rollback channel、停用 remote feature 或要求最低版本；每種方式都依賴 app 內建相容支援。</p>
<h2 id="平台差異判讀">平台差異判讀</h2>
<p>平台差異判讀的責任是讓 CI matrix 對應真實發布風險。桌面發布除了確認「三平台都 build 成功」，還要確認每個平台的安裝、啟動、更新與卸載行為。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>高風險點</th>
          <th>驗證方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>macOS</td>
          <td>Developer ID、notarization、universal binary</td>
          <td>Gatekeeper、arm64 / x64 啟動</td>
      </tr>
      <tr>
          <td>Windows</td>
          <td>Authenticode、SmartScreen、installer 權限</td>
          <td>安裝、更新、卸載、權限提示</td>
      </tr>
      <tr>
          <td>Linux</td>
          <td>AppImage、deb、rpm、repository key</td>
          <td>dependency、desktop entry、sandbox</td>
      </tr>
  </tbody>
</table>
<p>這張表的用途是避免平台細節被單一「desktop build」欄位抹平。每個 OS 的失敗代價不同，CI 應保留平台專屬 gate。</p>
<h2 id="update-feed-契約">Update feed 契約</h2>
<p><a href="/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed</a> 契約的責任是讓已安裝使用者安全升級。Auto-update 需要簽章、版本比較、channel、最低版本與回退策略共同成立，才能讓新版本 URL 進入 feed。</p>
<ol>
<li>Feed 只指向已簽章且已驗證的 artifact。</li>
<li>Stable feed 只接收 stable release，beta feed 只接收 beta release。</li>
<li>App 啟動時能處理 feed 暫時不可用。</li>
<li>Delta update 失敗時能 fallback 到 full installer。</li>
<li>錯誤版本要能從 feed 撤下，並讓未更新使用者停止取得。</li>
<li>已更新使用者要有 hotfix 或 rollback channel。</li>
</ol>
<p>這些條件讓更新通道具備操作性。若 app 只知道「看到新版就下載」，錯誤 feed 會把事故放大到所有啟動中的使用者。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Desktop 部署總覽：回 <a href="../">Desktop Client 部署 CI/CD</a>。</li>
<li>App 發布通用觀念：讀 <a href="../../app-deploy/signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程</a>。</li>
<li>簽章術語：讀 <a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a>。</li>
</ul>
]]></content:encoded></item><item><title>IaC plan、apply、drift 與 recovery 流程</title><link>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</guid><description>&lt;p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、apply、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 與 recovery 分成明確 gate。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Plan&lt;/td>
 &lt;td>預覽資源差異與風險&lt;/td>
 &lt;td>create / update / replace / destroy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>審核變更意圖、權限與影響面&lt;/td>
 &lt;td>高風險資源、跨環境、資料資源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apply&lt;/td>
 &lt;td>在鎖定狀態下套用變更&lt;/td>
 &lt;td>state lock、timeout、partial apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verify&lt;/td>
 &lt;td>確認環境符合預期&lt;/td>
 &lt;td>health、policy、smoke、connectivity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>&lt;/td>
 &lt;td>偵測真實環境與宣告分叉&lt;/td>
 &lt;td>手動 hotfix、console edit、外部系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>回退、補正或 state repair&lt;/td>
 &lt;td>是否能安全恢復服務與 state&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。&lt;/p>
&lt;p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。&lt;/p>
&lt;p>Apply 階段負責把宣告狀態寫入環境。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock&lt;/a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。&lt;/p>
&lt;p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。&lt;/p>
&lt;p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 &lt;code>git revert&lt;/code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。&lt;/p>
&lt;h2 id="plan-review-判讀">Plan review 判讀&lt;/h2>
&lt;p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Plan 訊號&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>destroy&lt;/code>&lt;/td>
 &lt;td>資源將被刪除&lt;/td>
 &lt;td>確認資料、依賴與備份&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>replace&lt;/code>&lt;/td>
 &lt;td>先刪後建或重建資源&lt;/td>
 &lt;td>檢查 downtime、IP、DNS、資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IAM 權限擴大&lt;/td>
 &lt;td>blast radius 增加&lt;/td>
 &lt;td>資安或平台 owner 審核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network 開放&lt;/td>
 &lt;td>暴露面增加&lt;/td>
 &lt;td>檢查 security group / firewall&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 大量漂移&lt;/td>
 &lt;td>宣告與現況長期分叉&lt;/td>
 &lt;td>先處理 drift，再進 feature change&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。&lt;/p></description><content:encoded><![CDATA[<p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、apply、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 與 recovery 分成明確 gate。</p>
<h2 id="流程定位">流程定位</h2>
<p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan</td>
          <td>預覽資源差異與風險</td>
          <td>create / update / replace / destroy</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>審核變更意圖、權限與影響面</td>
          <td>高風險資源、跨環境、資料資源</td>
      </tr>
      <tr>
          <td>Apply</td>
          <td>在鎖定狀態下套用變更</td>
          <td>state lock、timeout、partial apply</td>
      </tr>
      <tr>
          <td>Verify</td>
          <td>確認環境符合預期</td>
          <td>health、policy、smoke、connectivity</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>偵測真實環境與宣告分叉</td>
          <td>手動 hotfix、console edit、外部系統</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>回退、補正或 state repair</td>
          <td>是否能安全恢復服務與 state</td>
      </tr>
  </tbody>
</table>
<p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。</p>
<p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。</p>
<p>Apply 階段負責把宣告狀態寫入環境。<a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。</p>
<p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。</p>
<p><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。</p>
<p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 <code>git revert</code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。</p>
<h2 id="plan-review-判讀">Plan review 判讀</h2>
<p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。</p>
<table>
  <thead>
      <tr>
          <th>Plan 訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>destroy</code></td>
          <td>資源將被刪除</td>
          <td>確認資料、依賴與備份</td>
      </tr>
      <tr>
          <td><code>replace</code></td>
          <td>先刪後建或重建資源</td>
          <td>檢查 downtime、IP、DNS、資料</td>
      </tr>
      <tr>
          <td>IAM 權限擴大</td>
          <td>blast radius 增加</td>
          <td>資安或平台 owner 審核</td>
      </tr>
      <tr>
          <td>Network 開放</td>
          <td>暴露面增加</td>
          <td>檢查 security group / firewall</td>
      </tr>
      <tr>
          <td>State 大量漂移</td>
          <td>宣告與現況長期分叉</td>
          <td>先處理 drift，再進 feature change</td>
      </tr>
  </tbody>
</table>
<p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。</p>
<h2 id="drift-處理路由">Drift 處理路由</h2>
<p>Drift 處理的責任是把現況重新帶回可管理狀態。Drift 發現後不應直接 apply 覆蓋，因為 drift 可能是事故 hotfix、外部系統自動調整或宣告檔過期。</p>
<ol>
<li>確認 drift 來源：人工 hotfix、provider 預設、外部 controller 或宣告過期。</li>
<li>判斷 drift 是否仍需要保留：若是真實修復，應回寫到 IaC。</li>
<li>判斷 apply 是否會破壞服務：特別看 replacement、destroy、權限與 network。</li>
<li>修正宣告或 state：必要時使用 import、state mv 或 provider-specific repair。</li>
<li>重新 plan，確認差異收斂到預期。</li>
</ol>
<p>這個路由讓 drift 修復具備審查性。直接在 console 裡補到看起來正常，會讓下一次 CI apply 把修復覆蓋掉。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是把 IaC 降成指令自動化，忽略它承擔的狀態治理責任。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>plan 與 apply 使用不同輸入</td>
          <td>review 內容與實際套用內容分叉</td>
          <td>保存 plan artifact 或鎖定版本</td>
      </tr>
      <tr>
          <td>沒有 <a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a></td>
          <td>併發 apply 覆寫狀態</td>
          <td>使用 remote backend 與 locking</td>
      </tr>
      <tr>
          <td>drift 長期忽略</td>
          <td>宣告失去可信度</td>
          <td>定期 drift detection 與 owner 路由</td>
      </tr>
      <tr>
          <td>高風險資源無額外 gate</td>
          <td>資料或網路變更直接進環境</td>
          <td>environment protection / approval</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>IaC 部署總覽：回 <a href="../">IaC / Platform 部署 CI/CD</a>。</li>
<li>環境保護：讀 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Image build、scan、registry 與 promotion 流程</title><link>https://tarrragon.github.io/blog/ci/docker-deploy/image-supply-chain-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/docker-deploy/image-supply-chain-flow/</guid><description>&lt;p>Image 供應鏈流程的核心責任是讓 container image 從 build 到 runtime 都可追溯。Image 同時包含 application、runtime、OS package 與 dependency；CI/CD 需要把 Dockerfile、base image、tag、scan、registry 與 deployment manifest 串成同一條供應鏈。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>Image deployment 的風險集中在「看似同名、實際不同」的產物漂移。&lt;code>latest&lt;/code>、mutable tag、重新 build 與跨 registry promotion 都可能讓 staging 測過的 image 不等於 production 跑的 image。嚴謹流程應以 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a> 或 immutable tag 作為 artifact 身分。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>從 Dockerfile 產生 image&lt;/td>
 &lt;td>base image、lockfile、build arg 是否固定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tag&lt;/td>
 &lt;td>建立查詢與推進入口&lt;/td>
 &lt;td>commit SHA、semver、digest 是否可追&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scan&lt;/td>
 &lt;td>顯性化漏洞、secret、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a> 風險&lt;/td>
 &lt;td>阻擋門檻與例外流程是否存在&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry&lt;/a>&lt;/td>
 &lt;td>保存 image 並控制 promotion&lt;/td>
 &lt;td>immutable、retention、權限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime handoff&lt;/td>
 &lt;td>讓 deployment 使用已驗證 image&lt;/td>
 &lt;td>manifest 是否指向已掃描 digest&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責封裝 runtime。Multi-stage build、dependency cache、base image pinning 與 build secret 處理會直接影響安全性；CI 應能在乾淨 runner 上重建 image，避免開發機狀態被帶入。&lt;/p>
&lt;p>Tag 階段負責支援不同查詢情境。Commit SHA 適合事故追溯，semver 適合 release 溝通，&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a> 適合 runtime 精準鎖定；production 判讀應以 digest 為準，tag 只作為人類入口。&lt;/p>
&lt;p>Scan 階段負責把風險分流。Vulnerability scan、secret scan、license scan 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a> 不應只是報表；流程要定義哪些風險阻擋發布、哪些風險允許例外、例外誰審核、何時重新評估。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry&lt;/a> 階段負責保存與推進 image。Registry 要處理權限、retention、immutability、promotion 與垃圾回收；若 production 直接從 feature branch push 的 tag 拉 image，供應鏈邊界就失去治理。&lt;/p>
&lt;p>Runtime handoff 階段負責把已驗證 image 交給部署平台。Kubernetes、ECS、Compose 或其他 runtime 都應指向已驗證 digest 或 immutable tag，並把 health、readiness、resource limit 與 rollback 連到同一次 release。&lt;/p>
&lt;h2 id="tag-與-digest-策略">Tag 與 digest 策略&lt;/h2>
&lt;p>Tag 策略的責任是讓人查得到、機器鎖得住。單一 tag 很難同時滿足可讀性、可追溯與不可變三個需求，因此實務上常搭配多個 tag 與 digest。&lt;/p></description><content:encoded><![CDATA[<p>Image 供應鏈流程的核心責任是讓 container image 從 build 到 runtime 都可追溯。Image 同時包含 application、runtime、OS package 與 dependency；CI/CD 需要把 Dockerfile、base image、tag、scan、registry 與 deployment manifest 串成同一條供應鏈。</p>
<h2 id="流程定位">流程定位</h2>
<p>Image deployment 的風險集中在「看似同名、實際不同」的產物漂移。<code>latest</code>、mutable tag、重新 build 與跨 registry promotion 都可能讓 staging 測過的 image 不等於 production 跑的 image。嚴謹流程應以 <a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 或 immutable tag 作為 artifact 身分。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>從 Dockerfile 產生 image</td>
          <td>base image、lockfile、build arg 是否固定</td>
      </tr>
      <tr>
          <td>Tag</td>
          <td>建立查詢與推進入口</td>
          <td>commit SHA、semver、digest 是否可追</td>
      </tr>
      <tr>
          <td>Scan</td>
          <td>顯性化漏洞、secret、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 風險</td>
          <td>阻擋門檻與例外流程是否存在</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry</a></td>
          <td>保存 image 並控制 promotion</td>
          <td>immutable、retention、權限</td>
      </tr>
      <tr>
          <td>Runtime handoff</td>
          <td>讓 deployment 使用已驗證 image</td>
          <td>manifest 是否指向已掃描 digest</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責封裝 runtime。Multi-stage build、dependency cache、base image pinning 與 build secret 處理會直接影響安全性；CI 應能在乾淨 runner 上重建 image，避免開發機狀態被帶入。</p>
<p>Tag 階段負責支援不同查詢情境。Commit SHA 適合事故追溯，semver 適合 release 溝通，<a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 適合 runtime 精準鎖定；production 判讀應以 digest 為準，tag 只作為人類入口。</p>
<p>Scan 階段負責把風險分流。Vulnerability scan、secret scan、license scan 與 <a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 不應只是報表；流程要定義哪些風險阻擋發布、哪些風險允許例外、例外誰審核、何時重新評估。</p>
<p><a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry</a> 階段負責保存與推進 image。Registry 要處理權限、retention、immutability、promotion 與垃圾回收；若 production 直接從 feature branch push 的 tag 拉 image，供應鏈邊界就失去治理。</p>
<p>Runtime handoff 階段負責把已驗證 image 交給部署平台。Kubernetes、ECS、Compose 或其他 runtime 都應指向已驗證 digest 或 immutable tag，並把 health、readiness、resource limit 與 rollback 連到同一次 release。</p>
<h2 id="tag-與-digest-策略">Tag 與 digest 策略</h2>
<p>Tag 策略的責任是讓人查得到、機器鎖得住。單一 tag 很難同時滿足可讀性、可追溯與不可變三個需求，因此實務上常搭配多個 tag 與 digest。</p>
<table>
  <thead>
      <tr>
          <th>標識</th>
          <th>適合用途</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commit SHA</td>
          <td>從 runtime 回查 source</td>
          <td>對使用者不友善</td>
      </tr>
      <tr>
          <td>Semver</td>
          <td>對外 release 溝通</td>
          <td>tag 可能被覆寫，需搭配 immutability</td>
      </tr>
      <tr>
          <td>Branch tag</td>
          <td>preview / staging 快速迭代</td>
          <td>不適合作為 production 依據</td>
      </tr>
      <tr>
          <td>Digest</td>
          <td>runtime 精準鎖定</td>
          <td>人類閱讀成本高</td>
      </tr>
  </tbody>
</table>
<p>Production deployment 應能從 running pod 或 task 反查 image digest，再反查 registry metadata、scan report、workflow run 與 source commit。這條查詢路徑是 incident response 的基本能力。</p>
<h2 id="scan-gate-分流">Scan gate 分流</h2>
<p>Scan gate 的責任是讓安全訊號變成可操作路由。掃描工具會產生大量結果，沒有分流規則時，團隊會在兩種壞狀態間搖擺：全部阻擋導致發不出去，全部忽略導致掃描失去信任。</p>
<table>
  <thead>
      <tr>
          <th>結果類型</th>
          <th>策略</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Critical exploitable</td>
          <td>阻擋 production promotion</td>
          <td>升級 dependency / base image</td>
      </tr>
      <tr>
          <td>High with mitigation</td>
          <td>需要審核例外與到期日</td>
          <td>記錄風險、設定重新掃描</td>
      </tr>
      <tr>
          <td>Base image aging</td>
          <td>排入 base image refresh</td>
          <td>建立定期更新節奏</td>
      </tr>
      <tr>
          <td>Secret in layer</td>
          <td>阻擋並輪替 secret</td>
          <td>重建 image、撤銷已暴露 credential</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> missing</td>
          <td>阻擋高治理環境，低風險環境警告</td>
          <td>補 provenance / SBOM 產出</td>
      </tr>
  </tbody>
</table>
<p>這個分流讓 scan 成為 gate。例外流程要有 owner 與到期日，讓例外維持可追蹤、可重新評估。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是讓 image 身分失去穩定錨點。當 image 身分漂移，測試結果、掃描結果與 runtime 狀態會彼此分叉。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>production 使用 <code>latest</code></td>
          <td>running image 缺少精準身分</td>
          <td>使用 <a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 或 immutable tag</td>
      </tr>
      <tr>
          <td>staging 與 production 各自 build</td>
          <td>測試產物與上線產物分叉</td>
          <td>build once，promote same image</td>
      </tr>
      <tr>
          <td>build secret 留在 layer</td>
          <td>secret 進入 registry 與節點</td>
          <td>使用 BuildKit secret mount</td>
      </tr>
      <tr>
          <td>scan 只報告不阻擋</td>
          <td>高風險漏洞仍進 production</td>
          <td>定義阻擋門檻與例外流程</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Image 部署總覽：回 <a href="../">Docker / Image 部署 CI/CD</a>。</li>
<li>Registry 術語：讀 <a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a>。</li>
<li>後端 runtime 部署：讀 <a href="../../backend-deploy/">後端部署 CI/CD</a>。</li>
</ul>
]]></content:encoded></item><item><title>Serverless function 版本、事件來源與回復流程</title><link>https://tarrragon.github.io/blog/ci/serverless-deploy/function-version-event-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/serverless-deploy/function-version-event-flow/</guid><description>&lt;p>Serverless 發布流程的核心責任是把函式 artifact、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a>、權限與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a> 一起推進。Serverless 部署看起來比長駐服務短，但每次 invocation 都依賴 runtime、IAM、event source、retry policy 與 observability；CI/CD 需要把這些條件視為發布契約。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>Serverless 的風險集中在觸發條件。函式部署成功只代表新版本存在，實際風險會在 HTTP request、queue message、topic event、scheduled job 或 edge request 觸發時出現。發布流程要能區分「版本建立成功」「alias 切流量成功」「事件來源行為正確」三件事。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Package&lt;/td>
 &lt;td>產生 function bundle / layer&lt;/td>
 &lt;td>dependency、runtime target 是否固定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Version&lt;/td>
 &lt;td>發布 immutable function version&lt;/td>
 &lt;td>version 是否可追到 commit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alias / traffic&lt;/td>
 &lt;td>控制新舊版本流量&lt;/td>
 &lt;td>alias 權重、錯誤率、冷啟動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission&lt;/td>
 &lt;td>限制 IAM、secret、resource policy&lt;/td>
 &lt;td>最小權限與環境隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a>&lt;/td>
 &lt;td>管理 trigger、retry、dead-letter&lt;/td>
 &lt;td>重試與毒訊息處理是否明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>alias rollback、disable trigger、replay&lt;/td>
 &lt;td>是否能止血與修補資料&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Package 階段負責產生可執行 bundle。Serverless 常見失敗是本機 dependency 可用，但打包後缺檔、runtime target 不符、native extension 不相容或 layer 版本漂移；CI 應在接近目標 runtime 的環境做 smoke test。&lt;/p>
&lt;p>Version 階段負責建立不可變版本。直接覆蓋 &lt;code>$LATEST&lt;/code> 會讓事故追溯困難；正式流量應指向 version 或 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a>，讓 rollback 能把 alias 切回前一個已知版本。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a> / traffic 階段負責控制流量切換。HTTP function 可以用少量權重 canary；queue trigger 則要觀察 batch failure、retry、dead-letter 與 downstream side effect，因為同一個錯誤 event 可能被重試多次。&lt;/p>
&lt;p>Permission 階段負責限制 blast radius。Serverless 函式容易因部署方便而累積過大 IAM 權限；每個 function 應只拿到必要 resource、secret 與 network access，並把 production secret 與 preview / staging 隔離。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a> 階段負責定義失敗重送語意。Queue、topic、object storage、HTTP 與 scheduler 的錯誤行為不同；CI/CD 文件要記錄 retry 次數、dead-letter destination、batch size、concurrency limit 與 replay 條件。&lt;/p>
&lt;p>Recovery 階段負責止血。Serverless 常見止血方式是 alias rollback、停用 trigger、降低 concurrency、清理毒訊息、重放事件或 forward fix；只回退 code 版本不一定能處理已經排入 queue 的事件。&lt;/p></description><content:encoded><![CDATA[<p>Serverless 發布流程的核心責任是把函式 artifact、<a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a>、權限與 <a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a> 一起推進。Serverless 部署看起來比長駐服務短，但每次 invocation 都依賴 runtime、IAM、event source、retry policy 與 observability；CI/CD 需要把這些條件視為發布契約。</p>
<h2 id="流程定位">流程定位</h2>
<p>Serverless 的風險集中在觸發條件。函式部署成功只代表新版本存在，實際風險會在 HTTP request、queue message、topic event、scheduled job 或 edge request 觸發時出現。發布流程要能區分「版本建立成功」「alias 切流量成功」「事件來源行為正確」三件事。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Package</td>
          <td>產生 function bundle / layer</td>
          <td>dependency、runtime target 是否固定</td>
      </tr>
      <tr>
          <td>Version</td>
          <td>發布 immutable function version</td>
          <td>version 是否可追到 commit</td>
      </tr>
      <tr>
          <td>Alias / traffic</td>
          <td>控制新舊版本流量</td>
          <td>alias 權重、錯誤率、冷啟動</td>
      </tr>
      <tr>
          <td>Permission</td>
          <td>限制 IAM、secret、resource policy</td>
          <td>最小權限與環境隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a></td>
          <td>管理 trigger、retry、dead-letter</td>
          <td>重試與毒訊息處理是否明確</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>alias rollback、disable trigger、replay</td>
          <td>是否能止血與修補資料</td>
      </tr>
  </tbody>
</table>
<p>Package 階段負責產生可執行 bundle。Serverless 常見失敗是本機 dependency 可用，但打包後缺檔、runtime target 不符、native extension 不相容或 layer 版本漂移；CI 應在接近目標 runtime 的環境做 smoke test。</p>
<p>Version 階段負責建立不可變版本。直接覆蓋 <code>$LATEST</code> 會讓事故追溯困難；正式流量應指向 version 或 <a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a>，讓 rollback 能把 alias 切回前一個已知版本。</p>
<p><a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a> / traffic 階段負責控制流量切換。HTTP function 可以用少量權重 canary；queue trigger 則要觀察 batch failure、retry、dead-letter 與 downstream side effect，因為同一個錯誤 event 可能被重試多次。</p>
<p>Permission 階段負責限制 blast radius。Serverless 函式容易因部署方便而累積過大 IAM 權限；每個 function 應只拿到必要 resource、secret 與 network access，並把 production secret 與 preview / staging 隔離。</p>
<p><a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a> 階段負責定義失敗重送語意。Queue、topic、object storage、HTTP 與 scheduler 的錯誤行為不同；CI/CD 文件要記錄 retry 次數、dead-letter destination、batch size、concurrency limit 與 replay 條件。</p>
<p>Recovery 階段負責止血。Serverless 常見止血方式是 alias rollback、停用 trigger、降低 concurrency、清理毒訊息、重放事件或 forward fix；只回退 code 版本不一定能處理已經排入 queue 的事件。</p>
<h2 id="事件來源判讀">事件來源判讀</h2>
<p>事件來源判讀的責任是找出失敗是否可重試。Serverless 常被誤判為「函式自己失敗」，但實際根因可能是 event schema、權限、上游重試或下游限流。</p>
<table>
  <thead>
      <tr>
          <th>Event source</th>
          <th>常見失敗</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HTTP / API</td>
          <td>status code、timeout、冷啟動</td>
          <td>看 latency、concurrency、alias</td>
      </tr>
      <tr>
          <td>Queue</td>
          <td>batch failure、毒訊息、重試風暴</td>
          <td>看 DLQ、batch size、visibility timeout</td>
      </tr>
      <tr>
          <td>Topic</td>
          <td>event schema 漂移</td>
          <td>驗證 publisher / subscriber 契約</td>
      </tr>
      <tr>
          <td>Object store</td>
          <td>權限或路徑 pattern 錯誤</td>
          <td>檢查 resource policy 與 filter</td>
      </tr>
      <tr>
          <td>Scheduler</td>
          <td>timezone、重入、上次執行未完成</td>
          <td>檢查 idempotency 與 lock</td>
      </tr>
  </tbody>
</table>
<p>這張表讓 release failure 能被導向正確 owner。若 event schema 變了，修 function 可能只是表面補丁；真正的 gate 要加在 publisher contract 或 sample event validation。</p>
<h2 id="最小發布-gate">最小發布 gate</h2>
<p>Serverless workflow 的最小 gate 應覆蓋 package、permission、event 與 alias。缺其中一段，部署成功就可能只是建立了一個尚未被驗證的函式版本。</p>
<ol>
<li>Package bundle，固定 runtime target 與 dependency。</li>
<li>對 bundle 執行 unit / contract / sample event test。</li>
<li>用 least privilege policy 做 deploy dry run 或 policy diff。</li>
<li>發布 immutable function version。</li>
<li>用 alias 將少量流量導向新版本。</li>
<li>觀察 error、latency、retry、DLQ 與 downstream 指標。</li>
<li>指標穩定後提高 alias 權重或完成切換。</li>
<li>指標觸發 tripwire 時切回 alias、停用 trigger 或啟動 repair。</li>
</ol>
<p>這個流程把 Serverless 發布從「上傳函式」提升成可回復流程。對事件驅動函式而言，trigger 與 retry policy 是發布契約的一部分。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Serverless 部署總覽：回 <a href="../">Serverless 部署 CI/CD</a>。</li>
<li>Rollout 概念：讀 <a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a>。</li>
<li>失敗處理：讀 <a href="../../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>前端 artifact 與 preview deployment 流程</title><link>https://tarrragon.github.io/blog/ci/frontend-deploy/static-artifact-preview-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/frontend-deploy/static-artifact-preview-flow/</guid><description>&lt;p>前端 artifact 流程的核心責任是讓測試、預覽與正式發布使用同一份靜態產物。前端部署常見輸出是 HTML、CSS、JavaScript、圖片、sourcemap 與搜尋索引；這些產物一旦被重新 build，就可能受到環境變數、依賴版本、base URL 或 framework 設定影響，因此 CI/CD 需要把「產生一次、驗證一次、推進同一份」當成主線。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>前端部署的風險集中在 build time。後端服務可以在 runtime 讀取設定、檢查資料庫與逐步接流量；前端靜態產物多半在 build 階段就把 route、asset path、環境變數與 feature flag 預先寫入 bundle。CI/CD 的判讀重點因此是「被部署的 artifact 是否就是已驗證的那一份」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>產生 production-like static artifact&lt;/td>
 &lt;td>lockfile、Node 版本、base URL 是否固定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser test&lt;/td>
 &lt;td>驗證使用者可見行為&lt;/td>
 &lt;td>測試是否跑在 build 後 artifact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment&lt;/a>&lt;/td>
 &lt;td>讓 PR 變更可被 reviewer 實際操作&lt;/td>
 &lt;td>preview URL 是否對應 commit / PR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>推進到 hosting、Pages 或 CDN&lt;/td>
 &lt;td>HTML cache、asset cache、SPA fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a>&lt;/td>
 &lt;td>重新服務上一份已知可用 artifact&lt;/td>
 &lt;td>舊 artifact、cache purge 與 API 相容性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責建立可驗證產物。真實服務裡，&lt;code>npm run dev&lt;/code> 成功不代表 production build 能成功；CI 應固定 Node 版本、package manager、lockfile、build command 與必要環境變數，讓 artifact 可以從乾淨環境重建。&lt;/p>
&lt;p>Browser test 階段負責驗證使用者實際會看到的頁面。Playwright、visual diff、a11y check 或 smoke test 應盡量對 build 後的靜態站執行，避免 dev server 的 fallback、熱更新或寬鬆路由遮蔽 production 問題。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment&lt;/a> 階段負責把 PR 變成可操作畫面。Preview URL 要能追到 PR、commit 與 workflow run，reviewer 才能把畫面問題回報到正確版本；preview 也要隔離 production 資料與 credential，避免預覽環境變成未受控入口。&lt;/p>
&lt;p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。前端部署失敗常出現在 cache policy、SPA fallback、base URL、static route 與 sourcemap 權限；deploy 成功只代表檔案上傳完成，仍需要檢查入口頁、核心路由與 asset 是否能從公開網址載入。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a> 階段負責恢復上一份可用靜態產物。前端 rollback 表面上只是切回舊檔案，但若 API schema、build time config 或 CDN cache 已經變動，舊頁面仍可能呼叫不相容的後端，因此 rollback 要搭配 smoke test 與 cache purge。&lt;/p>
&lt;h2 id="常見失敗路由">常見失敗路由&lt;/h2>
&lt;p>前端 CI 紅燈要先判斷失敗在 build、browser test、preview 還是 production deploy。不同層的修復入口不同；把所有紅燈都當成「重跑 workflow」會掩蓋 artifact 漂移與 cache 問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>本機 dev 正常、CI build 失敗&lt;/td>
 &lt;td>production build 條件與本機不同&lt;/td>
 &lt;td>回本機跑 CI 同一條 build command&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試通過、上線後空白頁&lt;/td>
 &lt;td>測試沒有覆蓋 production artifact / URL&lt;/td>
 &lt;td>對已部署 artifact 跑 smoke test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Preview URL 顯示舊畫面&lt;/td>
 &lt;td>preview cache 或 commit 對應錯位&lt;/td>
 &lt;td>檢查 preview artifact 與 workflow run&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只有深層路由 404&lt;/td>
 &lt;td>SPA fallback 或 static route 設定錯誤&lt;/td>
 &lt;td>檢查 hosting rewrite / base URL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback 後仍看到新版&lt;/td>
 &lt;td>CDN / browser cache 尚未失效&lt;/td>
 &lt;td>檢查 cache invalidation 與 HTML cache policy&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的用途是縮短定位時間。前端部署問題常被誤判成「CDN 壞掉」或「瀏覽器快取」，但更常見的根因是 build artifact、route 與 cache policy 的契約沒有明確寫進 pipeline。&lt;/p></description><content:encoded><![CDATA[<p>前端 artifact 流程的核心責任是讓測試、預覽與正式發布使用同一份靜態產物。前端部署常見輸出是 HTML、CSS、JavaScript、圖片、sourcemap 與搜尋索引；這些產物一旦被重新 build，就可能受到環境變數、依賴版本、base URL 或 framework 設定影響，因此 CI/CD 需要把「產生一次、驗證一次、推進同一份」當成主線。</p>
<h2 id="流程定位">流程定位</h2>
<p>前端部署的風險集中在 build time。後端服務可以在 runtime 讀取設定、檢查資料庫與逐步接流量；前端靜態產物多半在 build 階段就把 route、asset path、環境變數與 feature flag 預先寫入 bundle。CI/CD 的判讀重點因此是「被部署的 artifact 是否就是已驗證的那一份」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 production-like static artifact</td>
          <td>lockfile、Node 版本、base URL 是否固定</td>
      </tr>
      <tr>
          <td>Browser test</td>
          <td>驗證使用者可見行為</td>
          <td>測試是否跑在 build 後 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment</a></td>
          <td>讓 PR 變更可被 reviewer 實際操作</td>
          <td>preview URL 是否對應 commit / PR</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>推進到 hosting、Pages 或 CDN</td>
          <td>HTML cache、asset cache、SPA fallback</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a></td>
          <td>重新服務上一份已知可用 artifact</td>
          <td>舊 artifact、cache purge 與 API 相容性</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責建立可驗證產物。真實服務裡，<code>npm run dev</code> 成功不代表 production build 能成功；CI 應固定 Node 版本、package manager、lockfile、build command 與必要環境變數，讓 artifact 可以從乾淨環境重建。</p>
<p>Browser test 階段負責驗證使用者實際會看到的頁面。Playwright、visual diff、a11y check 或 smoke test 應盡量對 build 後的靜態站執行，避免 dev server 的 fallback、熱更新或寬鬆路由遮蔽 production 問題。</p>
<p><a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment</a> 階段負責把 PR 變成可操作畫面。Preview URL 要能追到 PR、commit 與 workflow run，reviewer 才能把畫面問題回報到正確版本；preview 也要隔離 production 資料與 credential，避免預覽環境變成未受控入口。</p>
<p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。前端部署失敗常出現在 cache policy、SPA fallback、base URL、static route 與 sourcemap 權限；deploy 成功只代表檔案上傳完成，仍需要檢查入口頁、核心路由與 asset 是否能從公開網址載入。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a> 階段負責恢復上一份可用靜態產物。前端 rollback 表面上只是切回舊檔案，但若 API schema、build time config 或 CDN cache 已經變動，舊頁面仍可能呼叫不相容的後端，因此 rollback 要搭配 smoke test 與 cache purge。</p>
<h2 id="常見失敗路由">常見失敗路由</h2>
<p>前端 CI 紅燈要先判斷失敗在 build、browser test、preview 還是 production deploy。不同層的修復入口不同；把所有紅燈都當成「重跑 workflow」會掩蓋 artifact 漂移與 cache 問題。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本機 dev 正常、CI build 失敗</td>
          <td>production build 條件與本機不同</td>
          <td>回本機跑 CI 同一條 build command</td>
      </tr>
      <tr>
          <td>測試通過、上線後空白頁</td>
          <td>測試沒有覆蓋 production artifact / URL</td>
          <td>對已部署 artifact 跑 smoke test</td>
      </tr>
      <tr>
          <td>Preview URL 顯示舊畫面</td>
          <td>preview cache 或 commit 對應錯位</td>
          <td>檢查 preview artifact 與 workflow run</td>
      </tr>
      <tr>
          <td>只有深層路由 404</td>
          <td>SPA fallback 或 static route 設定錯誤</td>
          <td>檢查 hosting rewrite / base URL</td>
      </tr>
      <tr>
          <td>rollback 後仍看到新版</td>
          <td>CDN / browser cache 尚未失效</td>
          <td>檢查 cache invalidation 與 HTML cache policy</td>
      </tr>
  </tbody>
</table>
<p>這張表的用途是縮短定位時間。前端部署問題常被誤判成「CDN 壞掉」或「瀏覽器快取」，但更常見的根因是 build artifact、route 與 cache policy 的契約沒有明確寫進 pipeline。</p>
<h2 id="最小-workflow-骨架">最小 workflow 骨架</h2>
<p>前端 workflow 應把 build、test、preview 與 deploy 的資料流顯性化。下面是概念骨架，重點在 artifact handoff 的方向，特定平台語法是次要的。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm ci</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run test:e2e:static</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">preview</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.event_name == &#39;pull_request&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run deploy:preview</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.ref == &#39;refs/heads/main&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run deploy:production</span></span></span></code></pre></div><p>這個骨架讓 deploy 依賴 test，也讓 test 與 deploy 使用 build job 產生的同一份產物。若專案需要在不同環境注入設定，要明確區分 build time config 與 runtime config，避免同一份 artifact 被重新 build 成另一份內容。</p>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提醒前端 workflow 需要重切。當同一類問題反覆出現，局部補命令通常只能暫時遮住資料流錯位。</p>
<ul>
<li>Preview 常和 production 不一致：把 preview 改成部署 build artifact，讓 preview job 沿用同一份產物。</li>
<li>E2E 測試通過但 production 壞：把 E2E 改到 static artifact 或 production-like server 上執行。</li>
<li>rollback 依賴人工找舊 commit：保留 release artifact 與版本索引，讓回退指向明確產物。</li>
<li>CDN cache 問題反覆出現：把 HTML cache、asset cache 與 purge 策略寫進 deploy checklist。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>前端部署總覽：回 <a href="../">前端部署 CI/CD</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>本 blog 靜態站案例：讀 <a href="../../blog-project-deploy/">本 blog 專案部署</a>。</li>
</ul>
]]></content:encoded></item><item><title>後端 migration、rollout 與 rollback 流程</title><link>https://tarrragon.github.io/blog/ci/backend-deploy/migration-rollout-rollback-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/backend-deploy/migration-rollout-rollback-flow/</guid><description>&lt;p>後端部署流程的核心責任是讓程式、資料與流量在相容窗口內推進。後端服務通常會同時依賴 database、queue、cache、外部 API 與 runtime config；CI/CD 需要把 build 成功、migration 安全、readiness 可信、rollback 可執行分成不同 gate。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>後端部署的主要風險是有狀態依賴。前端 artifact 可以直接回退上一份靜態檔，後端服務一旦寫入新資料、消費 queue message 或呼叫外部 side effect，rollback 就不再只是換回舊 image。發布流程要先定義新舊版本如何短暫共存，再決定 migration 與流量切換順序。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>產生 binary、package 或 image&lt;/td>
 &lt;td>版本是否可追到 commit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Contract test&lt;/td>
 &lt;td>驗證 API、queue、DB 相容性&lt;/td>
 &lt;td>新舊 schema / message 是否可共存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>推進資料結構與資料狀態&lt;/td>
 &lt;td>是否可漸進、可重試、可停止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a>&lt;/td>
 &lt;td>分批接流量&lt;/td>
 &lt;td>readiness、error rate、latency 是否可信&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a>&lt;/td>
 &lt;td>縮小錯誤版本影響&lt;/td>
 &lt;td>程式、資料、queue 與 config 是否可回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可部署服務。服務版本要能從 runtime 反查 commit、workflow run、image digest 與 migration 版本，讓事故時能快速定位哪一次變更進入環境。&lt;/p>
&lt;p>Contract test 階段負責驗證跨邊界相容。API response、database schema、queue message 與 config key 都是契約；只測 service 內部函式，通常抓不到新舊版本並存時的破壞性變更。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 階段負責推進資料狀態。安全 migration 通常採 expand-and-contract：先加相容欄位或表、部署可讀新舊格式的程式、回填資料，最後移除舊格式。直接在同一次 release 刪欄位與切程式，會讓 rollback 失去空間。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy&lt;/a> 階段負責控制新版本接到的流量。Rolling、canary 與 blue-green 都需要可信 readiness；readiness 應檢查服務能否接流量，而不只是 process alive。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a> 階段負責定義失敗時的處理路由。後端 rollback 常見做法是 app rollback、config rollback、traffic rollback 或 forward fix；資料已被新程式寫入時，forward fix 往往比直接資料回滾安全。&lt;/p>
&lt;h2 id="migration-順序">Migration 順序&lt;/h2>
&lt;p>Migration 順序的責任是保留相容窗口。資料結構變更應讓至少兩個相鄰程式版本能共存，避免部署中途任何一端先完成都造成服務不可用。&lt;/p>
&lt;ol>
&lt;li>新增向前相容 schema，例如新增 nullable column 或新表。&lt;/li>
&lt;li>部署可同時讀舊欄位與新欄位的程式。&lt;/li>
&lt;li>執行 backfill 或 background migration。&lt;/li>
&lt;li>切換讀取來源或寫入路徑。&lt;/li>
&lt;li>觀察穩定後移除舊欄位、舊 index 或舊 message 格式。&lt;/li>
&lt;/ol>
&lt;p>這個順序的價值是可停止。若第 3 步回填異常，可以暫停 backfill，不必立即回退 app；若第 4 步切換後錯誤率升高，可以先切回舊讀取路徑，再評估資料修補。&lt;/p>
&lt;h2 id="rollout-判讀">Rollout 判讀&lt;/h2>
&lt;p>Rollout 判讀要同時看技術指標與業務副作用。服務能啟動不代表能安全接流量；API error、queue lag、database lock、第三方 API 錯誤與核心業務漏斗都可能是發布問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>readiness 未通過&lt;/td>
 &lt;td>新版本尚未能接流量&lt;/td>
 &lt;td>暫停 rollout，查 config / 依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error rate 上升&lt;/td>
 &lt;td>新版本或相依服務契約出錯&lt;/td>
 &lt;td>降低流量或切回舊版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>migration lock 久&lt;/td>
 &lt;td>schema 變更影響正常查詢&lt;/td>
 &lt;td>停止 migration，改成分段方案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>consumer lag 上升&lt;/td>
 &lt;td>worker 消費速度或 message 壞&lt;/td>
 &lt;td>暫停新版 worker 或降速&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback 後仍錯&lt;/td>
 &lt;td>資料或外部 side effect 已變動&lt;/td>
 &lt;td>進入 forward fix / repair 流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些訊號要先接到發布流程。若指標只存在 dashboard 裡、workflow 不知道如何判讀，團隊仍會在事故當下靠人工臨場決策。&lt;/p></description><content:encoded><![CDATA[<p>後端部署流程的核心責任是讓程式、資料與流量在相容窗口內推進。後端服務通常會同時依賴 database、queue、cache、外部 API 與 runtime config；CI/CD 需要把 build 成功、migration 安全、readiness 可信、rollback 可執行分成不同 gate。</p>
<h2 id="流程定位">流程定位</h2>
<p>後端部署的主要風險是有狀態依賴。前端 artifact 可以直接回退上一份靜態檔，後端服務一旦寫入新資料、消費 queue message 或呼叫外部 side effect，rollback 就不再只是換回舊 image。發布流程要先定義新舊版本如何短暫共存，再決定 migration 與流量切換順序。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 binary、package 或 image</td>
          <td>版本是否可追到 commit</td>
      </tr>
      <tr>
          <td>Contract test</td>
          <td>驗證 API、queue、DB 相容性</td>
          <td>新舊 schema / message 是否可共存</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>推進資料結構與資料狀態</td>
          <td>是否可漸進、可重試、可停止</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a></td>
          <td>分批接流量</td>
          <td>readiness、error rate、latency 是否可信</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a></td>
          <td>縮小錯誤版本影響</td>
          <td>程式、資料、queue 與 config 是否可回復</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可部署服務。服務版本要能從 runtime 反查 commit、workflow run、image digest 與 migration 版本，讓事故時能快速定位哪一次變更進入環境。</p>
<p>Contract test 階段負責驗證跨邊界相容。API response、database schema、queue message 與 config key 都是契約；只測 service 內部函式，通常抓不到新舊版本並存時的破壞性變更。</p>
<p><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 階段負責推進資料狀態。安全 migration 通常採 expand-and-contract：先加相容欄位或表、部署可讀新舊格式的程式、回填資料，最後移除舊格式。直接在同一次 release 刪欄位與切程式，會讓 rollback 失去空間。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout strategy</a> 階段負責控制新版本接到的流量。Rolling、canary 與 blue-green 都需要可信 readiness；readiness 應檢查服務能否接流量，而不只是 process alive。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a> 階段負責定義失敗時的處理路由。後端 rollback 常見做法是 app rollback、config rollback、traffic rollback 或 forward fix；資料已被新程式寫入時，forward fix 往往比直接資料回滾安全。</p>
<h2 id="migration-順序">Migration 順序</h2>
<p>Migration 順序的責任是保留相容窗口。資料結構變更應讓至少兩個相鄰程式版本能共存，避免部署中途任何一端先完成都造成服務不可用。</p>
<ol>
<li>新增向前相容 schema，例如新增 nullable column 或新表。</li>
<li>部署可同時讀舊欄位與新欄位的程式。</li>
<li>執行 backfill 或 background migration。</li>
<li>切換讀取來源或寫入路徑。</li>
<li>觀察穩定後移除舊欄位、舊 index 或舊 message 格式。</li>
</ol>
<p>這個順序的價值是可停止。若第 3 步回填異常，可以暫停 backfill，不必立即回退 app；若第 4 步切換後錯誤率升高，可以先切回舊讀取路徑，再評估資料修補。</p>
<h2 id="rollout-判讀">Rollout 判讀</h2>
<p>Rollout 判讀要同時看技術指標與業務副作用。服務能啟動不代表能安全接流量；API error、queue lag、database lock、第三方 API 錯誤與核心業務漏斗都可能是發布問題。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>readiness 未通過</td>
          <td>新版本尚未能接流量</td>
          <td>暫停 rollout，查 config / 依賴</td>
      </tr>
      <tr>
          <td>error rate 上升</td>
          <td>新版本或相依服務契約出錯</td>
          <td>降低流量或切回舊版本</td>
      </tr>
      <tr>
          <td>migration lock 久</td>
          <td>schema 變更影響正常查詢</td>
          <td>停止 migration，改成分段方案</td>
      </tr>
      <tr>
          <td>consumer lag 上升</td>
          <td>worker 消費速度或 message 壞</td>
          <td>暫停新版 worker 或降速</td>
      </tr>
      <tr>
          <td>rollback 後仍錯</td>
          <td>資料或外部 side effect 已變動</td>
          <td>進入 forward fix / repair 流程</td>
      </tr>
  </tbody>
</table>
<p>這些訊號要先接到發布流程。若指標只存在 dashboard 裡、workflow 不知道如何判讀，團隊仍會在事故當下靠人工臨場決策。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是把後端部署當成單一 deploy 動作。後端發布的本質是多個相依狀態的協調流程。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>app 與 destructive migration 同步</td>
          <td>rollback 後舊程式失去讀取契約</td>
          <td>expand-and-contract</td>
      </tr>
      <tr>
          <td>readiness 只檢查 process alive</td>
          <td>流量進入尚未準備好的服務</td>
          <td>檢查依賴、config 與初始化狀態</td>
      </tr>
      <tr>
          <td>rollback 只切 image tag</td>
          <td>資料與 queue side effect 留下</td>
          <td>定義 app / data / config 路由</td>
      </tr>
      <tr>
          <td>migration 沒有 dry run</td>
          <td>發布時才發現權限或鎖表問題</td>
          <td>staging 或 shadow 環境先跑驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>後端部署總覽：回 <a href="../">後端部署 CI/CD</a>。</li>
<li>Migration 術語：讀 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Binary release 與 installer 模式</title><link>https://tarrragon.github.io/blog/ci/package-library-release/binary-release-and-installer/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/package-library-release/binary-release-and-installer/</guid><description>&lt;p>Binary release 是一條直接把預編譯執行檔掛在 GitHub Release 下供使用者下載的發版通道，跳過 package registry。它解決的問題是：當套件不是函式庫而是 CLI binary，下游不需要重新編譯、也不一定有對應語言的 toolchain 時，需要一條「平台無關、即拿即用」的安裝路線。本篇用 &lt;a href="https://github.com/sysprog21/zhtw-mcp">&lt;code>zhtw-mcp&lt;/code>&lt;/a> 為陪跑案例，公開協作軌跡可直接對照 &lt;a href="https://github.com/sysprog21/zhtw-mcp/issues/35">issue #35&lt;/a> 與 &lt;a href="https://github.com/sysprog21/zhtw-mcp/pull/40">PR #40&lt;/a>。&lt;/p>
&lt;h2 id="為什麼需要這條通道">為什麼需要這條通道&lt;/h2>
&lt;p>CLI binary 跟函式庫的下游使用脈絡不同。函式庫需要被同語言專案 import，自然走 registry（&lt;code>npm install&lt;/code>、&lt;code>pip install&lt;/code>、&lt;code>cargo add&lt;/code>）。CLI binary 的目標讀者是「只想跑這個工具」的人，他們不一定有對應 toolchain、不想花時間編譯，也不會接受「先裝開發環境才能用」的入場門檻。&lt;/p>
&lt;p>Binary release 的契約是：&lt;strong>上游負責編譯、下游負責下載&lt;/strong>。這條契約成立需要三個前提同時滿足：&lt;/p>
&lt;ol>
&lt;li>CI 能在多平台 cross-compile 出可執行檔（macOS x64/arm64、Linux x64/arm64、Windows x64）&lt;/li>
&lt;li>編譯產物有穩定 URL，下游可以用一行 shell 命令取得&lt;/li>
&lt;li>安裝過程不依賴開發環境（不需要 git clone、不需要 build toolchain）&lt;/li>
&lt;/ol>
&lt;p>達成這三點需要一個 release 工具鏈把 build matrix、artifact 上傳、installer script 產生包成一個 tag-driven 的 workflow。Rust 生態用 &lt;a href="https://opensource.axo.dev/cargo-dist/">cargo-dist&lt;/a>、Go 生態用 &lt;a href="https://goreleaser.com/">goreleaser&lt;/a>、語言中性的方案則是手刻 GitHub Actions matrix。三者觸發條件相同（push semver tag）、產物落點相同（GitHub Release assets），只在 build pipeline 細節有差。&lt;/p>
&lt;h2 id="tag-driven-release-的鏈路">Tag-driven release 的鏈路&lt;/h2>
&lt;p>Tag-driven 的核心設計：&lt;strong>push tag 是發版意圖的唯一訊號&lt;/strong>。這條因果鏈每一環都要實作起來才會通：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">維護者 push tag vX.Y.Z ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → release.yml workflow 觸發（tag pattern 匹配）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → cross-compile to N platforms（GitHub Actions matrix）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → 打包成 &amp;lt;pkg&amp;gt;-x86_64-apple-darwin.tar.xz 等 N 個 archive
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> → 產生 &amp;lt;pkg&amp;gt;-installer.sh / .ps1（內嵌指向上述 archive 的 download URL）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> → 建立 GitHub Release vX.Y.Z
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> → 上傳所有 archive + installer 為 release assets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> → GitHub 自動把 vX.Y.Z 的 assets 也鏡射到 /releases/latest/download/&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條鏈路上每個節點都是一塊要設定的工作：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tag pattern&lt;/strong>：cargo-dist 預設匹配 &lt;code>**[0-9]+.[0-9]+.[0-9]+*&lt;/code>，符合 semver 才會觸發&lt;/li>
&lt;li>&lt;strong>Build matrix&lt;/strong>：在 &lt;code>Cargo.toml&lt;/code> 的 &lt;code>[workspace.metadata.dist]&lt;/code> 宣告 &lt;code>targets = [...]&lt;/code>，cargo-dist 會展開成對應的 GitHub Actions runners&lt;/li>
&lt;li>&lt;strong>Pre-build hooks&lt;/strong>：如果編譯前需要產生程式碼或下載資料，要透過 &lt;code>github-build-setup&lt;/code> 注入（zhtw-mcp 的案例就是要先跑 &lt;code>gen-s2t-tables.py&lt;/code> 產生 &lt;code>s2t_data.rs&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Installer 範本&lt;/strong>：cargo-dist 內建 &lt;code>shell&lt;/code> / &lt;code>powershell&lt;/code> / &lt;code>homebrew&lt;/code> / &lt;code>npm&lt;/code> 等多種 installer 產生器，在 &lt;code>installers = [...]&lt;/code> 設定&lt;/li>
&lt;li>&lt;strong>&lt;code>/releases/latest/download/&lt;/code> alias&lt;/strong>：GitHub 自動提供，指向 latest non-prerelease release 的 asset；prerelease 不會更新這個 alias&lt;/li>
&lt;/ul>
&lt;p>這也解釋了為什麼 &lt;code>git tag dev&lt;/code> 或單純 commit 到 main 都不會發版 — 那不符合 tag pattern、不是發版意圖。&lt;/p></description><content:encoded><![CDATA[<p>Binary release 是一條直接把預編譯執行檔掛在 GitHub Release 下供使用者下載的發版通道，跳過 package registry。它解決的問題是：當套件不是函式庫而是 CLI binary，下游不需要重新編譯、也不一定有對應語言的 toolchain 時，需要一條「平台無關、即拿即用」的安裝路線。本篇用 <a href="https://github.com/sysprog21/zhtw-mcp"><code>zhtw-mcp</code></a> 為陪跑案例，公開協作軌跡可直接對照 <a href="https://github.com/sysprog21/zhtw-mcp/issues/35">issue #35</a> 與 <a href="https://github.com/sysprog21/zhtw-mcp/pull/40">PR #40</a>。</p>
<h2 id="為什麼需要這條通道">為什麼需要這條通道</h2>
<p>CLI binary 跟函式庫的下游使用脈絡不同。函式庫需要被同語言專案 import，自然走 registry（<code>npm install</code>、<code>pip install</code>、<code>cargo add</code>）。CLI binary 的目標讀者是「只想跑這個工具」的人，他們不一定有對應 toolchain、不想花時間編譯，也不會接受「先裝開發環境才能用」的入場門檻。</p>
<p>Binary release 的契約是：<strong>上游負責編譯、下游負責下載</strong>。這條契約成立需要三個前提同時滿足：</p>
<ol>
<li>CI 能在多平台 cross-compile 出可執行檔（macOS x64/arm64、Linux x64/arm64、Windows x64）</li>
<li>編譯產物有穩定 URL，下游可以用一行 shell 命令取得</li>
<li>安裝過程不依賴開發環境（不需要 git clone、不需要 build toolchain）</li>
</ol>
<p>達成這三點需要一個 release 工具鏈把 build matrix、artifact 上傳、installer script 產生包成一個 tag-driven 的 workflow。Rust 生態用 <a href="https://opensource.axo.dev/cargo-dist/">cargo-dist</a>、Go 生態用 <a href="https://goreleaser.com/">goreleaser</a>、語言中性的方案則是手刻 GitHub Actions matrix。三者觸發條件相同（push semver tag）、產物落點相同（GitHub Release assets），只在 build pipeline 細節有差。</p>
<h2 id="tag-driven-release-的鏈路">Tag-driven release 的鏈路</h2>
<p>Tag-driven 的核心設計：<strong>push tag 是發版意圖的唯一訊號</strong>。這條因果鏈每一環都要實作起來才會通：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">維護者 push tag vX.Y.Z         ↓
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                →  release.yml workflow 觸發（tag pattern 匹配）
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                →  cross-compile to N platforms（GitHub Actions matrix）
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                →  打包成 &lt;pkg&gt;-x86_64-apple-darwin.tar.xz 等 N 個 archive
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                →  產生 &lt;pkg&gt;-installer.sh / .ps1（內嵌指向上述 archive 的 download URL）
</span></span><span class="line"><span class="ln">6</span><span class="cl">                                →  建立 GitHub Release vX.Y.Z
</span></span><span class="line"><span class="ln">7</span><span class="cl">                                →  上傳所有 archive + installer 為 release assets
</span></span><span class="line"><span class="ln">8</span><span class="cl">                                →  GitHub 自動把 vX.Y.Z 的 assets 也鏡射到 /releases/latest/download/</span></span></code></pre></div><p>這條鏈路上每個節點都是一塊要設定的工作：</p>
<ul>
<li><strong>Tag pattern</strong>：cargo-dist 預設匹配 <code>**[0-9]+.[0-9]+.[0-9]+*</code>，符合 semver 才會觸發</li>
<li><strong>Build matrix</strong>：在 <code>Cargo.toml</code> 的 <code>[workspace.metadata.dist]</code> 宣告 <code>targets = [...]</code>，cargo-dist 會展開成對應的 GitHub Actions runners</li>
<li><strong>Pre-build hooks</strong>：如果編譯前需要產生程式碼或下載資料，要透過 <code>github-build-setup</code> 注入（zhtw-mcp 的案例就是要先跑 <code>gen-s2t-tables.py</code> 產生 <code>s2t_data.rs</code>）</li>
<li><strong>Installer 範本</strong>：cargo-dist 內建 <code>shell</code> / <code>powershell</code> / <code>homebrew</code> / <code>npm</code> 等多種 installer 產生器，在 <code>installers = [...]</code> 設定</li>
<li><strong><code>/releases/latest/download/</code> alias</strong>：GitHub 自動提供，指向 latest non-prerelease release 的 asset；prerelease 不會更新這個 alias</li>
</ul>
<p>這也解釋了為什麼 <code>git tag dev</code> 或單純 commit 到 main 都不會發版 — 那不符合 tag pattern、不是發版意圖。</p>
<h2 id="第一次搭-cargo-dist-的實作步驟">第一次搭 cargo-dist 的實作步驟</h2>
<p>從零開始的維護者視角，Rust binary 專案要搭 cargo-dist 大致是這幾步：</p>
<ol>
<li><strong>裝 cargo-dist CLI</strong>：<code>cargo install cargo-dist</code>（或從它自家的 installer 裝）</li>
<li><strong>跑 <code>dist init</code></strong>：互動式問答，選 targets、installers、CI provider（GitHub Actions），它會在 <code>Cargo.toml</code> 寫入 <code>[workspace.metadata.dist]</code> 並產生 <code>.github/workflows/release.yml</code></li>
<li><strong>檢查產出</strong>：<code>release.yml</code> 是 auto-generated、開頭會標 <code># This file was autogenerated by dist</code>，<strong>不要手改</strong>，下次 <code>dist generate</code> 會被覆蓋</li>
<li><strong>設定 pre-build hook（如果需要）</strong>：在 <code>Cargo.toml</code> 加 <code>github-build-setup = &quot;build-setup.yml&quot;</code>，把編譯前要跑的步驟寫在 <code>.github/build-setup.yml</code>（這個檔不會被 <code>dist generate</code> 覆蓋）</li>
<li><strong>設定 preflight gate（重要）</strong>：把現有的 main CI workflow 加上 <code>workflow_call</code> trigger，在 <code>Cargo.toml</code> 設 <code>plan-jobs = [&quot;./.github/workflows/main.yml&quot;]</code>，讓 release pipeline 在 cross-compile 前先確認測試全綠</li>
<li><strong>推第一個 prerelease tag 試水溫</strong>：<code>git tag v0.1.0-alpha.1 &amp;&amp; git push origin v0.1.0-alpha.1</code>，看 release.yml 跑出來的 matrix 是不是全綠</li>
<li><strong>確認 installer script 可用</strong>：在乾淨機器上跑 <code>curl ... /releases/download/v0.1.0-alpha.1/&lt;pkg&gt;-installer.sh | sh</code>（注意 prerelease 要用完整 tag URL、不是 <code>latest</code>）</li>
<li><strong>推第一個正式 tag</strong>：跑 <code>v0.1.0</code>，這時 <code>/releases/latest/download/</code> alias 才會生效</li>
<li><strong>更新 README</strong>：把 installer 安裝命令寫上去；正式版發出後就能用 <code>latest</code> URL，prerelease 階段要寫完整 tag URL</li>
<li><strong>後續維護</strong>：bump version → tag → push，cargo-dist 自動處理；只有改 <code>[workspace.metadata.dist]</code> 時才需要重跑 <code>dist generate</code></li>
</ol>
<p>第 5 步的 preflight gate 是新手最容易漏的關。沒有它的話、main 紅燈時你還是能 push tag、cargo-dist 還是會跑 cross-compile、爛 binary 還是會推到所有人。<code>workflow_call</code> 反向 reuse 這個 pattern 在 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a> 有更完整討論。</p>
<h2 id="installer-script-模式的契約">Installer script 模式的契約</h2>
<p><code>curl ... | sh</code> 是這條通道的常見下游入口。這個入口要成立，前提是上游提供可驗證產物、下游執行前有最小安全檢查。</p>
<p>cargo-dist 產生的 installer 命令長這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl --proto <span class="s1">&#39;=https&#39;</span> --tlsv1.2 -LsSf <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  https://github.com/&lt;owner&gt;/&lt;repo&gt;/releases/latest/download/&lt;pkg&gt;-installer.sh <span class="p">|</span> sh</span></span></code></pre></div><p>逐項拆解 curl 的 flag：</p>
<table>
  <thead>
      <tr>
          <th>片段</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--proto '=https'</code></td>
          <td>限制只走 HTTPS，避免被中間人 downgrade 到 HTTP</td>
      </tr>
      <tr>
          <td><code>--tlsv1.2</code></td>
          <td>拒絕舊版 TLS</td>
      </tr>
      <tr>
          <td><code>-L</code></td>
          <td>跟隨 redirect（GitHub 的 latest alias 是 302）</td>
      </tr>
      <tr>
          <td><code>-sS</code></td>
          <td>安靜但保留錯誤訊息</td>
      </tr>
      <tr>
          <td><code>-f</code></td>
          <td>HTTP 錯誤時 curl 自己 exit non-zero（不把 404 HTML 當內容 pipe 進 sh）</td>
      </tr>
      <tr>
          <td><code>| sh</code></td>
          <td>把腳本內容餵給 shell 執行</td>
      </tr>
  </tbody>
</table>
<p><code>-f</code> 那個 flag 是這條鏈路的安全點：沒有它的話、如果 release URL 暫時 404，GitHub 的 404 HTML 會被 pipe 到 sh 然後爆出一堆語法錯誤；有 <code>-f</code> 時 curl 會直接 exit 22、<code>sh</code> 不會被呼叫，使用者看到的是清楚的錯誤碼。這就是為什麼 cargo-dist 產生的範本預設帶 <code>-f</code>、不能省。</p>
<p>PowerShell 版本（<code>irm | iex</code>）的等價契約相同 — <code>Invoke-RestMethod</code> 對 404 也會丟 exception、不會把 HTML 餵給 <code>Invoke-Expression</code>。</p>
<p>Installer script 自己的內部行為：偵測平台、下載對應 archive、解壓、放到 <code>~/.local/bin</code> 或 <code>~/.cargo/bin</code>、視需要更新 PATH。這部分由 cargo-dist 範本生成、跨專案幾乎一致、維護者不需要手寫。</p>
<h2 id="最小安全基線教學案例版">最小安全基線（教學案例版）</h2>
<p>教學案例可以示範 <code>curl | sh</code>，但可維護版本要同時提供「下載、驗證、執行」路徑，讓使用者在高風險環境可切換到可審計流程。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1) 下載 installer 與 checksum</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl --proto <span class="s1">&#39;=https&#39;</span> --tlsv1.2 -LsSf <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -o /tmp/&lt;pkg&gt;-installer.sh <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  https://github.com/&lt;owner&gt;/&lt;repo&gt;/releases/download/vX.Y.Z/&lt;pkg&gt;-installer.sh
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">curl --proto <span class="s1">&#39;=https&#39;</span> --tlsv1.2 -LsSf <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  -o /tmp/&lt;pkg&gt;-checksums.txt <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  https://github.com/&lt;owner&gt;/&lt;repo&gt;/releases/download/vX.Y.Z/&lt;pkg&gt;-checksums.txt
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 2) 驗證 checksum（sha256sum 或 shasum 擇一）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">sha256sum -c /tmp/&lt;pkg&gt;-checksums.txt --ignore-missing
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># shasum -a 256 -c /tmp/&lt;pkg&gt;-checksums.txt</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 3) 執行 installer</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">sh /tmp/&lt;pkg&gt;-installer.sh</span></span></code></pre></div><p>這條路徑的責任分工是：</p>
<ol>
<li>上游：發布 installer 與對應 checksum（或 provenance）。</li>
<li>下游：先驗證再執行。</li>
<li>文件：同時提供快速路徑與可審計路徑，並標明適用情境。</li>
</ol>
<h2 id="pre-releaseearly-adopter通道">Pre-release（early adopter）通道</h2>
<p>第一個正式 release 之前，pipeline 本身需要先被驗證。這時 prerelease tag（<code>v0.1.0-alpha.1</code>、<code>v0.1.0-rc1</code> 之類）就派上用場：</p>
<ul>
<li><strong>作為 pipeline 自身的測試</strong>：tag 推下去能跑出多平台 binary，代表 cargo-dist 設定正確</li>
<li><strong>給 early adopter 試用</strong>：願意當先驅者的使用者可以用完整 tag URL 取得 binary</li>
<li><strong>不污染 latest alias</strong>：GitHub 的 <code>releases/latest/download/</code> 只指向 non-prerelease，所以 prerelease 不會「假發版」</li>
</ul>
<p>代價是 prerelease 沒有 stable URL — 每個版本要寫完整 tag、不能用 <code>latest</code>。所以 README 安裝段落在 v0.1.0 出來之前要寫：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Pre-release example（給 early adopter）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl --proto <span class="s1">&#39;=https&#39;</span> --tlsv1.2 -LsSf <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  https://github.com/&lt;owner&gt;/&lt;repo&gt;/releases/download/v0.1.0-alpha.1/&lt;pkg&gt;-installer.sh <span class="p">|</span> sh</span></span></code></pre></div><p>正式 v0.1.0 出來之後再切回 <code>latest</code> URL。這是 zhtw-mcp issue #35 討論裡 hydai 提的折衷方案，能讓社群在 pipeline 完備前先試用、又不誤導不明就裡的使用者以為正式版已就位。</p>
<h2 id="zhtw-mcp-案例社群協作把-release-pipeline-搭起來">zhtw-mcp 案例：社群協作把 release pipeline 搭起來</h2>
<p>zhtw-mcp 的 <a href="https://github.com/sysprog21/zhtw-mcp/issues/35">issue #35</a> 跟 <a href="https://github.com/sysprog21/zhtw-mcp/pull/40">PR #40</a> 是這條搭建過程的活案例。整個討論的時間軸：</p>
<ol>
<li><strong>dlackty 提 issue #35</strong>：建議導入 cargo-dist + Homebrew、列出建議 targets、指出 <code>s2t_data.rs</code> 需要 pre-build hook</li>
<li><strong>作者 jserv 回應</strong>：認同方向，但坦承自己 Rust 經驗有限、這個專案部分目的就是為了學 Rust 生態，邀請社群提 PR 推進</li>
<li><strong>hydai 開 PR #40</strong>：第一次用 cargo-dist，自己也在學，誠實表示「想知道方向對不對，希望熟手能接手」，並引用自己之前用 knope 手刻 release 的另一個 repo 作為對照</li>
<li><strong>jserv 提到 installer URL 失效</strong>：README 已經寫了 <code>releases/latest/download/...</code>，但還沒有正式 release，建議用 pre-release 給 early adopter</li>
<li><strong>hydai 提議 <code>v0.1.0-alpha.1</code></strong>：作為 early adopter 通道、提醒 prerelease 沒有 latest alias、要用完整 tag URL</li>
</ol>
<p>這個討論留下幾個值得學的點：</p>
<ul>
<li><strong>公開承認還在學是好事</strong>：jserv 直接說「我 Rust 經驗有限、我也在學」、hydai 說「我第一次用 cargo-dist」，這比假裝專家有效率多了。社群協作的核心是大家都看到同一個未完成狀態、一起補。</li>
<li><strong>README 先寫安裝命令再補 release 是常見順序</strong>：把 release 路線當作目標釘出來、再倒推實作，是刻意的設計。先寫文件再補 pipeline 的順序也讓 issue #35 / PR #40 更容易聚焦。</li>
<li><strong>特殊 build hook 是 cargo-dist 的明確支援點</strong>：zhtw-mcp 需要在編譯前跑 <code>gen-s2t-tables.py</code> 產生 <code>s2t_data.rs</code>，這正好是 <code>github-build-setup</code> 設計給的場景。如果你的 repo 有類似「編譯前要產生程式碼／下載資料」的需求、不必為此放棄 cargo-dist。</li>
<li><strong>Pre-release 是 pipeline 學習期的合理工具</strong>：先用 <code>v0.1.0-alpha.1</code> 把 pipeline 跑通、把問題暴露出來，比等到一切完美才發版更有效率。</li>
</ul>
<p>跟著這個 issue 串看完一輪、可以得到一個從零搭 cargo-dist 的真實參照框架，比官方文件更貼近實際遇到的問題。</p>
<h2 id="homebrew-通道cargo-dist-怎麼幫你出-formula">Homebrew 通道：cargo-dist 怎麼幫你出 formula</h2>
<p><code>brew install</code> 是 macOS 使用者最熟的安裝路線，但 Homebrew 有兩種發版形式：</p>
<table>
  <thead>
      <tr>
          <th>形式</th>
          <th>怎麼裝</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Homebrew core</td>
          <td><code>brew install &lt;pkg&gt;</code></td>
          <td>高 — 要過 homebrew-core 的 PR review，門檻嚴</td>
      </tr>
      <tr>
          <td>Homebrew tap</td>
          <td><code>brew install &lt;user&gt;/&lt;tap&gt;/&lt;pkg&gt;</code></td>
          <td>低 — 在自己的 GitHub repo <code>homebrew-&lt;tap&gt;</code> 放 formula</td>
      </tr>
  </tbody>
</table>
<p>cargo-dist 預設支援的是後者（tap）。設定方式是在 <code>[workspace.metadata.dist]</code> 加：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">installers</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;shell&#34;</span><span class="p">,</span> <span class="s2">&#34;powershell&#34;</span><span class="p">,</span> <span class="s2">&#34;homebrew&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">tap</span> <span class="p">=</span> <span class="s2">&#34;&lt;your-github-username&gt;/homebrew-&lt;tap-name&gt;&#34;</span></span></span></code></pre></div><p>然後在 GitHub 開一個叫 <code>homebrew-&lt;tap-name&gt;</code> 的 repo（命名規則是 Homebrew 強制的），cargo-dist 會在每次 release 自動 push 一個更新過的 formula 到那個 repo。下游使用者只要：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">brew tap &lt;your-github-username&gt;/&lt;tap-name&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">brew install &lt;pkg&gt;</span></span></code></pre></div><p>要走 homebrew-core 是另一個層級的事 — 需要套件夠成熟、有穩定使用者基數、有清楚的 license、過 homebrew-core maintainer 的 review。多數新專案先做 tap、累積使用者跟成熟度後再考慮 core。</p>
<h2 id="上線前的最後檢查">上線前的最後檢查</h2>
<p>第一個正式 v0.1.0 推出去之前最後跑一遍：</p>
<ul>
<li><input disabled="" type="checkbox"> Prerelease tag（<code>v0.1.0-alpha.1</code> 之類）跑過 release.yml、cross-compile matrix 全綠</li>
<li><input disabled="" type="checkbox"> 從乾淨機器跑 README 寫的 installer 命令、從下載到執行整條順</li>
<li><input disabled="" type="checkbox"> Pre-build hook（如果有）在所有 platform 都能跑、不依賴特定 OS</li>
<li><input disabled="" type="checkbox"> Preflight gate 的 <code>workflow_call</code> reuse 確實 block 住紅燈 main</li>
<li><input disabled="" type="checkbox"> README 的 installer URL 跟實際 asset 命名規則一致（cargo-dist 會用 <code>&lt;pkg&gt;-installer.sh</code>、不要寫成 <code>install.sh</code>）</li>
<li><input disabled="" type="checkbox"> Changelog 跟 tag 對齊（cargo-dist 會把 changelog 抓進 release notes）</li>
<li><input disabled="" type="checkbox"> 有提供可審計安裝路徑（下載 + checksum/provenance 驗證 + 執行）</li>
</ul>
<p>第一條 v0.1.0 推出去後 <code>releases/latest/download/...</code> alias 才會生效、那時就能把 README 改成 <code>latest</code> URL、徹底完成這條通道的搭建。</p>
<h2 id="來源與規格">來源與規格</h2>
<ul>
<li>cargo-dist 官方文件：<a href="https://opensource.axo.dev/cargo-dist/">https://opensource.axo.dev/cargo-dist/</a></li>
<li>cargo-dist GitHub Action / 生成流程：<a href="https://github.com/axodotdev/cargo-dist">https://github.com/axodotdev/cargo-dist</a></li>
<li>GitHub Releases 與 latest 行為：<a href="https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases">https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases</a></li>
<li>zhtw-mcp 案例 issue：<a href="https://github.com/sysprog21/zhtw-mcp/issues/35">https://github.com/sysprog21/zhtw-mcp/issues/35</a></li>
<li>zhtw-mcp 案例 PR：<a href="https://github.com/sysprog21/zhtw-mcp/pull/40">https://github.com/sysprog21/zhtw-mcp/pull/40</a></li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解整體 release 類型分類：回 <a href="../">Package / Library Release CI/CD</a>。</li>
<li>想理解 workflow_call 的反向 reuse：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>想理解 release workflow 紅燈時的處理：讀 <a href="../../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>想理解 artifact 可信度：讀 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance</a>。</li>
</ul>
]]></content:encoded></item><item><title>CI Pipeline</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/ci-pipeline/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/ci-pipeline/</guid><description>&lt;p>CI Pipeline 的核心概念是「在合併前自動驗證變更」。它把品質門檻前移，讓問題在進主線前被發現。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>CI Pipeline 位在開發提交、pull request 與主線保護之間，常由 lint、test、build、security check 組成。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>PR 需要依賴檢查結果決定能否合併。&lt;/li>
&lt;li>團隊需要一致的失敗判讀入口。&lt;/li>
&lt;li>本機通過但共享流程失敗時，需要明確定位差異。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>前端專案會把 markdown lint、browser test 與 production build 放在同一套 CI 驗證入口。後端專案則可能加入 contract test、migration check 或 image scan。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>CI Pipeline 要定義必跑檢查、失敗回饋路由與執行時間上限，讓綠燈具備可發布前提。&lt;/p></description><content:encoded><![CDATA[<p>CI Pipeline 的核心概念是「在合併前自動驗證變更」。它把品質門檻前移，讓問題在進主線前被發現。</p>
<h2 id="概念位置">概念位置</h2>
<p>CI Pipeline 位在開發提交、pull request 與主線保護之間，常由 lint、test、build、security check 組成。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>PR 需要依賴檢查結果決定能否合併。</li>
<li>團隊需要一致的失敗判讀入口。</li>
<li>本機通過但共享流程失敗時，需要明確定位差異。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>前端專案會把 markdown lint、browser test 與 production build 放在同一套 CI 驗證入口。後端專案則可能加入 contract test、migration check 或 image scan。</p>
<h2 id="設計責任">設計責任</h2>
<p>CI Pipeline 要定義必跑檢查、失敗回饋路由與執行時間上限，讓綠燈具備可發布前提。</p>
]]></content:encoded></item><item><title>CI/CD 失敗到修復發布流程</title><link>https://tarrragon.github.io/blog/ci/github-actions-failure-flow/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/github-actions-failure-flow/</guid><description>&lt;p>CI/CD 失敗處理的核心責任是把紅燈轉成明確的下一步路由。紅燈本身是驗證或交付層的訊號；工程流程要做的是找出失敗層、重現同一個條件、修正後重新讓 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline&lt;/a> 證明變更可發布。&lt;/p>
&lt;h2 id="失敗後先看什麼">失敗後先看什麼&lt;/h2>
&lt;p>失敗後第一步是定位 workflow 與 job。CI/CD 系統會把一次 push、pull request、tag 或 release 拆成多個 workflow，每個 workflow 下面又有多個 job；真正的下一步取決於是哪一層失敗。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗位置&lt;/th>
 &lt;th>常見原因&lt;/th>
 &lt;th>下一步路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Lint / format&lt;/td>
 &lt;td>程式碼、文件或設定格式不符&lt;/td>
 &lt;td>回本機跑同一條 lint / format 命令&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>單元、整合、瀏覽器或裝置測試回歸&lt;/td>
 &lt;td>下載 report，回本機用同條件重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>編譯、bundle、package 或靜態產物失敗&lt;/td>
 &lt;td>回本機跑 production build 入口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Package&lt;/td>
 &lt;td>image、app bundle、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a> 產生失敗&lt;/td>
 &lt;td>檢查版本、簽章、registry 或路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>hosting、runtime、store 或權限設定&lt;/td>
 &lt;td>先確認 build &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a> 是否已成功&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Lint / format 失敗代表靜態契約沒有通過。常見情境是程式格式、文件格式、型別檢查、schema 或設定規則不符合規範。這類失敗的修復路徑通常很短：讀錯誤訊息、修正來源、必要時跑 formatter，再提交修正。&lt;/p>
&lt;p>Test 失敗代表某個行為或契約沒有符合預期。這類失敗要先看 report、screenshot、trace、device log 或 error context，確認是功能真的回歸、測試假設過期，還是測試環境缺少 production-like artifact。直接改測試前，要先確認測試原本守的是哪個使用者或系統行為。&lt;/p>
&lt;p>Build 失敗代表 pipeline 尚未產生可部署產物。這類失敗通常來自編譯錯誤、bundle 設定、依賴版本、環境變數、template 或資源路徑。修復時以專案定義的 production build 命令作為最小重現入口。&lt;/p>
&lt;p>Deploy 失敗代表發布動作沒有完成。這類失敗需要先區分 artifact 是否存在、發布通道權限是否正確、環境保護是否放行。若測試與 build 已成功，deploy 失敗多半是發布通道問題；若 artifact 沒有產生，應回到 build 或 package 階段。&lt;/p>
&lt;h2 id="本機重現流程">本機重現流程&lt;/h2>
&lt;p>本機重現的責任是讓修復建立在同一個驗證條件上。CI 是用乾淨環境執行的一組命令；只要能在本機跑出同樣的失敗，修復就能被快速驗證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">make build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">make &lt;span class="nb">test&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">make deploy-dry-run&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build 命令驗證 production artifact 是否能產生。這一步應該接近 CI 使用的 build 入口，避免開發模式遮蔽 production 問題。&lt;/p>
&lt;p>Test 命令驗證產物或程式行為。前端可能是 browser test，後端可能是 integration / contract test，App 可能是 device test，Docker 可能是 image scan 或 smoke test。&lt;/p>
&lt;p>Deploy dry-run 命令驗證發布前條件。高風險部署至少要能檢查 artifact、權限、環境與版本資訊；沒有 dry-run 的專案，也應保留對等的 preflight check。&lt;/p>
&lt;h2 id="修復與重新觸發">修復與重新觸發&lt;/h2>
&lt;p>修復流程的核心是用新 commit 讓 CI 重新驗證。一般流程不需要刪掉失敗 commit，也不需要 force push；失敗 commit 留在歷史裡，後續 fix commit 會形成清楚的修復脈絡。&lt;/p>
&lt;ol>
&lt;li>讀失敗 job 的 log 或 artifact。&lt;/li>
&lt;li>在本機跑對應命令重現。&lt;/li>
&lt;li>修改最小必要範圍。&lt;/li>
&lt;li>跑同一條本機命令確認修復。&lt;/li>
&lt;li>commit 並 push。&lt;/li>
&lt;li>等 GitHub Actions 重新跑。&lt;/li>
&lt;/ol>
&lt;p>這個流程的好處是保留可追溯性。日後再看到同類失敗，可以從 commit history 與 CI log 找到當時的判讀方式。&lt;/p>
&lt;h2 id="發布-gate-路由">發布 gate 路由&lt;/h2>
&lt;p>發布 gate 的責任是把「是否進入下一階段」變成明確條件。這一頁只處理失敗後的操作路由；&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">required checks&lt;/a>、job &lt;code>needs&lt;/code>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">environment protection&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff&lt;/a> 的設計原理，獨立放在 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>CI/CD 失敗處理的核心責任是把紅燈轉成明確的下一步路由。紅燈本身是驗證或交付層的訊號；工程流程要做的是找出失敗層、重現同一個條件、修正後重新讓 <a href="/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline</a> 證明變更可發布。</p>
<h2 id="失敗後先看什麼">失敗後先看什麼</h2>
<p>失敗後第一步是定位 workflow 與 job。CI/CD 系統會把一次 push、pull request、tag 或 release 拆成多個 workflow，每個 workflow 下面又有多個 job；真正的下一步取決於是哪一層失敗。</p>
<table>
  <thead>
      <tr>
          <th>失敗位置</th>
          <th>常見原因</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Lint / format</td>
          <td>程式碼、文件或設定格式不符</td>
          <td>回本機跑同一條 lint / format 命令</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>單元、整合、瀏覽器或裝置測試回歸</td>
          <td>下載 report，回本機用同條件重現</td>
      </tr>
      <tr>
          <td>Build</td>
          <td>編譯、bundle、package 或靜態產物失敗</td>
          <td>回本機跑 production build 入口</td>
      </tr>
      <tr>
          <td>Package</td>
          <td>image、app bundle、<a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a> 產生失敗</td>
          <td>檢查版本、簽章、registry 或路徑</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>hosting、runtime、store 或權限設定</td>
          <td>先確認 build <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a> 是否已成功</td>
      </tr>
  </tbody>
</table>
<p>Lint / format 失敗代表靜態契約沒有通過。常見情境是程式格式、文件格式、型別檢查、schema 或設定規則不符合規範。這類失敗的修復路徑通常很短：讀錯誤訊息、修正來源、必要時跑 formatter，再提交修正。</p>
<p>Test 失敗代表某個行為或契約沒有符合預期。這類失敗要先看 report、screenshot、trace、device log 或 error context，確認是功能真的回歸、測試假設過期，還是測試環境缺少 production-like artifact。直接改測試前，要先確認測試原本守的是哪個使用者或系統行為。</p>
<p>Build 失敗代表 pipeline 尚未產生可部署產物。這類失敗通常來自編譯錯誤、bundle 設定、依賴版本、環境變數、template 或資源路徑。修復時以專案定義的 production build 命令作為最小重現入口。</p>
<p>Deploy 失敗代表發布動作沒有完成。這類失敗需要先區分 artifact 是否存在、發布通道權限是否正確、環境保護是否放行。若測試與 build 已成功，deploy 失敗多半是發布通道問題；若 artifact 沒有產生，應回到 build 或 package 階段。</p>
<h2 id="本機重現流程">本機重現流程</h2>
<p>本機重現的責任是讓修復建立在同一個驗證條件上。CI 是用乾淨環境執行的一組命令；只要能在本機跑出同樣的失敗，修復就能被快速驗證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">make build
</span></span><span class="line"><span class="ln">2</span><span class="cl">make <span class="nb">test</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">make deploy-dry-run</span></span></code></pre></div><p>Build 命令驗證 production artifact 是否能產生。這一步應該接近 CI 使用的 build 入口，避免開發模式遮蔽 production 問題。</p>
<p>Test 命令驗證產物或程式行為。前端可能是 browser test，後端可能是 integration / contract test，App 可能是 device test，Docker 可能是 image scan 或 smoke test。</p>
<p>Deploy dry-run 命令驗證發布前條件。高風險部署至少要能檢查 artifact、權限、環境與版本資訊；沒有 dry-run 的專案，也應保留對等的 preflight check。</p>
<h2 id="修復與重新觸發">修復與重新觸發</h2>
<p>修復流程的核心是用新 commit 讓 CI 重新驗證。一般流程不需要刪掉失敗 commit，也不需要 force push；失敗 commit 留在歷史裡，後續 fix commit 會形成清楚的修復脈絡。</p>
<ol>
<li>讀失敗 job 的 log 或 artifact。</li>
<li>在本機跑對應命令重現。</li>
<li>修改最小必要範圍。</li>
<li>跑同一條本機命令確認修復。</li>
<li>commit 並 push。</li>
<li>等 GitHub Actions 重新跑。</li>
</ol>
<p>這個流程的好處是保留可追溯性。日後再看到同類失敗，可以從 commit history 與 CI log 找到當時的判讀方式。</p>
<h2 id="發布-gate-路由">發布 gate 路由</h2>
<p>發布 gate 的責任是把「是否進入下一階段」變成明確條件。這一頁只處理失敗後的操作路由；<a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">required checks</a>、job <code>needs</code>、<a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">environment protection</a> 與 <a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff</a> 的設計原理，獨立放在 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</p>
<h2 id="常見處理情境">常見處理情境</h2>
<p>CI 失敗但本機通過時，優先檢查環境差異。常見差異包括語言版本、套件管理器版本、缺少子模組、缺少 build artifact、測試依賴未安裝、時區或檔案大小寫差異。這類問題要把版本與建置前置條件寫進 workflow、Makefile 或 script，讓重現條件成為專案的一部分。</p>
<p>測試不穩定時，優先把 <a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test</a> 狀態標出來並建立 owner。短期可以隔離或重跑，長期要找到不穩定來源，例如等待條件錯誤、外部網路依賴、時間假設、測試資料不穩或動畫 transition 尚未完成。測試不穩定會降低 gate 信任度，因此它本身就是需要治理的 CI 問題。</p>
<p>Deploy 失敗但測試通過時，優先看 artifact 與權限。若 build output 存在且可下載，問題通常在部署通道、token permission 或 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">environment protection</a>；若 artifact 缺失，就回到 build job。</p>
<h2 id="反模式與替代做法">反模式與替代做法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看到紅燈直接重跑</td>
          <td>掩蓋 flaky 或環境問題</td>
          <td>先看失敗 log，再決定是否重跑</td>
      </tr>
      <tr>
          <td>用 <code>--no-verify</code> 或跳過 CI</td>
          <td>把局部問題帶進主線</td>
          <td>修掉 gate 或明確記錄例外</td>
      </tr>
      <tr>
          <td>CI 與本機命令不同</td>
          <td>本機通過但 CI 失敗</td>
          <td>把命令收斂到 Makefile / npm script</td>
      </tr>
      <tr>
          <td>測試直接打外部服務</td>
          <td>網路與第三方狀態污染判斷</td>
          <td>使用 fixture、mock 或可控環境</td>
      </tr>
  </tbody>
</table>
<p>反模式的共同問題是讓 CI 失去判讀價值。CI 的目標是讓綠燈代表「這次變更在定義好的條件下可發布」。</p>
<h2 id="最小可用流程">最小可用流程</h2>
<p>最小可用流程是讓每次變更都有同一條路徑。對小型靜態網站或個人 blog，先做到以下四件事，就能形成穩定發布節奏。</p>
<ol>
<li><code>push</code> 或 PR 觸發 lint / test / build。</li>
<li>production build 有單一入口。</li>
<li>測試失敗時保留 artifact 或 report。</li>
<li>deploy 只接受測試與 build 通過後的產物。</li>
</ol>
<p>這套流程建立後，CI 紅燈就會成為清楚的路由訊號：哪一層壞、用哪個命令重現、修完後用哪個 gate 放行。</p>
<p>若變更涉及後端服務，可再對照 backend 知識卡的 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a>、<a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy</a> 與 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a> 進一步細化故障處理順序與放行條件。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>需要理解 CI 在可靠性模組的位置：讀 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>。</li>
<li>需要看靜態站部署案例：讀 <a href="../blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>需要理解 CI gate 設計：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>需要理解發布阻擋策略：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
</ul>
]]></content:encoded></item><item><title>本 blog 專案的 GitHub Actions workflow</title><link>https://tarrragon.github.io/blog/ci/blog-project-deploy/github-actions-workflows/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/blog-project-deploy/github-actions-workflows/</guid><description>&lt;p>本 blog 的 GitHub Actions workflow 負責把內容檢查、瀏覽器回歸測試、Hugo 發布與 Claude 協作分成不同自動化流程。每條 workflow 都是一個獨立入口；維護時要先分清楚它是在保護內容品質、使用者行為、發布產物，還是協作流程。&lt;/p>
&lt;h2 id="workflow-總覽">Workflow 總覽&lt;/h2>
&lt;p>本專案目前有五條 workflow。三條屬於 CI / CD 主流程，兩條屬於 Claude 協作輔助流程。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Workflow&lt;/th>
 &lt;th>檔案&lt;/th>
 &lt;th>觸發條件&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>md-check&lt;/code>&lt;/td>
 &lt;td>&lt;code>.github/workflows/md-check.yml&lt;/code>&lt;/td>
 &lt;td>push / pull request 到 &lt;code>main&lt;/code>&lt;/td>
 &lt;td>檢查 content Markdown 契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>Playwright tests&lt;/code>&lt;/td>
 &lt;td>&lt;code>.github/workflows/playwright.yml&lt;/code>&lt;/td>
 &lt;td>push / pull request 到 &lt;code>main&lt;/code>&lt;/td>
 &lt;td>驗證瀏覽器層行為與版面回歸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>Deploy Hugo site to Pages&lt;/code>&lt;/td>
 &lt;td>&lt;code>.github/workflows/deploy.yml&lt;/code>&lt;/td>
 &lt;td>push 到 &lt;code>main&lt;/code>&lt;/td>
 &lt;td>建置 Hugo、產生搜尋索引並部署&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>Claude Code&lt;/code>&lt;/td>
 &lt;td>&lt;code>.github/workflows/claude.yml&lt;/code>&lt;/td>
 &lt;td>issue / comment / review 叫 Claude&lt;/td>
 &lt;td>讓 Claude 讀 issue、PR 與 CI 結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>Claude Code Review&lt;/code>&lt;/td>
 &lt;td>&lt;code>.github/workflows/claude-code-review.yml&lt;/code>&lt;/td>
 &lt;td>PR opened / synchronize 等事件&lt;/td>
 &lt;td>對 PR 進行 Claude code review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的責任是提供入口。看到 GitHub Actions 紅燈時，先對照 workflow 名稱，把失敗歸到內容檢查、瀏覽器測試、部署或協作流程。&lt;/p>
&lt;h2 id="md-check">&lt;code>md-check&lt;/code>&lt;/h2>
&lt;p>&lt;code>md-check&lt;/code> 的責任是讓 &lt;code>content/&lt;/code> 裡的 Markdown 維持同一套結構契約。它會先用 Go build 出 &lt;code>scripts/mdtools&lt;/code>，再依序執行 formatter 檢查、lint 與卡片連結檢查。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">md-check&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">main]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">main]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這條 workflow 的核心步驟是：&lt;/p>
&lt;ol>
&lt;li>&lt;code>actions/checkout@v6&lt;/code>&lt;/li>
&lt;li>&lt;code>actions/setup-go@v6&lt;/code>&lt;/li>
&lt;li>&lt;code>go build -o ../../bin/mdtools&lt;/code>&lt;/li>
&lt;li>&lt;code>./bin/mdtools fmt --check content/&lt;/code>&lt;/li>
&lt;li>&lt;code>./bin/mdtools lint content/&lt;/code>&lt;/li>
&lt;li>&lt;code>./bin/mdtools cards content/&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>&lt;code>md-check&lt;/code> 失敗時，下一步是回本機跑同一組命令。&lt;code>fmt --check&lt;/code> 失敗代表格式可由 &lt;code>fmt --fix&lt;/code> 修正；&lt;code>lint&lt;/code> 失敗代表標題、front matter、URL、code block 等結構契約不符；&lt;code>cards&lt;/code> 失敗代表卡片連結、orphan 或 K4 規則需要修。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">./bin/mdtools fmt --check content/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">./bin/mdtools lint content/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">./bin/mdtools cards content/&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>維護這條 workflow 時，規則來源要和 &lt;a href="https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查&lt;/a> 對齊。改 &lt;code>scripts/mdtools/internal/rules/&lt;/code> 時，也要同步更新規範文章，避免 CI 行為和文件描述分叉。&lt;/p>
&lt;h2 id="playwright-tests">&lt;code>Playwright tests&lt;/code>&lt;/h2>
&lt;p>&lt;code>Playwright tests&lt;/code> 的責任是驗證使用者可見行為。它會先建出完整 Hugo site 與 Pagefind index，再用 Chromium 驗證搜尋、版面與互動。&lt;/p></description><content:encoded><![CDATA[<p>本 blog 的 GitHub Actions workflow 負責把內容檢查、瀏覽器回歸測試、Hugo 發布與 Claude 協作分成不同自動化流程。每條 workflow 都是一個獨立入口；維護時要先分清楚它是在保護內容品質、使用者行為、發布產物，還是協作流程。</p>
<h2 id="workflow-總覽">Workflow 總覽</h2>
<p>本專案目前有五條 workflow。三條屬於 CI / CD 主流程，兩條屬於 Claude 協作輔助流程。</p>
<table>
  <thead>
      <tr>
          <th>Workflow</th>
          <th>檔案</th>
          <th>觸發條件</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>md-check</code></td>
          <td><code>.github/workflows/md-check.yml</code></td>
          <td>push / pull request 到 <code>main</code></td>
          <td>檢查 content Markdown 契約</td>
      </tr>
      <tr>
          <td><code>Playwright tests</code></td>
          <td><code>.github/workflows/playwright.yml</code></td>
          <td>push / pull request 到 <code>main</code></td>
          <td>驗證瀏覽器層行為與版面回歸</td>
      </tr>
      <tr>
          <td><code>Deploy Hugo site to Pages</code></td>
          <td><code>.github/workflows/deploy.yml</code></td>
          <td>push 到 <code>main</code></td>
          <td>建置 Hugo、產生搜尋索引並部署</td>
      </tr>
      <tr>
          <td><code>Claude Code</code></td>
          <td><code>.github/workflows/claude.yml</code></td>
          <td>issue / comment / review 叫 Claude</td>
          <td>讓 Claude 讀 issue、PR 與 CI 結果</td>
      </tr>
      <tr>
          <td><code>Claude Code Review</code></td>
          <td><code>.github/workflows/claude-code-review.yml</code></td>
          <td>PR opened / synchronize 等事件</td>
          <td>對 PR 進行 Claude code review</td>
      </tr>
  </tbody>
</table>
<p>這張表的責任是提供入口。看到 GitHub Actions 紅燈時，先對照 workflow 名稱，把失敗歸到內容檢查、瀏覽器測試、部署或協作流程。</p>
<h2 id="md-check"><code>md-check</code></h2>
<p><code>md-check</code> 的責任是讓 <code>content/</code> 裡的 Markdown 維持同一套結構契約。它會先用 Go build 出 <code>scripts/mdtools</code>，再依序執行 formatter 檢查、lint 與卡片連結檢查。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">md-check</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span></span></span></code></pre></div><p>這條 workflow 的核心步驟是：</p>
<ol>
<li><code>actions/checkout@v6</code></li>
<li><code>actions/setup-go@v6</code></li>
<li><code>go build -o ../../bin/mdtools</code></li>
<li><code>./bin/mdtools fmt --check content/</code></li>
<li><code>./bin/mdtools lint content/</code></li>
<li><code>./bin/mdtools cards content/</code></li>
</ol>
<p><code>md-check</code> 失敗時，下一步是回本機跑同一組命令。<code>fmt --check</code> 失敗代表格式可由 <code>fmt --fix</code> 修正；<code>lint</code> 失敗代表標題、front matter、URL、code block 等結構契約不符；<code>cards</code> 失敗代表卡片連結、orphan 或 K4 規則需要修。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">./bin/mdtools fmt --check content/
</span></span><span class="line"><span class="ln">2</span><span class="cl">./bin/mdtools lint content/
</span></span><span class="line"><span class="ln">3</span><span class="cl">./bin/mdtools cards content/</span></span></code></pre></div><p>維護這條 workflow 時，規則來源要和 <a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a> 對齊。改 <code>scripts/mdtools/internal/rules/</code> 時，也要同步更新規範文章，避免 CI 行為和文件描述分叉。</p>
<h2 id="playwright-tests"><code>Playwright tests</code></h2>
<p><code>Playwright tests</code> 的責任是驗證使用者可見行為。它會先建出完整 Hugo site 與 Pagefind index，再用 Chromium 驗證搜尋、版面與互動。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Playwright tests</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span></span></span></code></pre></div><p>這條 workflow 的核心步驟是：</p>
<ol>
<li>checkout，並包含 submodules</li>
<li>安裝 Hugo <code>0.148.2</code> extended</li>
<li>安裝 Node <code>24</code></li>
<li><code>npm ci</code></li>
<li><code>npx playwright install --with-deps chromium</code></li>
<li><code>make site</code></li>
<li><code>npx playwright test</code></li>
<li>失敗時上傳 <code>playwright-report/</code></li>
</ol>
<p><code>make site</code> 是這條 workflow 的關鍵前置條件。它會產生 Hugo 靜態檔與三份 Pagefind index：<code>pagefind</code>、<code>pagefind-title</code>、<code>pagefind-content</code>。如果只跑 <code>hugo --minify</code> 就跑 Playwright，搜尋測試會因為缺少 index 而失敗。</p>
<p>Playwright 失敗時，下一步是下載 <code>playwright-report</code> 或讀 error context。若失敗發生在搜尋頁，先確認 <code>make site</code> 是否完整成功；若失敗發生在版面，先看 screenshot、bounding box 或 computed style；若失敗發生在互動，先看 selector 是否仍對準真實 DOM。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">make site
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm test</span></span></code></pre></div><p>維護這條 workflow 時，測試要守使用者行為，不應只守 implementation detail。像 TOC RWD 這類版面行為，可以用 viewport 測試固定桌面、筆電與手機三種狀態。</p>
<h2 id="deploy-hugo-site-to-pages"><code>Deploy Hugo site to Pages</code></h2>
<p><code>Deploy Hugo site to Pages</code> 的責任是把 <code>main</code> 上的內容建置成 GitHub Pages artifact 並部署。它只在 push 到 <code>main</code> 時觸發，不在 pull request 上部署。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy Hugo site to Pages</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span>- <span class="l">main</span></span></span></code></pre></div><p>這條 workflow 有兩個 job：</p>
<table>
  <thead>
      <tr>
          <th>Job</th>
          <th>責任</th>
          <th>關鍵設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>build</code></td>
          <td>checkout、Hugo build、Pagefind、artifact</td>
          <td><code>runs-on: ubuntu-latest</code></td>
      </tr>
      <tr>
          <td><code>deploy</code></td>
          <td>發布 GitHub Pages</td>
          <td><code>needs: build</code></td>
      </tr>
  </tbody>
</table>
<p><code>build</code> job 會先跑 <code>hugo --minify</code>，並把輸出寫到 <code>hugo-build-output.txt</code>。目前它設了 <code>continue-on-error: true</code>，所以 Hugo build 失敗時會進入 Claude Debug 步驟，嘗試讓 Claude 分析錯誤並 commit 修復。</p>
<p><code>Fail if build was not fixed</code> 是第二道保護。若原本 Hugo build 失敗，workflow 會重新跑一次 <code>hugo --minify</code>；如果 Claude 沒修好，這一步會讓 workflow 停止。</p>
<p>Pagefind index 會在 Hugo build 後產生：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">npx -y pagefind --site public --root-selector main
</span></span><span class="line"><span class="ln">2</span><span class="cl">npx -y pagefind --site public --root-selector <span class="s2">&#34;article.article-content &gt; h1&#34;</span> --output-subdir pagefind-title
</span></span><span class="line"><span class="ln">3</span><span class="cl">npx -y pagefind --site public --root-selector <span class="s2">&#34;.article-body&#34;</span> --output-subdir pagefind-content</span></span></code></pre></div><p>Deploy 失敗時，下一步先分層判讀。若 <code>build</code> job 失敗，回到 Hugo 或 Pagefind；若 <code>Upload artifact</code> 成功但 <code>deploy</code> job 失敗，檢查 Pages environment、permission、artifact 與 GitHub Pages 設定。</p>
<p>這條 workflow 目前的注意事項是：deploy workflow 自己沒有直接 <code>needs</code> <code>md-check</code> 或 <code>Playwright tests</code>，因為它們是獨立 workflow。這是本專案目前的實際邊界；gate 設計原理見 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</p>
<h2 id="claude-code"><code>Claude Code</code></h2>
<p><code>Claude Code</code> 的責任是提供互動式 Claude 協作入口。它不會在每次 push 自動修程式，而是在 issue、comment 或 review 內容包含 <code>@claude</code> 時觸發。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">issue_comment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">created]</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">pull_request_review_comment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">created]</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">issues</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">opened, assigned]</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="nt">pull_request_review</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">submitted]</span></span></span></code></pre></div><p>這條 workflow 的 gate 寫在 job <code>if</code>。只有以下情境會真正執行：</p>
<ul>
<li>issue comment 包含 <code>@claude</code></li>
<li>pull request review comment 包含 <code>@claude</code></li>
<li>pull request review body 包含 <code>@claude</code></li>
<li>issue title 或 body 包含 <code>@claude</code></li>
</ul>
<p>這條 workflow 給 Claude <code>actions: read</code> 權限，讓它能讀 PR 上的 CI 結果。這對「請 Claude 看 CI 為什麼失敗」很重要，因為 Claude 需要讀 workflow run、job log 或 check 結果才能判斷。</p>
<p>維護這條 workflow 時，重點是權限最小化。它目前給的是 <code>contents: read</code>、<code>pull-requests: read</code>、<code>issues: read</code>、<code>id-token: write</code>、<code>actions: read</code>，適合互動分析；若未來要讓 Claude 直接 commit，才需要重新評估寫入權限與保護條件。</p>
<h2 id="claude-code-review"><code>Claude Code Review</code></h2>
<p><code>Claude Code Review</code> 的責任是在 PR 事件發生時跑 Claude code review。它和 <code>Claude Code</code> 不同，前者是 PR review automation，後者是被 <code>@claude</code> 叫起來的互動入口。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">opened, synchronize, ready_for_review, reopened]</span></span></span></code></pre></div><p>這條 workflow 使用 <code>code-review@claude-code-plugins</code>，prompt 是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}</span></span></code></pre></div><p>它的責任是提供 review 視角。Claude review 可以指出風險、邏輯問題或測試缺口；真正阻擋合併與發布的責任仍在 <a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks</a>、測試 workflow 與 deploy gate。</p>
<p>維護這條 workflow 時，可以依 PR 類型決定是否加 path filter。若未來只想在程式碼或 workflow 變更時觸發，可打開 <code>paths</code> 設定；若希望文章內容也被 review，就維持目前全 PR 觸發。</p>
<h2 id="本專案的發布阻擋邊界">本專案的發布阻擋邊界</h2>
<p>本 blog 的發布阻擋邊界需要同時看 YAML 與 GitHub repository 設定。這一節只記錄本專案目前能從 YAML 判讀出的事實；required checks、environment protection 與 artifact handoff 的原理不在本頁展開。</p>
<p>目前從 YAML 可直接確認的阻擋關係是：</p>
<table>
  <thead>
      <tr>
          <th>關係</th>
          <th>是否在 YAML 中明確存在</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>deploy</code> 等 <code>build</code></td>
          <td>是</td>
          <td><code>deploy</code> job 有 <code>needs: build</code></td>
      </tr>
      <tr>
          <td><code>deploy</code> 等 <code>md-check</code></td>
          <td>否</td>
          <td><code>md-check</code> 是另一條 workflow</td>
      </tr>
      <tr>
          <td><code>deploy</code> 等 Playwright</td>
          <td>否</td>
          <td><code>Playwright tests</code> 是另一條 workflow</td>
      </tr>
      <tr>
          <td>PR 需要通過測試才能合併</td>
          <td>需查 repository 設定</td>
          <td>需要看 GitHub branch protection 設定</td>
      </tr>
      <tr>
          <td>Pages deploy 需要人工審核</td>
          <td>需查 environment 設定</td>
          <td>需要看 GitHub Pages environment protection 設定</td>
      </tr>
  </tbody>
</table>
<p>若日後發現測試紅燈但 Pages 仍發布，本頁只負責指出目前 workflow 邊界；具體改法回到 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a> 判斷，並對照 <a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks</a> 與 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</p>
<h2 id="失敗時的維護路由">失敗時的維護路由</h2>
<p>失敗時的維護路由要先定位 workflow，再定位 job，再回到本機重現。這能避免在錯誤層修錯問題。</p>
<table>
  <thead>
      <tr>
          <th>紅燈位置</th>
          <th>優先看什麼</th>
          <th>本機重現命令</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>md-check</code></td>
          <td>mdtools 訊息</td>
          <td><code>./bin/mdtools lint content/</code></td>
      </tr>
      <tr>
          <td><code>Playwright tests</code></td>
          <td><code>playwright-report</code> / error context</td>
          <td><code>make site</code> 後 <code>npm test</code></td>
      </tr>
      <tr>
          <td><code>Deploy</code> 的 Hugo build</td>
          <td><code>hugo-build-output.txt</code></td>
          <td><code>hugo --minify</code></td>
      </tr>
      <tr>
          <td><code>Deploy</code> 的 Pagefind</td>
          <td>Pagefind command output</td>
          <td><code>make site</code></td>
      </tr>
      <tr>
          <td><code>Deploy</code> 的 Pages step</td>
          <td>artifact / permission / environment</td>
          <td>GitHub Actions UI + Pages 設定</td>
      </tr>
      <tr>
          <td><code>Claude Code</code></td>
          <td>secret / permission / trigger <code>if</code></td>
          <td>檢查 <code>@claude</code> 觸發文字與 secrets</td>
      </tr>
      <tr>
          <td><code>Claude Code Review</code></td>
          <td>plugin marketplace / token</td>
          <td>檢查 PR event、secret 與 action log</td>
      </tr>
  </tbody>
</table>
<p>這份路由也可以當維護 checklist。新增 workflow 時，至少要補三件事：觸發條件、失敗時看哪個 artifact 或 log、本機要用哪條命令重現。</p>
<h2 id="本專案維護注意事項">本專案維護注意事項</h2>
<p>本專案維護注意事項的責任是記錄和目前 YAML 直接相關的操作提醒。這些提醒隨 workflow 實作改變而更新，不承擔通用 CI 設計原理。</p>
<ul>
<li><code>Playwright tests</code> 依賴 <code>make site</code> 產生 Pagefind index；搜尋測試失敗時先確認 production build 是否完整。</li>
<li><code>deploy.yml</code> 的 Hugo build 使用 <code>continue-on-error: true</code>，後面用 Claude Debug 與 retry build 接住失敗。</li>
<li><code>Claude Code</code> 目前是 read-oriented 互動入口；若未來要寫入 repo，需要重新審核 permission。</li>
<li><code>.github/workflows/*.yml</code> 有實作變更時，要同步更新本頁，讓維護入口維持可信。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>CI 紅燈處理流程：讀 <a href="../../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>CI gate 設計原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>CI 在可靠性模組的位置：讀 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>。</li>
<li>發布 gate 設計：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
<li>Markdown 檢查規則：讀 <a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a>。</li>
</ul>
]]></content:encoded></item><item><title>GitHub Actions：Environment Protection 與 OIDC Cloud Auth</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/environment-protection-and-oidc-cloud-auth/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/environment-protection-and-oidc-cloud-auth/</guid><description>&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CI pipeline 的可靠性驗證在測試階段結束後，還需要兩道控制面才算完整。第一道是 deploy approval gate — 決定誰可以核准 production deploy、在什麼條件下放行。第二道是 credential 安全 — deploy 需要 cloud credential，但 long-lived secret 存在 CI 環境中會擴大洩漏面。&lt;/p>
&lt;p>GitHub Actions 用 environment protection rules 處理第一道，用 OIDC federation 處理第二道。兩者搭配讓 deploy 流程同時滿足 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a> 的放行控制與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的 credential 最小暴露原則。&lt;/p>
&lt;h2 id="environment-protection-rules">Environment Protection Rules&lt;/h2>
&lt;p>Environment 是 GitHub Actions 的 deploy 分層單位。每個 environment（staging / canary / production）可以獨立設定 protection rules，讓不同風險等級的 deploy 走不同的放行流程。&lt;/p>
&lt;h3 id="protection-rule-類型">Protection rule 類型&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>規則&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>典型設定&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Required reviewers&lt;/td>
 &lt;td>指定人員核准後才能 deploy&lt;/td>
 &lt;td>production 需 2 人核准&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Wait timer&lt;/td>
 &lt;td>deploy 前強制等待，讓最後一刻能攔住&lt;/td>
 &lt;td>production 等 15 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deployment branch policy&lt;/td>
 &lt;td>只允許特定 branch deploy 到該 environment&lt;/td>
 &lt;td>production 只接受 main / release/*&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Required reviewers 是 deploy 層的 release gate。當 workflow job 標記 &lt;code>environment: production&lt;/code>，GitHub 會暫停 job 直到指定 reviewer 核准。reviewer 的選擇應對齊服務 ownership — 由該服務的 on-call lead 或 tech lead 核准，避免核准權過於集中或分散。&lt;/p>
&lt;p>Wait timer 提供一個緩衝窗口。deploy 前等待 N 分鐘讓團隊有時間檢查 staging 結果、確認沒有進行中的事故、或在發現問題時取消 deploy。timer 長度跟服務風險等級對齊 — 低風險服務可以 0 分鐘，交易路徑可以 15-30 分鐘。&lt;/p>
&lt;p>Deployment branch policy 限制哪些 branch 可以觸發特定 environment 的 deploy。這防止 feature branch 意外 deploy 到 production。production 通常只接受 main 或 release branch。&lt;/p>
&lt;h3 id="分層建議">分層建議&lt;/h3>
&lt;p>staging 用自動 deploy — push 到 staging branch 直接觸發 workflow，無需 approval，回饋速度最大化。production 用 required reviewer + wait timer — 確保每次 production deploy 都經過人工確認與緩衝。canary 介於兩者之間 — 可以自動 deploy 但加 wait timer，讓觀測指標有時間反映。&lt;/p>
&lt;h2 id="oidc-cloud-auth">OIDC Cloud Auth&lt;/h2>
&lt;h3 id="long-lived-credential-的風險">Long-lived credential 的風險&lt;/h3>
&lt;p>CI deploy 需要 cloud credential（AWS access key / GCP service account key / Azure service principal）。傳統做法是把這些 credential 存在 GitHub repository secret 或 environment secret 中。long-lived credential 的風險在於：洩漏後攻擊者可以長期使用、rotation 需要手動更新 CI 設定、credential scope 常設得比實際需求更大。&lt;/p></description><content:encoded><![CDATA[<h2 id="問題情境">問題情境</h2>
<p>CI pipeline 的可靠性驗證在測試階段結束後，還需要兩道控制面才算完整。第一道是 deploy approval gate — 決定誰可以核准 production deploy、在什麼條件下放行。第二道是 credential 安全 — deploy 需要 cloud credential，但 long-lived secret 存在 CI 環境中會擴大洩漏面。</p>
<p>GitHub Actions 用 environment protection rules 處理第一道，用 OIDC federation 處理第二道。兩者搭配讓 deploy 流程同時滿足 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a> 的放行控制與 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的 credential 最小暴露原則。</p>
<h2 id="environment-protection-rules">Environment Protection Rules</h2>
<p>Environment 是 GitHub Actions 的 deploy 分層單位。每個 environment（staging / canary / production）可以獨立設定 protection rules，讓不同風險等級的 deploy 走不同的放行流程。</p>
<h3 id="protection-rule-類型">Protection rule 類型</h3>
<table>
  <thead>
      <tr>
          <th>規則</th>
          <th>責任</th>
          <th>典型設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Required reviewers</td>
          <td>指定人員核准後才能 deploy</td>
          <td>production 需 2 人核准</td>
      </tr>
      <tr>
          <td>Wait timer</td>
          <td>deploy 前強制等待，讓最後一刻能攔住</td>
          <td>production 等 15 分鐘</td>
      </tr>
      <tr>
          <td>Deployment branch policy</td>
          <td>只允許特定 branch deploy 到該 environment</td>
          <td>production 只接受 main / release/*</td>
      </tr>
  </tbody>
</table>
<p>Required reviewers 是 deploy 層的 release gate。當 workflow job 標記 <code>environment: production</code>，GitHub 會暫停 job 直到指定 reviewer 核准。reviewer 的選擇應對齊服務 ownership — 由該服務的 on-call lead 或 tech lead 核准，避免核准權過於集中或分散。</p>
<p>Wait timer 提供一個緩衝窗口。deploy 前等待 N 分鐘讓團隊有時間檢查 staging 結果、確認沒有進行中的事故、或在發現問題時取消 deploy。timer 長度跟服務風險等級對齊 — 低風險服務可以 0 分鐘，交易路徑可以 15-30 分鐘。</p>
<p>Deployment branch policy 限制哪些 branch 可以觸發特定 environment 的 deploy。這防止 feature branch 意外 deploy 到 production。production 通常只接受 main 或 release branch。</p>
<h3 id="分層建議">分層建議</h3>
<p>staging 用自動 deploy — push 到 staging branch 直接觸發 workflow，無需 approval，回饋速度最大化。production 用 required reviewer + wait timer — 確保每次 production deploy 都經過人工確認與緩衝。canary 介於兩者之間 — 可以自動 deploy 但加 wait timer，讓觀測指標有時間反映。</p>
<h2 id="oidc-cloud-auth">OIDC Cloud Auth</h2>
<h3 id="long-lived-credential-的風險">Long-lived credential 的風險</h3>
<p>CI deploy 需要 cloud credential（AWS access key / GCP service account key / Azure service principal）。傳統做法是把這些 credential 存在 GitHub repository secret 或 environment secret 中。long-lived credential 的風險在於：洩漏後攻擊者可以長期使用、rotation 需要手動更新 CI 設定、credential scope 常設得比實際需求更大。</p>
<h3 id="oidc-federation-的運作方式">OIDC federation 的運作方式</h3>
<p>GitHub Actions 支援作為 OIDC identity provider。workflow 在執行時可以向 GitHub 請求一個 short-lived OIDC token，cloud provider 信任這個 token 後發出 short-lived cloud credential。整個流程不需要在 CI 環境中存放任何 long-lived secret。</p>
<p>流程：workflow 啟動 → 向 GitHub OIDC provider 請求 token → token 帶有 repo / branch / environment 等 claim → cloud provider 的 trust policy 驗證 claim → 發出 short-lived credential（通常 1 小時有效期）。</p>
<h3 id="cloud-provider-配置">Cloud provider 配置</h3>
<p><strong>AWS</strong>：在 IAM 設定 OIDC identity provider（issuer: <code>token.actions.githubusercontent.com</code>）、建立 IAM role 並設定 trust policy 限制 repo + branch + environment。workflow 中用 <code>aws-actions/configure-aws-credentials</code> action 取得 session credential。</p>
<p><strong>GCP</strong>：設定 Workload Identity Federation pool + provider、建立 service account 並綁定 pool。workflow 中用 <code>google-github-actions/auth</code> action 取得 short-lived token。</p>
<p><strong>Azure</strong>：在 Azure AD 設定 federated credential 給 app registration、限制 repo + branch + environment。workflow 中用 <code>azure/login</code> action。</p>
<h3 id="trust-policy-的安全邊界">Trust policy 的安全邊界</h3>
<p>OIDC trust policy 必須限制到特定 repo、branch 與 environment。trust policy 寫成 wildcard（信任整個 GitHub org 的所有 repo）等於讓 org 內任何 repo 的 workflow 都能取得 cloud credential。最小權限原則：production environment 的 trust policy 只信任 <code>repo:org/service:environment:production</code>，不信任其他 environment 或 branch。</p>
<h2 id="實作範例">實作範例</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># .github/workflows/deploy.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">deploy-staging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/staging-deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">deploy-production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">deploy-staging</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/production-deploy</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh production</span></span></span></code></pre></div><p>staging job 自動觸發。production job 等 staging 完成後暫停，等待 environment protection rules 中設定的 reviewer 核准。兩個 job 各自用不同的 IAM role，scope 分離。</p>
<p>Environment secret 與 repository secret 的差異：environment secret 只在該 environment 的 job 中可用。把 production-only 的設定（如 database connection string）存在 production environment secret 而非 repository secret，避免 staging workflow 意外存取 production 資源。</p>
<h2 id="邊界與陷阱">邊界與陷阱</h2>
<p>Environment protection rules 在 private repo 上需要 GitHub Team 或 Enterprise 方案。Free 方案的 private repo 無法使用 required reviewers 與 wait timer，只有 public repo 或付費方案可用。</p>
<p>OIDC trust policy 的常見錯誤是 subject claim 設定太寬。<code>sub</code> claim 的格式是 <code>repo:{owner}/{repo}:environment:{name}</code>（使用 environment 時）或 <code>repo:{owner}/{repo}:ref:refs/heads/{branch}</code>（不使用 environment 時）。用 wildcard match 或省略 environment 限制會讓非預期的 workflow 取得 credential。</p>
<p>Wait timer 設定要跟服務風險等級對齊。所有服務統一用 30 分鐘 wait timer 會拖慢低風險服務的 deploy velocity。對齊方式：低風險服務 0 分鐘、中風險 5-10 分鐘、高風險（交易路徑）15-30 分鐘。</p>
<p>Required reviewer 數量跟團隊大小對齊。只有 1 個 reviewer 等於沒有四眼原則；需要 5 個 reviewer 會造成 approval 排隊。2-3 個 reviewer 是多數團隊的平衡點。</p>
<h2 id="整合路由">整合路由</h2>
<ul>
<li>上游：<a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>（CI gate 通過後才進入 deploy 階段）</li>
<li>下游：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（environment protection 是 deploy 層的 release gate）</li>
<li>下游：<a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>（deploy 結果作為 release evidence）</li>
<li>平行：<a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a> contexts + approval jobs（同類功能的不同實作）</li>
<li>案例回寫：<a href="/blog/backend/06-reliability/cases/microsoft/change-management-and-reliability-governance/" data-link-title="Microsoft：變更治理與可靠性門檻" data-link-desc="透過分層變更管理與發布閘門，降低大型 SaaS 平台的系統性回歸風險。">Microsoft 變更分層</a>（變更風險分層對應 environment 分層）、<a href="/blog/backend/06-reliability/cases/google/error-budget-policy-and-release-gating/" data-link-title="Google：Error Budget 政策如何決定發布節奏" data-link-desc="把 SLO 消耗量轉成 release gate，讓可靠性與交付速度共用同一套決策語言。">Google Error Budget</a>（error budget 消耗時提高 gate 門檻 → 可動態調整 required reviewer 數量）</li>
</ul>
]]></content:encoded></item><item><title>CI gate 與 workflow 邊界</title><link>https://tarrragon.github.io/blog/ci/ci-gate-workflow-boundary/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/ci-gate-workflow-boundary/</guid><description>&lt;p>CI gate 的核心責任是把「是否進入下一階段」變成明確條件。測試、建置、發布與人工審核可以分成不同 workflow 或 job，但只要它們共同決定同一次發布，就需要有清楚的 gate 關係。&lt;/p>
&lt;h2 id="gate-形式">Gate 形式&lt;/h2>
&lt;p>Gate 形式要依控制範圍選擇。PR 合併、job 執行順序、production 發布與 artifact 傳遞是四種不同責任，混在一起會讓紅燈的意義變模糊。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Gate 形式&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required checks&lt;/a>&lt;/td>
 &lt;td>阻止未通過測試的 commit 合併&lt;/td>
 &lt;td>PR 或 branch protection 顯示必須通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Job &lt;code>needs&lt;/code>&lt;/td>
 &lt;td>讓 deploy 等 test / build&lt;/td>
 &lt;td>同一 workflow 內 deploy 依賴前置 job&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment protection&lt;/a>&lt;/td>
 &lt;td>控制 production / target environment 發布&lt;/td>
 &lt;td>部署環境需要審核或 required reviewers&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff&lt;/a>&lt;/td>
 &lt;td>確保測試與發布使用同一份產物&lt;/td>
 &lt;td>test job 產生 artifact，deploy job 使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required checks&lt;/a> 適合保護主線。它讓測試結果成為合併條件，避免紅燈變更進入 &lt;code>main&lt;/code> 或 release branch（backend 延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI Pipeline&lt;/a>）。&lt;/p>
&lt;p>Job &lt;code>needs&lt;/code> 適合同一條 workflow 內的發布管線。它讓 &lt;code>deploy&lt;/code> 必須等 &lt;code>test&lt;/code>、&lt;code>build&lt;/code> 或 &lt;code>package&lt;/code> 成功後才執行，避免 deploy job 先於驗證結果流動（platform 延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract&lt;/a>）。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment protection&lt;/a> 適合正式環境。即使 build 與測試通過，production 或其他目標環境仍可要求人工審核、特定分支或特定 reviewer 才能部署（治理延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate&lt;/a>）。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff&lt;/a> 適合避免「測試一份、發布另一份」的漂移。較嚴謹的流程會讓 build job 產生 artifact，test job 驗證這份 artifact，deploy job 發布同一份 artifact（供應鏈延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance&lt;/a>）。&lt;/p>
&lt;h2 id="workflow-邊界">Workflow 邊界&lt;/h2>
&lt;p>Workflow 邊界的責任是決定哪些步驟共享同一條執行圖。放在同一條 workflow 裡的 job 可以用 &lt;code>needs&lt;/code> 建立顯式依賴；分散在不同 workflow 裡的流程，通常要靠 branch protection 或 environment protection 建立跨 workflow gate。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>結構&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>常見風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>單一 workflow 多 job&lt;/td>
 &lt;td>test / build / deploy 緊密相依&lt;/td>
 &lt;td>YAML 變長，但依賴關係清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多 workflow&lt;/td>
 &lt;td>不同觸發條件或責任完全不同&lt;/td>
 &lt;td>跨 workflow gate 要靠 repo 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PR workflow + deploy&lt;/td>
 &lt;td>PR 驗證、main 發布分離&lt;/td>
 &lt;td>main push 若缺 required checks 會漏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Artifact pipeline&lt;/td>
 &lt;td>同一份產物要被測試再發布&lt;/td>
 &lt;td>artifact 版本與權限要治理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多 workflow 的關鍵風險是順序假設。GitHub Actions 的 workflow 彼此獨立；跨 workflow 順序需要靠 repository 設定或 API 顯式串接。&lt;/p></description><content:encoded><![CDATA[<p>CI gate 的核心責任是把「是否進入下一階段」變成明確條件。測試、建置、發布與人工審核可以分成不同 workflow 或 job，但只要它們共同決定同一次發布，就需要有清楚的 gate 關係。</p>
<h2 id="gate-形式">Gate 形式</h2>
<p>Gate 形式要依控制範圍選擇。PR 合併、job 執行順序、production 發布與 artifact 傳遞是四種不同責任，混在一起會讓紅燈的意義變模糊。</p>
<table>
  <thead>
      <tr>
          <th>Gate 形式</th>
          <th>責任</th>
          <th>判讀方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required checks</a></td>
          <td>阻止未通過測試的 commit 合併</td>
          <td>PR 或 branch protection 顯示必須通過</td>
      </tr>
      <tr>
          <td>Job <code>needs</code></td>
          <td>讓 deploy 等 test / build</td>
          <td>同一 workflow 內 deploy 依賴前置 job</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment protection</a></td>
          <td>控制 production / target environment 發布</td>
          <td>部署環境需要審核或 required reviewers</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff</a></td>
          <td>確保測試與發布使用同一份產物</td>
          <td>test job 產生 artifact，deploy job 使用</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required checks</a> 適合保護主線。它讓測試結果成為合併條件，避免紅燈變更進入 <code>main</code> 或 release branch（backend 延伸見 <a href="/blog/backend/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合流程如何在合併前驗證品質與相容性">CI Pipeline</a>）。</p>
<p>Job <code>needs</code> 適合同一條 workflow 內的發布管線。它讓 <code>deploy</code> 必須等 <code>test</code>、<code>build</code> 或 <code>package</code> 成功後才執行，避免 deploy job 先於驗證結果流動（platform 延伸見 <a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract</a>）。</p>
<p><a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment protection</a> 適合正式環境。即使 build 與測試通過，production 或其他目標環境仍可要求人工審核、特定分支或特定 reviewer 才能部署（治理延伸見 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a>）。</p>
<p><a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff</a> 適合避免「測試一份、發布另一份」的漂移。較嚴謹的流程會讓 build job 產生 artifact，test job 驗證這份 artifact，deploy job 發布同一份 artifact（供應鏈延伸見 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance</a>）。</p>
<h2 id="workflow-邊界">Workflow 邊界</h2>
<p>Workflow 邊界的責任是決定哪些步驟共享同一條執行圖。放在同一條 workflow 裡的 job 可以用 <code>needs</code> 建立顯式依賴；分散在不同 workflow 裡的流程，通常要靠 branch protection 或 environment protection 建立跨 workflow gate。</p>
<table>
  <thead>
      <tr>
          <th>結構</th>
          <th>適合情境</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 workflow 多 job</td>
          <td>test / build / deploy 緊密相依</td>
          <td>YAML 變長，但依賴關係清楚</td>
      </tr>
      <tr>
          <td>多 workflow</td>
          <td>不同觸發條件或責任完全不同</td>
          <td>跨 workflow gate 要靠 repo 設定</td>
      </tr>
      <tr>
          <td>PR workflow + deploy</td>
          <td>PR 驗證、main 發布分離</td>
          <td>main push 若缺 required checks 會漏</td>
      </tr>
      <tr>
          <td>Artifact pipeline</td>
          <td>同一份產物要被測試再發布</td>
          <td>artifact 版本與權限要治理</td>
      </tr>
  </tbody>
</table>
<p>多 workflow 的關鍵風險是順序假設。GitHub Actions 的 workflow 彼此獨立；跨 workflow 順序需要靠 repository 設定或 API 顯式串接。</p>
<h2 id="發布阻擋判讀">發布阻擋判讀</h2>
<p>發布阻擋要同時看 YAML 與 GitHub repository 設定。YAML 說明 workflow 或 job 如何執行；跨 workflow 的「測試通過才發布」通常要靠 <a href="/blog/ci/knowledge-cards/branch-protection/" data-link-title="Branch Protection" data-link-desc="說明主線分支如何以規則保護合併與發布前置條件">Branch Protection</a>、required status checks 或 environment protection。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>只看 YAML 能判斷嗎</th>
          <th>應檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>deploy 是否等 build</td>
          <td>可以</td>
          <td>同 workflow 的 <code>needs</code></td>
      </tr>
      <tr>
          <td>deploy 是否等另一條 test workflow</td>
          <td>通常要查設定</td>
          <td><a href="/blog/ci/knowledge-cards/branch-protection/" data-link-title="Branch Protection" data-link-desc="說明主線分支如何以規則保護合併與發布前置條件">Branch Protection</a> / <a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks</a></td>
      </tr>
      <tr>
          <td>PR 是否必須通過測試才能合併</td>
          <td>需要查 repo 設定</td>
          <td><a href="/blog/ci/knowledge-cards/branch-protection/" data-link-title="Branch Protection" data-link-desc="說明主線分支如何以規則保護合併與發布前置條件">Branch Protection</a></td>
      </tr>
      <tr>
          <td>目標環境是否需要人工審核</td>
          <td>需要查環境設定</td>
          <td>Environment protection</td>
      </tr>
      <tr>
          <td>測試與發布是否同一份 artifact</td>
          <td>可以部分判斷</td>
          <td>workflow artifact upload / download</td>
      </tr>
  </tbody>
</table>
<p>這個判讀順序能避免錯修。若測試紅燈但目標環境仍發布，問題通常在 deploy gate 尚未把測試狀態納入發布條件。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是讓 CI 綠燈與發布安全之間失去因果關係。CI 的目標是讓綠燈代表「這次變更在定義好的條件下可進下一階段」。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>deploy workflow 不等 test</td>
          <td>測試紅燈仍可能發布</td>
          <td>用 required checks 或 <code>needs</code></td>
      </tr>
      <tr>
          <td>CI 與本機命令不同</td>
          <td>本機通過但 CI 失敗</td>
          <td>把命令收斂到 Makefile / npm script</td>
      </tr>
      <tr>
          <td>測試與發布各自 build</td>
          <td>測試產物與發布產物漂移</td>
          <td>用 artifact handoff</td>
      </tr>
      <tr>
          <td>看到紅燈直接重跑</td>
          <td>掩蓋 flaky 或環境問題</td>
          <td>先看失敗 log，再決定是否重跑</td>
      </tr>
      <tr>
          <td>用 <code>--no-verify</code> 或跳過 CI</td>
          <td>把局部問題帶進主線</td>
          <td>修掉 gate 或明確記錄例外</td>
      </tr>
  </tbody>
</table>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提示什麼時候 workflow 結構需要重切，讓團隊從局部 patch 回到 gate 設計。</p>
<ul>
<li>測試紅燈仍發布：把 deploy gate 顯式化，使用 required checks 或同 workflow <code>needs</code>。</li>
<li>本機常常重現不出 CI：把命令收斂到 <code>Makefile</code> 或 <code>npm scripts</code>，減少 workflow 專屬命令。</li>
<li>測試常因 artifact 缺失失敗：建立 artifact handoff，讓測試與發布使用同一份產物。</li>
<li>workflow 說明與實作分叉：同步更新 workflow 文件與 YAML，讓維護入口保持可信。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>CI 紅燈處理流程：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>靜態站部署案例：讀 <a href="../blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>可靠性層的 release gate：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
</ul>
]]></content:encoded></item><item><title>Required Checks</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/</guid><description>&lt;p>Required Checks 的核心概念是「把合併條件綁定到檢查結果」。它讓主線保護不依賴人工記憶，而依賴可觀測狀態。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Required Checks 位在 repository branch protection，連接 pull request 與 CI workflow 結果。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>PR 是否可合併取決於特定 checks 狀態。&lt;/li>
&lt;li>團隊需要確保高風險變更不繞過驗證。&lt;/li>
&lt;li>CI workflow 增刪後需要同步調整合併條件。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>專案可要求 &lt;code>md-check&lt;/code> 與 &lt;code>Playwright tests&lt;/code> 都通過才能合併 &lt;code>main&lt;/code>。若只跑 workflow 但未設為 required，主線仍可能進入紅燈變更。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Required Checks 要定義必要檢查集合、擁有者與變更流程，並和 workflow 命名保持一致。&lt;/p></description><content:encoded><![CDATA[<p>Required Checks 的核心概念是「把合併條件綁定到檢查結果」。它讓主線保護不依賴人工記憶，而依賴可觀測狀態。</p>
<h2 id="概念位置">概念位置</h2>
<p>Required Checks 位在 repository branch protection，連接 pull request 與 CI workflow 結果。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>PR 是否可合併取決於特定 checks 狀態。</li>
<li>團隊需要確保高風險變更不繞過驗證。</li>
<li>CI workflow 增刪後需要同步調整合併條件。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>專案可要求 <code>md-check</code> 與 <code>Playwright tests</code> 都通過才能合併 <code>main</code>。若只跑 workflow 但未設為 required，主線仍可能進入紅燈變更。</p>
<h2 id="設計責任">設計責任</h2>
<p>Required Checks 要定義必要檢查集合、擁有者與變更流程，並和 workflow 命名保持一致。</p>
]]></content:encoded></item><item><title>CI 中的服務 fixture 管理</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</guid><description>&lt;p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。&lt;/p>
&lt;h2 id="三種服務管理方案">三種服務管理方案&lt;/h2>
&lt;h3 id="processstart直接啟動程序">Process.start（直接啟動程序）&lt;/h3>
&lt;p>在 test 的 setUp 中用 &lt;code>Process.start&lt;/code> 啟動服務程序，tearDown 中用 &lt;code>process.kill&lt;/code> 停止。&lt;/p>
&lt;p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&amp;lt; 2 秒），不需要持久化狀態。&lt;/p>
&lt;p>app_tunnel 的 ttyd 就是這個模式。&lt;code>ttyd bash&lt;/code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">setUp: process = Process.start(&amp;#39;ttyd&amp;#39;, [&amp;#39;--port&amp;#39;, &amp;#39;7681&amp;#39;, &amp;#39;bash&amp;#39;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> await waitForPort(7681, timeout: 3s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">tearDown: process.kill()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-compose">Docker Compose&lt;/h3>
&lt;p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 &lt;code>docker compose up&lt;/code>，after_all 階段 &lt;code>docker compose down&lt;/code>。&lt;/p>
&lt;p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。&lt;/p>
&lt;p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。&lt;/p>
&lt;h3 id="testcontainers">Testcontainers&lt;/h3>
&lt;p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。&lt;/p>
&lt;p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。&lt;/p>
&lt;p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。&lt;/p>
&lt;h2 id="健康檢查">健康檢查&lt;/h2>
&lt;p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。&lt;/p>
&lt;p>健康檢查的方式依服務類型而定：&lt;/p>
&lt;p>&lt;strong>TCP port 可達&lt;/strong>：&lt;code>waitForPort(port, timeout)&lt;/code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。&lt;/p>
&lt;p>&lt;strong>HTTP health endpoint&lt;/strong>：對 &lt;code>/health&lt;/code> 或 &lt;code>/ready&lt;/code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。&lt;/p>
&lt;p>&lt;strong>特定操作成功&lt;/strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。&lt;/p>
&lt;h2 id="服務狀態隔離">服務狀態隔離&lt;/h2>
&lt;p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。&lt;/p>
&lt;p>三種隔離策略：&lt;/p>
&lt;p>&lt;strong>每 test 重啟服務&lt;/strong>：最強隔離，最慢。適合服務啟動快（&amp;lt; 1 秒）的場景。&lt;/p>
&lt;p>&lt;strong>每 test 重設狀態&lt;/strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。&lt;/p>
&lt;p>&lt;strong>每 test 用獨立 namespace&lt;/strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。&lt;/p>
&lt;p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。&lt;/p></description><content:encoded><![CDATA[<p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。</p>
<h2 id="三種服務管理方案">三種服務管理方案</h2>
<h3 id="processstart直接啟動程序">Process.start（直接啟動程序）</h3>
<p>在 test 的 setUp 中用 <code>Process.start</code> 啟動服務程序，tearDown 中用 <code>process.kill</code> 停止。</p>
<p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&lt; 2 秒），不需要持久化狀態。</p>
<p>app_tunnel 的 ttyd 就是這個模式。<code>ttyd bash</code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">setUp: process = Process.start(&#39;ttyd&#39;, [&#39;--port&#39;, &#39;7681&#39;, &#39;bash&#39;])
</span></span><span class="line"><span class="ln">2</span><span class="cl">       await waitForPort(7681, timeout: 3s)
</span></span><span class="line"><span class="ln">3</span><span class="cl">tearDown: process.kill()</span></span></code></pre></div><h3 id="docker-compose">Docker Compose</h3>
<p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 <code>docker compose up</code>，after_all 階段 <code>docker compose down</code>。</p>
<p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。</p>
<p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。</p>
<h3 id="testcontainers">Testcontainers</h3>
<p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。</p>
<p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。</p>
<p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。</p>
<h2 id="健康檢查">健康檢查</h2>
<p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。</p>
<p>健康檢查的方式依服務類型而定：</p>
<p><strong>TCP port 可達</strong>：<code>waitForPort(port, timeout)</code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。</p>
<p><strong>HTTP health endpoint</strong>：對 <code>/health</code> 或 <code>/ready</code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。</p>
<p><strong>特定操作成功</strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。</p>
<h2 id="服務狀態隔離">服務狀態隔離</h2>
<p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。</p>
<p>三種隔離策略：</p>
<p><strong>每 test 重啟服務</strong>：最強隔離，最慢。適合服務啟動快（&lt; 1 秒）的場景。</p>
<p><strong>每 test 重設狀態</strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。</p>
<p><strong>每 test 用獨立 namespace</strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。</p>
<p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>什麼時候值得建 protocol integration test 基礎設施 → <a href="/blog/testing/03-protocol-integration-test/cost-judgment/" data-link-title="成本判斷表" data-link-desc="什麼時候值得寫 protocol integration test、什麼時候用 contract test 或實機測試替代 — 根據服務啟動成本和協議複雜度判斷">成本判斷表</a></li>
<li>Protocol integration test 的定義 → <a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">Protocol integration test 定義</a></li>
<li>WebSocket 的 protocol test 實作 → <a href="/blog/testing/03-protocol-integration-test/websocket-protocol-test/" data-link-title="WebSocket 協議測試實作" data-link-desc="對真實 ttyd 驗證 frame type 和 auth handshake — 從 T.C1 和 T.C2 的教訓推導出的 protocol integration test 設計">WebSocket 協議測試實作</a></li>
</ul>
]]></content:encoded></item><item><title>Flaky test 根因分類</title><link>https://tarrragon.github.io/blog/testing/05-test-design-judgment/flaky-test-root-cause/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/05-test-design-judgment/flaky-test-root-cause/</guid><description>&lt;p>Flaky test 是指在程式碼沒有改變的情況下，test 的結果在通過和失敗之間隨機切換。Flaky test 侵蝕團隊對 test suite 的信任 — 如果 test 經常「隨便」失敗，開發者會習慣性地 re-run 而非調查失敗原因，真正的 bug 可能在 re-run 中被忽略。&lt;/p>
&lt;h2 id="四類根因">四類根因&lt;/h2>
&lt;h3 id="計時依賴">計時依賴&lt;/h3>
&lt;p>Test 依賴特定的時間條件 — timeout、delay、animation duration。系統負載不同時，時間條件可能滿足也可能不滿足。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>&lt;code>await Future.delayed(Duration(seconds: 2))&lt;/code> + assertion — 如果操作在 2 秒內完成，test 通過；如果 CI 機器負載高導致操作超過 2 秒，test 失敗&lt;/li>
&lt;li>&lt;code>expect(stopwatch.elapsed, lessThan(Duration(seconds: 1)))&lt;/code> — 效能斷言在不同機器上結果不同&lt;/li>
&lt;/ul>
&lt;p>處理策略：用事件驅動代替 timeout。等待 &lt;code>stream.first&lt;/code> 代替 &lt;code>delay(2s) + check&lt;/code>；用 completion signal 代替固定等待時間。如果必須用 timeout，設定寬裕的上限（10x 預期時間）而非精確的預期值。&lt;/p>
&lt;h3 id="環境差異">環境差異&lt;/h3>
&lt;p>Test 在不同環境下行為不同 — 作業系統、檔案系統、時區、locale、DNS 解析。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>檔案路徑分隔符（&lt;code>/&lt;/code> vs &lt;code>\&lt;/code>）在不同 OS 下不同&lt;/li>
&lt;li>時間格式化結果依時區而定（UTC vs local）&lt;/li>
&lt;li>浮點數比較因 CPU 架構不同有微小差異&lt;/li>
&lt;/ul>
&lt;p>處理策略：用 &lt;code>path.join&lt;/code> 代替硬編碼路徑；時間操作用 UTC；浮點比較用 &lt;code>closeTo&lt;/code> 代替精確比較。在 CI 中固定環境變數（&lt;code>TZ=UTC&lt;/code>、&lt;code>LANG=en_US.UTF-8&lt;/code>）。&lt;/p>
&lt;h3 id="資源競爭">資源競爭&lt;/h3>
&lt;p>Test 依賴共享資源（port、暫存檔、資料庫行）— 平行執行時多個 test 同時存取同一資源，結果依賴執行順序。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>多個 test 監聽同一個 port — 第二個綁定失敗&lt;/li>
&lt;li>多個 test 寫入同一個暫存檔 — 內容被覆蓋&lt;/li>
&lt;li>多個 test 操作同一個資料庫 table — 資料互相干擾&lt;/li>
&lt;/ul>
&lt;p>處理策略：每個 test 使用獨立的資源（隨機 port、唯一檔名、隔離的資料庫 schema）。如果資源無法隔離，sequential 執行相關 test（&lt;code>@sequential&lt;/code> 標註）。&lt;/p>
&lt;h3 id="非確定性輸出">非確定性輸出&lt;/h3>
&lt;p>程式碼的輸出本身不確定 — &lt;code>Set&lt;/code> 的迭代順序、&lt;code>Map&lt;/code> 的 key 順序、非同步操作的完成順序。&lt;/p>
&lt;p>常見模式：&lt;/p>
&lt;ul>
&lt;li>斷言 &lt;code>Set&lt;/code> 的 &lt;code>toString()&lt;/code> 結果等於特定字串 — &lt;code>Set&lt;/code> 的迭代順序不保證&lt;/li>
&lt;li>斷言 &lt;code>Future.wait([a, b]).then((results) =&amp;gt; results[0])&lt;/code> — &lt;code>a&lt;/code> 和 &lt;code>b&lt;/code> 的完成順序不固定&lt;/li>
&lt;li>斷言 JSON 序列化的 key 順序 — &lt;code>Map&lt;/code> 的 key 順序在不同實作中不同&lt;/li>
&lt;/ul>
&lt;p>處理策略：不斷言順序（用 &lt;code>containsAll&lt;/code> 代替 &lt;code>equals&lt;/code> 比較集合）；不斷言序列化格式（反序列化後比較值）；用 &lt;code>completion&lt;/code> matcher 代替順序假設。&lt;/p>
&lt;h2 id="診斷步驟">診斷步驟&lt;/h2>
&lt;p>發現疑似 flaky test 時的診斷步驟：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>確認 flaky&lt;/strong>：在乾淨環境連續跑 20 次，確認失敗是隨機的（如果每次都失敗，是 bug 不是 flaky）&lt;/li>
&lt;li>&lt;strong>收集失敗訊息&lt;/strong>：記錄每次失敗的 assertion 訊息、stack trace、環境資訊（OS 版本、CI 機器 ID）&lt;/li>
&lt;li>&lt;strong>分類&lt;/strong>：失敗訊息指向時間（timeout）→ 計時依賴；指向值不同 → 非確定性或環境差異；指向連接失敗 → 資源競爭&lt;/li>
&lt;li>&lt;strong>修復&lt;/strong>：根據分類使用對應的處理策略&lt;/li>
&lt;/ol>
&lt;p>分類和修復之外，flaky test 的根因有時來自 assertion 本身的設計 — &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問&lt;/a>提供判斷 assertion 是否有效的框架。如果 flaky 的根因是 mock 和真實服務的行為差異，回到 &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表&lt;/a>判斷 mock 是否還適用。Protocol integration test 在 CI 中的&lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">服務 fixture 管理&lt;/a>也是 flaky 的常見來源 — 服務啟動不完全就開始跑 test。&lt;/p></description><content:encoded><![CDATA[<p>Flaky test 是指在程式碼沒有改變的情況下，test 的結果在通過和失敗之間隨機切換。Flaky test 侵蝕團隊對 test suite 的信任 — 如果 test 經常「隨便」失敗，開發者會習慣性地 re-run 而非調查失敗原因，真正的 bug 可能在 re-run 中被忽略。</p>
<h2 id="四類根因">四類根因</h2>
<h3 id="計時依賴">計時依賴</h3>
<p>Test 依賴特定的時間條件 — timeout、delay、animation duration。系統負載不同時，時間條件可能滿足也可能不滿足。</p>
<p>常見模式：</p>
<ul>
<li><code>await Future.delayed(Duration(seconds: 2))</code> + assertion — 如果操作在 2 秒內完成，test 通過；如果 CI 機器負載高導致操作超過 2 秒，test 失敗</li>
<li><code>expect(stopwatch.elapsed, lessThan(Duration(seconds: 1)))</code> — 效能斷言在不同機器上結果不同</li>
</ul>
<p>處理策略：用事件驅動代替 timeout。等待 <code>stream.first</code> 代替 <code>delay(2s) + check</code>；用 completion signal 代替固定等待時間。如果必須用 timeout，設定寬裕的上限（10x 預期時間）而非精確的預期值。</p>
<h3 id="環境差異">環境差異</h3>
<p>Test 在不同環境下行為不同 — 作業系統、檔案系統、時區、locale、DNS 解析。</p>
<p>常見模式：</p>
<ul>
<li>檔案路徑分隔符（<code>/</code> vs <code>\</code>）在不同 OS 下不同</li>
<li>時間格式化結果依時區而定（UTC vs local）</li>
<li>浮點數比較因 CPU 架構不同有微小差異</li>
</ul>
<p>處理策略：用 <code>path.join</code> 代替硬編碼路徑；時間操作用 UTC；浮點比較用 <code>closeTo</code> 代替精確比較。在 CI 中固定環境變數（<code>TZ=UTC</code>、<code>LANG=en_US.UTF-8</code>）。</p>
<h3 id="資源競爭">資源競爭</h3>
<p>Test 依賴共享資源（port、暫存檔、資料庫行）— 平行執行時多個 test 同時存取同一資源，結果依賴執行順序。</p>
<p>常見模式：</p>
<ul>
<li>多個 test 監聽同一個 port — 第二個綁定失敗</li>
<li>多個 test 寫入同一個暫存檔 — 內容被覆蓋</li>
<li>多個 test 操作同一個資料庫 table — 資料互相干擾</li>
</ul>
<p>處理策略：每個 test 使用獨立的資源（隨機 port、唯一檔名、隔離的資料庫 schema）。如果資源無法隔離，sequential 執行相關 test（<code>@sequential</code> 標註）。</p>
<h3 id="非確定性輸出">非確定性輸出</h3>
<p>程式碼的輸出本身不確定 — <code>Set</code> 的迭代順序、<code>Map</code> 的 key 順序、非同步操作的完成順序。</p>
<p>常見模式：</p>
<ul>
<li>斷言 <code>Set</code> 的 <code>toString()</code> 結果等於特定字串 — <code>Set</code> 的迭代順序不保證</li>
<li>斷言 <code>Future.wait([a, b]).then((results) =&gt; results[0])</code> — <code>a</code> 和 <code>b</code> 的完成順序不固定</li>
<li>斷言 JSON 序列化的 key 順序 — <code>Map</code> 的 key 順序在不同實作中不同</li>
</ul>
<p>處理策略：不斷言順序（用 <code>containsAll</code> 代替 <code>equals</code> 比較集合）；不斷言序列化格式（反序列化後比較值）；用 <code>completion</code> matcher 代替順序假設。</p>
<h2 id="診斷步驟">診斷步驟</h2>
<p>發現疑似 flaky test 時的診斷步驟：</p>
<ol>
<li><strong>確認 flaky</strong>：在乾淨環境連續跑 20 次，確認失敗是隨機的（如果每次都失敗，是 bug 不是 flaky）</li>
<li><strong>收集失敗訊息</strong>：記錄每次失敗的 assertion 訊息、stack trace、環境資訊（OS 版本、CI 機器 ID）</li>
<li><strong>分類</strong>：失敗訊息指向時間（timeout）→ 計時依賴；指向值不同 → 非確定性或環境差異；指向連接失敗 → 資源競爭</li>
<li><strong>修復</strong>：根據分類使用對應的處理策略</li>
</ol>
<p>分類和修復之外，flaky test 的根因有時來自 assertion 本身的設計 — <a href="/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問</a>提供判斷 assertion 是否有效的框架。如果 flaky 的根因是 mock 和真實服務的行為差異，回到 <a href="/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表</a>判斷 mock 是否還適用。Protocol integration test 在 CI 中的<a href="/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">服務 fixture 管理</a>也是 flaky 的常見來源 — 服務啟動不完全就開始跑 test。</p>
]]></content:encoded></item><item><title>Artifact</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/</guid><description>&lt;p>Artifact 的核心概念是「可被追溯的交付產物」。它是 build 的輸出單位，也是 test 與 deploy 的共同依據。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Artifact 位在 build、test、package、deploy 之間，常見形式包含靜態網站檔案、container image、app bundle、安裝包與報告檔案。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>測試與部署的輸入來源需要一致。&lt;/li>
&lt;li>發布事故需要從線上版本反查 build run。&lt;/li>
&lt;li>團隊需要管理產物保留時間與完整性驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>前端靜態站會把 &lt;code>public/&lt;/code> 作為 artifact，上傳後再部署。後端則用 image digest 作為 artifact 識別，推進到不同環境。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Artifact 要定義命名、版本追溯、保留策略與完整性檢查，讓發布結果可重播、可比對、可審計。&lt;/p></description><content:encoded><![CDATA[<p>Artifact 的核心概念是「可被追溯的交付產物」。它是 build 的輸出單位，也是 test 與 deploy 的共同依據。</p>
<h2 id="概念位置">概念位置</h2>
<p>Artifact 位在 build、test、package、deploy 之間，常見形式包含靜態網站檔案、container image、app bundle、安裝包與報告檔案。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>測試與部署的輸入來源需要一致。</li>
<li>發布事故需要從線上版本反查 build run。</li>
<li>團隊需要管理產物保留時間與完整性驗證。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>前端靜態站會把 <code>public/</code> 作為 artifact，上傳後再部署。後端則用 image digest 作為 artifact 識別，推進到不同環境。</p>
<h2 id="設計責任">設計責任</h2>
<p>Artifact 要定義命名、版本追溯、保留策略與完整性檢查，讓發布結果可重播、可比對、可審計。</p>
]]></content:encoded></item><item><title>Artifact Handoff</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/</guid><description>&lt;p>Artifact Handoff 的核心概念是「測試與發布共用同一份產物」。它把可重現性從口頭約定變成流程保證。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Artifact Handoff 位在 build、test、deploy 之間，透過 upload / download artifact 串接驗證與發布。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>測試通過但部署後行為與測試結果不一致。&lt;/li>
&lt;li>多環境重新 build 造成版本漂移。&lt;/li>
&lt;li>事故追查時缺少從部署版本反查 build run 的路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>CI build 產生靜態網站 artifact，browser test 驗證該 artifact，deploy job 再發布同一份產物。容器場域則可把 image digest 當成 handoff 物件。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Artifact Handoff 要定義產物格式、保留策略、完整性驗證與追溯欄位，讓測試結果可直接映射到發布結果。&lt;/p></description><content:encoded><![CDATA[<p>Artifact Handoff 的核心概念是「測試與發布共用同一份產物」。它把可重現性從口頭約定變成流程保證。</p>
<h2 id="概念位置">概念位置</h2>
<p>Artifact Handoff 位在 build、test、deploy 之間，透過 upload / download artifact 串接驗證與發布。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>測試通過但部署後行為與測試結果不一致。</li>
<li>多環境重新 build 造成版本漂移。</li>
<li>事故追查時缺少從部署版本反查 build run 的路徑。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>CI build 產生靜態網站 artifact，browser test 驗證該 artifact，deploy job 再發布同一份產物。容器場域則可把 image digest 當成 handoff 物件。</p>
<h2 id="設計責任">設計責任</h2>
<p>Artifact Handoff 要定義產物格式、保留策略、完整性驗證與追溯欄位，讓測試結果可直接映射到發布結果。</p>
]]></content:encoded></item><item><title>Preview Environment</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/</guid><description>&lt;p>Preview Environment 的核心概念是「在合併前提供接近正式環境的可驗證入口」。它把 code review 從靜態 diff 延伸到真實互動行為。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Preview Environment 位在 pull request workflow 與正式部署流程之間，常由臨時 URL、隔離資源與到期清理組成。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>團隊需要在合併前驗證 UI、路由或互動行為。&lt;/li>
&lt;li>單靠測試報告不足以判斷體驗差異。&lt;/li>
&lt;li>變更常包含環境變數、CDN 設定或靜態資產路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>前端 PR 自動建 preview URL 給 reviewer 驗證。後端則可能建立 review app 供 API 與整合測試使用。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Preview Environment 要定義建立條件、資源上限、可見範圍與清理策略，避免成本與風險失控。&lt;/p></description><content:encoded><![CDATA[<p>Preview Environment 的核心概念是「在合併前提供接近正式環境的可驗證入口」。它把 code review 從靜態 diff 延伸到真實互動行為。</p>
<h2 id="概念位置">概念位置</h2>
<p>Preview Environment 位在 pull request workflow 與正式部署流程之間，常由臨時 URL、隔離資源與到期清理組成。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>團隊需要在合併前驗證 UI、路由或互動行為。</li>
<li>單靠測試報告不足以判斷體驗差異。</li>
<li>變更常包含環境變數、CDN 設定或靜態資產路徑。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>前端 PR 自動建 preview URL 給 reviewer 驗證。後端則可能建立 review app 供 API 與整合測試使用。</p>
<h2 id="設計責任">設計責任</h2>
<p>Preview Environment 要定義建立條件、資源上限、可見範圍與清理策略，避免成本與風險失控。</p>
]]></content:encoded></item><item><title>Migration</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/migration/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/migration/</guid><description>&lt;p>Migration 的核心概念是「把舊狀態受控推進到新狀態」。它不只涉及資料庫 schema，也包含資料回填、相容窗口與發布順序。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Migration 位在 build 之後、deploy 與 rollout 之前後的關鍵路徑，常與 release gate、rollback strategy 一起設計。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>新舊版本需要共存一段時間。&lt;/li>
&lt;li>發布步驟包含 schema 或資料形狀變更。&lt;/li>
&lt;li>部署失敗時要判斷是否可回退或需要 forward fix。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>後端服務先擴充 schema，再讓新版本寫入新欄位，最後收斂舊欄位讀取；整個過程需要 migration gate 與回退方案。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Migration 要定義相容策略、執行順序、觀測指標與異常回復路由，避免部署成功但資料邏輯失效。&lt;/p></description><content:encoded><![CDATA[<p>Migration 的核心概念是「把舊狀態受控推進到新狀態」。它不只涉及資料庫 schema，也包含資料回填、相容窗口與發布順序。</p>
<h2 id="概念位置">概念位置</h2>
<p>Migration 位在 build 之後、deploy 與 rollout 之前後的關鍵路徑，常與 release gate、rollback strategy 一起設計。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>新舊版本需要共存一段時間。</li>
<li>發布步驟包含 schema 或資料形狀變更。</li>
<li>部署失敗時要判斷是否可回退或需要 forward fix。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>後端服務先擴充 schema，再讓新版本寫入新欄位，最後收斂舊欄位讀取；整個過程需要 migration gate 與回退方案。</p>
<h2 id="設計責任">設計責任</h2>
<p>Migration 要定義相容策略、執行順序、觀測指標與異常回復路由，避免部署成功但資料邏輯失效。</p>
]]></content:encoded></item><item><title>前端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/frontend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/frontend-deploy/</guid><description>&lt;p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment&lt;/a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a>、路由、cache、環境變數與使用者可見回歸。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>前端部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>bundle、static site、asset hashing&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>browser regression、a11y、layout&lt;/td>
 &lt;td>Playwright / visual diff 是否通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a>&lt;/td>
 &lt;td>static files、search index、sourcemap&lt;/td>
 &lt;td>測試與發布是否同一份產物&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>hosting、CDN、Pages、preview URL&lt;/td>
 &lt;td>cache invalidation 與路由是否正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>回退前一版 static artifact&lt;/td>
 &lt;td>是否保留可回復版本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。&lt;/p>
&lt;p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。&lt;/p>
&lt;p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>CDN cache 要和 asset hash、HTML cache policy 分開看。&lt;/li>
&lt;li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。&lt;/li>
&lt;li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。&lt;/li>
&lt;li>環境變數若在 build time 注入，重新發布才會生效。&lt;/li>
&lt;li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>&lt;/td>
 &lt;td>Static artifact and preview&lt;/td>
 &lt;td>串起 build、browser test、preview 與 rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>前端 artifact 流程：讀 &lt;a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>。&lt;/li>
&lt;li>本 blog 的靜態站案例：讀 &lt;a href="../blog-project-deploy/">本 blog 專案部署&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 <a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment</a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a>、路由、cache、環境變數與使用者可見回歸。</p>
<h2 id="場域定位">場域定位</h2>
<p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>前端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>bundle、static site、asset hashing</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>browser regression、a11y、layout</td>
          <td>Playwright / visual diff 是否通過</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a></td>
          <td>static files、search index、sourcemap</td>
          <td>測試與發布是否同一份產物</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>hosting、CDN、Pages、preview URL</td>
          <td>cache invalidation 與路由是否正確</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>回退前一版 static artifact</td>
          <td>是否保留可回復版本</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。</p>
<p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。</p>
<p><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。</p>
<p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>CDN cache 要和 asset hash、HTML cache policy 分開看。</li>
<li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。</li>
<li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。</li>
<li>環境變數若在 build time 注入，重新發布才會生效。</li>
<li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a></td>
          <td>Static artifact and preview</td>
          <td>串起 build、browser test、preview 與 rollback</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>前端 artifact 流程：讀 <a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a>。</li>
<li>本 blog 的靜態站案例：讀 <a href="../blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>Branch Protection</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/branch-protection/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/branch-protection/</guid><description>&lt;p>Branch Protection 的核心概念是「把主線寫入條件制度化」。它把 required checks、review policy 與合併限制集中成 repository gate。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Branch Protection 位在 pull request 與主線分支之間，屬於 CI workflow 之外的治理層。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>主線偶爾進入未驗證變更。&lt;/li>
&lt;li>workflow 已存在但合併條件未綁定。&lt;/li>
&lt;li>團隊需要統一 reviewer 與狀態檢查門檻。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>專案要求 &lt;code>md-check&lt;/code> 與 &lt;code>Playwright tests&lt;/code> 必須綠燈，且至少一位 reviewer 批准才可合併 &lt;code>main&lt;/code>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Branch Protection 要定義必要 checks、審查規則與例外流程，並和 workflow 命名同步維護。&lt;/p></description><content:encoded><![CDATA[<p>Branch Protection 的核心概念是「把主線寫入條件制度化」。它把 required checks、review policy 與合併限制集中成 repository gate。</p>
<h2 id="概念位置">概念位置</h2>
<p>Branch Protection 位在 pull request 與主線分支之間，屬於 CI workflow 之外的治理層。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>主線偶爾進入未驗證變更。</li>
<li>workflow 已存在但合併條件未綁定。</li>
<li>團隊需要統一 reviewer 與狀態檢查門檻。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>專案要求 <code>md-check</code> 與 <code>Playwright tests</code> 必須綠燈，且至少一位 reviewer 批准才可合併 <code>main</code>。</p>
<h2 id="設計責任">設計責任</h2>
<p>Branch Protection 要定義必要 checks、審查規則與例外流程，並和 workflow 命名同步維護。</p>
]]></content:encoded></item><item><title>後端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/backend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/backend-deploy/</guid><description>&lt;p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration&lt;/a>）、外部依賴、runtime config、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check&lt;/a>（backend 深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check&lt;/a>）、流量切換與 rollback。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>後端部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>binary、package、container image&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>unit、integration、contract、migration&lt;/td>
 &lt;td>是否覆蓋跨服務契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>schema change、backfill、rollback path&lt;/td>
 &lt;td>是否可漸進、可停止、可驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a>&lt;/td>
 &lt;td>rolling、canary、blue-green&lt;/td>
 &lt;td>health / readiness 是否可信&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>app rollback、migration rollback / forward fix&lt;/td>
 &lt;td>回復路徑是否演練&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。&lt;/p>
&lt;p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。&lt;/li>
&lt;li>Health check 只代表 process alive，readiness 才能判斷能否接流量。&lt;/li>
&lt;li>Worker / consumer 部署要考慮重複處理、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>。&lt;/li>
&lt;li>Config rollout 需要版本化與回退路徑（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a>）。&lt;/li>
&lt;li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>&lt;/td>
 &lt;td>Migration rollout and rollback&lt;/td>
 &lt;td>拆分資料變更、流量推進與回復路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>後端發布主流程：讀 &lt;a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Backend reliability：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程&lt;/a>。&lt;/li>
&lt;li>Release gate：讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>後端部署 CI/CD 的核心責任是把可執行服務安全推進到 runtime 環境。後端部署不只發布程式碼，還要處理資料庫 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration</a>）、外部依賴、runtime config、<a href="/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check</a>（backend 深入見 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">Readiness</a> / <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check</a>）、流量切換與 rollback。</p>
<h2 id="場域定位">場域定位</h2>
<p>後端部署的主要風險來自有狀態依賴與長時間執行。API、worker、scheduler 與 consumer 會連到資料庫、queue、cache 與第三方服務；部署流程需要確認程式、資料與流量切換順序。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>後端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>binary、package、container image</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>unit、integration、contract、migration</td>
          <td>是否覆蓋跨服務契約</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>schema change、backfill、rollback path</td>
          <td>是否可漸進、可停止、可驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a></td>
          <td>rolling、canary、blue-green</td>
          <td>health / readiness 是否可信</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>app rollback、migration rollback / forward fix</td>
          <td>回復路徑是否演練</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可部署服務。後端 build 常見形式是 binary、package 或 container image；判讀重點是版本是否能追到 commit、依賴是否固定、產物是否能在乾淨環境重建。</p>
<p>Test 階段負責驗證服務契約。單元測試只能覆蓋局部邏輯，integration、contract 與 migration 測試才會揭露資料庫、queue、cache 與外部服務之間的相容性風險。</p>
<p><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 階段負責推進資料結構與資料狀態。真實服務要支援新舊程式短暫共存，因此 migration 應偏向可漸進、可重試、可觀測，必要時用 forward fix 取代直接回滾資料。</p>
<p><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a> 階段負責把流量安全導向新版本。Rolling、canary 與 blue-green 都需要可靠的 health、readiness、metrics 與 log；若 readiness 只檢查 process alive，流量仍可能被送到尚未準備好的服務。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責在新版本失效時縮小影響範圍。後端 rollback 要同時考慮程式、資料、queue message、外部 side effect 與 config；只回退 image tag，通常不足以處理已寫入的資料變化。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>Migration 要和 app rollout 分開設計，避免新舊版本不相容。</li>
<li>Health check 只代表 process alive，readiness 才能判斷能否接流量。</li>
<li>Worker / consumer 部署要考慮重複處理、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 與 <a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>。</li>
<li>Config rollout 需要版本化與回退路徑（深入見 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a>）。</li>
<li>Rollback 不只回程式，也要處理資料與外部副作用（深入見 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">Rollback Strategy</a>）。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a></td>
          <td>Migration rollout and rollback</td>
          <td>拆分資料變更、流量推進與回復路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>後端發布主流程：讀 <a href="migration-rollout-rollback-flow/">後端 migration、rollout 與 rollback 流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Backend reliability：讀 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a>。</li>
<li>Release gate：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
</ul>
]]></content:encoded></item><item><title>Jenkins → GitHub Actions：Pipeline 5 段 lifecycle 的對位 + 翻譯</title><link>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/migrate-from-jenkins/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/migrate-from-jenkins/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://www.jenkins.io/">Jenkins&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Schema = High（Groovy DSL ↔ YAML workflow）→ Type A phased translation&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="pipeline-5-段-lifecycle-的對位--翻譯">Pipeline 5 段 lifecycle 的對位 + 翻譯&lt;/h2>
&lt;p>本文按 &lt;em>pipeline lifecycle 5 段&lt;/em> 組織內容（variant E）— 不是「為什麼遷」driver 開頭，是 &lt;em>Jenkins vs GHA 對 5 段各自的處理&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Lifecycle 段&lt;/th>
 &lt;th>Jenkins 機制&lt;/th>
 &lt;th>GHA 機制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. Source / SCM&lt;/td>
 &lt;td>SCM polling / webhook trigger&lt;/td>
 &lt;td>&lt;code>on: [push, pull_request]&lt;/code> event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. Build / Package&lt;/td>
 &lt;td>&lt;code>stage('Build') { sh 'mvn package' }&lt;/code>&lt;/td>
 &lt;td>&lt;code>jobs.build.steps[].run: mvn package&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Test / 並行 matrix&lt;/td>
 &lt;td>&lt;code>parallel { ... }&lt;/code> + agents&lt;/td>
 &lt;td>&lt;code>jobs.test.strategy.matrix: ...&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. Security scan&lt;/td>
 &lt;td>Plugin（Snyk / SonarQube / Aqua）&lt;/td>
 &lt;td>Action（snyk/actions / sonarsource-actions）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5. Deploy / promote&lt;/td>
 &lt;td>Deploy plugin + approval gate&lt;/td>
 &lt;td>&lt;code>environment: production&lt;/code> + reviewer approval&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>Groovy DSL ↔ YAML、syntax 完全不同&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>Self-hosted Jenkins → GHA SaaS / self-hosted runners&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Imperative pipeline → declarative workflow + events&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Jenkins + plugins → GHA + actions marketplace&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Build script 多數不改、CI integration 端要改&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>同單一 build state&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Schema = High（其他 Medium-Low）→ &lt;strong>Type A phased translation&lt;/strong> 為主、加 paradigm + operational 獨立段。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="https://www.jenkins.io/">Jenkins</a> 跟 <a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Schema = High（Groovy DSL ↔ YAML workflow）→ Type A phased translation</em>。</p></blockquote>
<h2 id="pipeline-5-段-lifecycle-的對位--翻譯">Pipeline 5 段 lifecycle 的對位 + 翻譯</h2>
<p>本文按 <em>pipeline lifecycle 5 段</em> 組織內容（variant E）— 不是「為什麼遷」driver 開頭，是 <em>Jenkins vs GHA 對 5 段各自的處理</em>：</p>
<table>
  <thead>
      <tr>
          <th>Lifecycle 段</th>
          <th>Jenkins 機制</th>
          <th>GHA 機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. Source / SCM</td>
          <td>SCM polling / webhook trigger</td>
          <td><code>on: [push, pull_request]</code> event</td>
      </tr>
      <tr>
          <td>2. Build / Package</td>
          <td><code>stage('Build') { sh 'mvn package' }</code></td>
          <td><code>jobs.build.steps[].run: mvn package</code></td>
      </tr>
      <tr>
          <td>3. Test / 並行 matrix</td>
          <td><code>parallel { ... }</code> + agents</td>
          <td><code>jobs.test.strategy.matrix: ...</code></td>
      </tr>
      <tr>
          <td>4. Security scan</td>
          <td>Plugin（Snyk / SonarQube / Aqua）</td>
          <td>Action（snyk/actions / sonarsource-actions）</td>
      </tr>
      <tr>
          <td>5. Deploy / promote</td>
          <td>Deploy plugin + approval gate</td>
          <td><code>environment: production</code> + reviewer approval</td>
      </tr>
  </tbody>
</table>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Groovy DSL ↔ YAML、syntax 完全不同</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Self-hosted Jenkins → GHA SaaS / self-hosted runners</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Imperative pipeline → declarative workflow + events</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Jenkins + plugins → GHA + actions marketplace</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Build script 多數不改、CI integration 端要改</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同單一 build state</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Schema = High（其他 Medium-Low）→ <strong>Type A phased translation</strong> 為主、加 paradigm + operational 獨立段。</p>
<h2 id="為什麼遷cost--vendor--cloud-native-三條-driver">為什麼遷：cost / vendor / cloud-native 三條 driver</h2>
<ul>
<li><strong>Cost</strong>：Jenkins self-hosted 是「免費 software + 高 ops cost」、GHA 按 minute 計費對中小團隊更便宜</li>
<li><strong>Vendor consolidation</strong>：repository 已在 GitHub、整合進 GHA 省一個外部系統</li>
<li><strong>Cloud-native</strong>：GHA matrix build + reusable workflow 對 cloud-native deploy（K8s / serverless）有 first-class action</li>
</ul>
<h2 id="phase-0audit--classify">Phase 0：Audit + classify</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Jenkins workspace 盤點</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">find . -name <span class="s2">&#34;Jenkinsfile&#34;</span> -o -name <span class="s2">&#34;*.groovy&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 列所有 pipeline file</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 統計 plugin 使用</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># Jenkinsfile 內 import / @Library / sh &#34;tool plugin...&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">grep -rE <span class="s2">&#34;@Library|import|tools\s*\{&#34;</span> Jenkinsfile*
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 每 pipeline 評估 complexity</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># - Simple linear pipeline: 1-3 stage、無 shared library</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># - Medium: parallel stage + 2-5 shared library</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># - Complex: 條件分支 + 動態 stage + 10+ plugin / 5+ shared library</span></span></span></code></pre></div><p>Audit output：</p>
<ul>
<li>列「100 個 pipeline、35 simple / 50 medium / 15 complex」</li>
<li>每 complexity level 估翻譯時間（simple 0.5 day / medium 2 day / complex 5-10 day）</li>
<li>Plugin 依賴清單對應 GHA action 替代品</li>
</ul>
<h2 id="phase-1schema-對位groovy-dsl--yaml">Phase 1：Schema 對位（Groovy DSL ↔ YAML）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// Jenkins Declarative Pipeline
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">pipeline</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">agent</span> <span class="o">{</span> <span class="n">label</span> <span class="s1">&#39;docker-build&#39;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">stages</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Test&#39;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="n">parallel</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Unit&#39;</span><span class="o">)</span> <span class="o">{</span> <span class="n">steps</span> <span class="o">{</span> <span class="n">sh</span> <span class="s1">&#39;mvn test&#39;</span> <span class="o">}</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">stage</span><span class="o">(</span><span class="s1">&#39;Integration&#39;</span><span class="o">)</span> <span class="o">{</span> <span class="n">steps</span> <span class="o">{</span> <span class="n">sh</span> <span class="s1">&#39;mvn verify&#39;</span> <span class="o">}</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="n">post</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">failure</span> <span class="o">{</span> <span class="n">mail</span> <span class="nl">to:</span> <span class="s1">&#39;devops@&#39;</span><span class="o">,</span> <span class="nl">subject:</span> <span class="s1">&#39;Build failed&#39;</span> <span class="o">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="o">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># GHA Workflow 對等</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">push]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">self-hosted, docker-build]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">suite</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">unit, integration]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run ${{ matrix.suite }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="sd">          case &#34;${{ matrix.suite }}&#34; in
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">            unit) mvn test ;;
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">            integration) mvn verify ;;
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">          esac</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span><span class="nt">notify-failure</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">failure()</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">dawidd6/action-send-mail@v3</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">to</span><span class="p">:</span><span class="w"> </span><span class="l">devops@</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">          </span><span class="nt">subject</span><span class="p">:</span><span class="w"> </span><span class="l">Build failed</span></span></span></code></pre></div><p>對位差異：</p>
<ul>
<li><code>parallel { ... }</code> → <code>strategy.matrix</code>（粒度不同、matrix 是「同 step 不同參數」、parallel 是「不同 step」）</li>
<li><code>post.failure</code> → 獨立 job + <code>if: failure()</code></li>
<li><code>@Library</code> shared library → reusable workflow（<code>uses: ./.github/workflows/reusable.yml</code>）</li>
<li>Jenkins <code>tools { jdk 'java17' }</code> → setup-java action（手動配 toolchain）</li>
</ul>
<h2 id="phase-2translation-pipeline3-tier-hybrid">Phase 2：Translation pipeline（3-tier hybrid）</h2>
<p>對應 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic translation</a> 同 3-tier：</p>
<ul>
<li><strong>Tier 1</strong>：community tool（jenkins-to-actions converter、cover 簡單 pipeline 30-50%）</li>
<li><strong>Tier 2</strong>：LLM-assisted（Claude / GPT 翻 medium complexity、人工 verify）</li>
<li><strong>Tier 3</strong>：manual（shared library 改 reusable workflow / conditional 動態 stage 重寫）</li>
</ul>
<h2 id="phase-3parallel-run雙-ci-跑-4-8-週">Phase 3：Parallel run（雙 CI 跑 4-8 週）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Repository ──┬─→ Jenkins webhook ──→ Jenkinsfile pipeline
</span></span><span class="line"><span class="ln">2</span><span class="cl">             └─→ GitHub Action ────→ .github/workflows/ci.yml
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">Compare:
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 同 commit 兩端結果一致
</span></span><span class="line"><span class="ln">6</span><span class="cl">- Latency / cost / artifact location 對齊</span></span></code></pre></div><p>Diff dashboard 列「test pass rate / build time / failure mode」三 metric、跑到 95%+ 一致才進 cutover。</p>
<h2 id="phase-4cutover--cleanup">Phase 4：Cutover + cleanup</h2>
<ul>
<li>Disable Jenkins webhook</li>
<li>GHA 成 primary CI</li>
<li>Jenkins 留 standby 2 週 fallback</li>
<li>Decommission Jenkins controller + agents</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1shared-library-equivalencereusable-workflow-表達不足">Case 1：Shared library equivalence、reusable workflow 表達不足</h3>
<p><strong>徵兆</strong>：複雜 Jenkins shared library（含 Groovy class / closure / 動態變數）翻成 reusable workflow 後失準、某些動態邏輯無法表達。</p>
<p><strong>根因</strong>：Jenkins Groovy 是 imperative + 完整 programming language；GHA reusable workflow 是 declarative YAML、limited expressiveness。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>複雜邏輯外包到 script</strong>：reusable workflow 只當 <em>orchestrator</em>、複雜邏輯放 <code>.github/scripts/*.sh</code> 或 <code>actions/javascript-action</code></li>
<li><strong>自定 composite action</strong>：multi-step logic 包進 composite action、reuse 程度比 reusable workflow 高</li>
<li><strong>退役過度設計的 shared library</strong>：trans 過程暴露 90% library code 其實只用 10%</li>
</ol>
<h3 id="case-2ephemeral-workspacebuild-cache-失敗">Case 2：Ephemeral workspace、build cache 失敗</h3>
<p><strong>徵兆</strong>：cutover 後 build time 從 5 分鐘漲到 20 分鐘；Maven / Gradle / node_modules / Docker layer 每次都重抓。</p>
<p><strong>根因</strong>：Jenkins agent workspace persistent、build cache 跨 build 保留；GHA ephemeral runner 每次新 VM、cache 預設沒帶。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong><code>actions/cache@v4</code></strong>：cache key 用 <code>hashFiles('**/pom.xml')</code> 等 lock file、cross-build 復用</li>
<li><strong>Self-hosted runner with cache</strong>：critical pipeline 跑 self-hosted runner、persistent volume</li>
<li><strong>Docker layer cache</strong>：用 <code>docker/build-push-action</code> 配 BuildKit cache、不 rebuild full image</li>
</ol>
<h3 id="case-3plugin-不對等ci-feature-退化">Case 3：Plugin 不對等、CI feature 退化</h3>
<p><strong>徵兆</strong>：Jenkins 用 50+ plugin、GHA action marketplace 找不到對應；team 對 SonarQube quality gate / Jira integration / custom report 等失去 first-class 支援。</p>
<p><strong>根因</strong>：Jenkins plugin ecosystem 20+ 年累積、GHA marketplace 5 年；某些 niche plugin 在 GHA 沒對等 action。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>API-based integration</strong>：用 <code>curl</code> 對 vendor API 直接 call、不依賴 plugin / action</li>
<li><strong>自寫 action</strong>：critical feature 自寫 composite / JavaScript action、publish 到 marketplace</li>
<li><strong>退役舊 plugin</strong>：trans 期間 audit plugin 真實使用、80% 可退役</li>
</ol>
<h3 id="case-4self-hosted-runner-setup--scaling">Case 4：Self-hosted runner setup + scaling</h3>
<p><strong>徵兆</strong>：production workload 需要 GPU / large memory runner；GHA hosted runner spec 不夠、想用 self-hosted runner、發現 scaling / security / monitoring 比 Jenkins agent 複雜。</p>
<p><strong>根因</strong>：GHA self-hosted runner 是 ephemeral、scaling 需要 <em>runner controller</em>（actions-runner-controller on K8s）；跟 Jenkins agent / Kubernetes plugin 對應但 setup 不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>actions-runner-controller (ARC)</strong>：K8s-native runner scaling、跟 Jenkins K8s plugin 對應</li>
<li><strong>Runner labels</strong>：用 label 路由 job（<code>runs-on: [self-hosted, gpu, linux]</code>）</li>
<li><strong>Security</strong>：ephemeral runner 用 short-lived token、不跨 job persist secret</li>
</ol>
<h3 id="case-5matrix-build-vs-parallel-stage-表達差">Case 5：Matrix build vs parallel stage 表達差</h3>
<p><strong>徵兆</strong>：Jenkins 有 <em>動態 parallel</em>（runtime 決定要跑哪些 stage、按 input 變動）；GHA matrix 是 <em>static at workflow load time</em>、表達不到。</p>
<p><strong>根因</strong>：GHA matrix 是 declarative、workflow parse 時 expand；runtime 動態決定 stage 需要用 <code>if:</code> condition + 多 job。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>動態 matrix</strong>：用 <code>jobs.set-matrix</code> 先跑一個 job 算 matrix、輸出 JSON、後續 job <code>strategy.matrix: ${{ needs.set-matrix.outputs.matrix }}</code></li>
<li><strong>conditional job</strong>：每個 dynamic stage 寫獨立 job + <code>if:</code> 控制觸發</li>
<li><strong>重設計</strong>：90% 動態邏輯其實可改 static matrix + condition、純 runtime 動態通常是 over-engineering</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Jenkins</th>
          <th>GitHub Actions</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute cost</td>
          <td>EC2 + agent licenses</td>
          <td>per-minute billing（free tier + over-cap）</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 FTE</td>
          <td>0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Plugin / action ecosystem</td>
          <td>20+ 年成熟</td>
          <td>5 年快速成長</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>Agent ready &lt; 1 min</td>
          <td>Hosted runner 30-60s spin-up</td>
      </tr>
      <tr>
          <td>Self-hosted scaling</td>
          <td>Jenkins K8s plugin</td>
          <td>ARC（actions-runner-controller）</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>Self-managed VPC + secret</td>
          <td>OIDC + repository secret + environment</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 1-3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：100+ pipeline organization 切 GHA 通常 6-12 月 ROI 持平、之後省 ops cost；&lt; 30 pipeline 早就該切。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-gitlab-ci-對位">跟 <a href="https://docs.gitlab.com/ee/ci/">GitLab CI</a> 對位</h3>
<p>GitLab CI YAML 語法跟 GHA 接近、shared library 對應 <code>include:</code>、self-hosted runner 對等；Jenkins → GitLab CI migration 流程跟本文鏡像對稱、3-tier translation pipeline 通用。</p>
<h3 id="跟-circle-ci-對位">跟 <a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">Circle CI</a> 對位</h3>
<p>CircleCI orb 對等 GHA composite action；跨 SaaS CI 切換比 Jenkins → GHA 簡單（都 YAML-based）。</p>
<h3 id="反向-migrationgha--jenkins">反向 migration（GHA → Jenkins）</h3>
<p>少數 enterprise（金融 / 政府）合規要求 self-hosted CI / on-prem；GHA → Jenkins 鏡像對稱、注意 Jenkins shared library 表達力更強、reusable workflow 內 dynamic 邏輯可不必拆。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Reusable workflow + composite action 混用</strong>：reusable workflow 適合 <em>跨 repo orchestration</em>、composite action 適合 <em>單 repo logic encapsulation</em></li>
<li><strong>OIDC + cloud deploy</strong>：用 OIDC token 取代 long-lived cloud credential、是 GHA migration 順便升級的機會</li>
<li><strong>Cost optimization</strong>：minute-based billing 對 high-volume CI 需要 monitoring + budget alert</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<a href="/blog/backend/06-reliability/vendors/github-actions/" data-link-title="GitHub Actions" data-link-desc="GitHub 原生 CI/CD、PR check、deploy gate">GitHub Actions</a></li>
<li>平行 vendor：<a href="/blog/backend/06-reliability/vendors/circleci/" data-link-title="CircleCI" data-link-desc="CI/CD 平台、強 cache 與 parallelism">CircleCI</a></li>
<li>平行 migration playbook（Type A）：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>App 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/app-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/app-deploy/</guid><description>&lt;p>App 部署 CI/CD 的核心責任是把可安裝的 client &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a> 安全送到發行通道。App 發布和 web 部署最大的差異是使用者裝置會保留舊版，app store 審核、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing&lt;/a>、版本號與分批發布會直接影響交付節奏。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>App 部署的風險集中在 artifact 不可變、簽章憑證、store review 與版本分佈。後端可以快速 rollback，前端靜態站可以重新部署，但已安裝的 App 需要靠更新、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 或服務端相容性管理。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>App 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>IPA、APK、AAB、desktop package&lt;/td>
 &lt;td>build number / version 是否遞增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Signing&lt;/td>
 &lt;td>certificate、profile、keystore&lt;/td>
 &lt;td>secret 是否安全、是否可輪替&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>unit、UI、device matrix&lt;/td>
 &lt;td>是否覆蓋目標 OS 與裝置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release&lt;/td>
 &lt;td>store review、phased rollout&lt;/td>
 &lt;td>審核狀態與 rollout 百分比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>hotfix、remote config、kill switch&lt;/td>
 &lt;td>是否能處理已安裝舊版&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生可安裝 artifact。Mobile 常見產物是 IPA、APK 或 AAB，desktop 則可能是 installer 或 signed package；版本號、build number 與 commit 對應關係會決定後續除錯與回報能否追溯。&lt;/p>
&lt;p>Signing 階段負責證明 artifact 由可信來源發布。憑證、profile、keystore 與 signing secret 都屬於發布能力；它們需要輪替、權限控管與備援流程，避免單一憑證問題中斷發布（安全治理延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>）。&lt;/p>
&lt;p>Test 階段負責驗證不同裝置與作業系統組合。App 測試常見風險是 emulator 通過但真機失敗、特定 OS 權限模型不同、背景執行限制不同；device matrix 要依使用者分佈與高風險功能選擇。&lt;/p>
&lt;p>Release 階段負責把 artifact 送進發行通道。Store review、phased rollout、internal testing、beta track 與 production track 都是 gate；發布節奏要把審核時間與分批比例納入 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">rollout strategy&lt;/a> 的風險控制（backend 延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout&lt;/a>）。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責處理已安裝版本。App 發布後會長期存在多個使用者版本，因此 hotfix、remote config、kill switch 與後端相容性要一起設計（相容治理延伸見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract&lt;/a>）。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>簽章憑證是發布能力的一部分，要用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a> 管理。&lt;/li>
&lt;li>版本號與 build number 要可追溯到 commit 與 artifact。&lt;/li>
&lt;li>Store review 會讓 rollback 和 hotfix 變慢，風險要提前用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 控制。&lt;/li>
&lt;li>Client / server contract 要支援多版本共存。&lt;/li>
&lt;li>Crash reporting 與 phased rollout 是發布後 gate 的一部分。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程&lt;/a>&lt;/td>
 &lt;td>Signing, review and rollout&lt;/td>
 &lt;td>管理簽章、審核、分批發布與多版本共存&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>App 發布主流程：讀 &lt;a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>App 部署 CI/CD 的核心責任是把可安裝的 client <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a> 安全送到發行通道。App 發布和 web 部署最大的差異是使用者裝置會保留舊版，app store 審核、<a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a>、版本號與分批發布會直接影響交付節奏。</p>
<h2 id="場域定位">場域定位</h2>
<p>App 部署的風險集中在 artifact 不可變、簽章憑證、store review 與版本分佈。後端可以快速 rollback，前端靜態站可以重新部署，但已安裝的 App 需要靠更新、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 或服務端相容性管理。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>App 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>IPA、APK、AAB、desktop package</td>
          <td>build number / version 是否遞增</td>
      </tr>
      <tr>
          <td>Signing</td>
          <td>certificate、profile、keystore</td>
          <td>secret 是否安全、是否可輪替</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>unit、UI、device matrix</td>
          <td>是否覆蓋目標 OS 與裝置</td>
      </tr>
      <tr>
          <td>Release</td>
          <td>store review、phased rollout</td>
          <td>審核狀態與 rollout 百分比</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>hotfix、remote config、kill switch</td>
          <td>是否能處理已安裝舊版</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生可安裝 artifact。Mobile 常見產物是 IPA、APK 或 AAB，desktop 則可能是 installer 或 signed package；版本號、build number 與 commit 對應關係會決定後續除錯與回報能否追溯。</p>
<p>Signing 階段負責證明 artifact 由可信來源發布。憑證、profile、keystore 與 signing secret 都屬於發布能力；它們需要輪替、權限控管與備援流程，避免單一憑證問題中斷發布（安全治理延伸見 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>）。</p>
<p>Test 階段負責驗證不同裝置與作業系統組合。App 測試常見風險是 emulator 通過但真機失敗、特定 OS 權限模型不同、背景執行限制不同；device matrix 要依使用者分佈與高風險功能選擇。</p>
<p>Release 階段負責把 artifact 送進發行通道。Store review、phased rollout、internal testing、beta track 與 production track 都是 gate；發布節奏要把審核時間與分批比例納入 <a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">rollout strategy</a> 的風險控制（backend 延伸見 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a>）。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責處理已安裝版本。App 發布後會長期存在多個使用者版本，因此 hotfix、remote config、kill switch 與後端相容性要一起設計（相容治理延伸見 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a>）。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>簽章憑證是發布能力的一部分，要用 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 管理。</li>
<li>版本號與 build number 要可追溯到 commit 與 artifact。</li>
<li>Store review 會讓 rollback 和 hotfix 變慢，風險要提前用 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 控制。</li>
<li>Client / server contract 要支援多版本共存。</li>
<li>Crash reporting 與 phased rollout 是發布後 gate 的一部分。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程</a></td>
          <td>Signing, review and rollout</td>
          <td>管理簽章、審核、分批發布與多版本共存</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>App 發布主流程：讀 <a href="signing-store-rollout-flow/">App 簽章、商店審核與分批發布流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>Docker / Image 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/docker-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/docker-deploy/</guid><description>&lt;p>Docker / image 部署 CI/CD 的核心責任是把可執行環境封裝成可追溯的 image。Image 同時承載 application、runtime、OS package、dependency 與安全掃描結果，因此它是可以被推進、掃描與回溯的部署產物；而 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry&lt;/a> 提供保存與推進的供應鏈節點。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Image 部署常出現在後端、worker、batch job 與自架服務。它把「在哪個環境跑」前移到 build 階段，但也引入 registry、tag、base image、vulnerability scan、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a> 與 promotion 流程（platform 概念可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container&lt;/a>）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Image 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>Dockerfile、multi-stage build&lt;/td>
 &lt;td>image 是否可重現、layer 是否合理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tag&lt;/td>
 &lt;td>semver、commit SHA、release tag&lt;/td>
 &lt;td>tag 是否能追到 source&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scan&lt;/td>
 &lt;td>vulnerability、secret、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a>&lt;/td>
 &lt;td>是否有阻擋門檻與例外流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Registry&lt;/td>
 &lt;td>push、retention、promotion&lt;/td>
 &lt;td>prod image 是否來自已驗證 artifact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime&lt;/td>
 &lt;td>Kubernetes、Compose、ECS 等&lt;/td>
 &lt;td>health、readiness、rollback 是否存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責把 application 與 runtime 封裝成 image。Multi-stage build、dependency cache、base image 與 layer 順序會影響速度、安全性與可重現性；CI 應能從 Dockerfile 與 lockfile 重建同一類產物。&lt;/p>
&lt;p>Tag 階段負責讓 image 可追溯。Commit SHA、release tag 與 semver 各自服務不同查詢情境；production 需要能從 running image 反查 source、workflow run 與掃描結果。&lt;/p>
&lt;p>Scan 階段負責讓 image 風險可見。Vulnerability scan、secret scan 與 SBOM 能把 base image、OS package 與 dependency 風險顯性化；阻擋門檻要和例外流程一起定義，讓掃描結果能被分流處理。&lt;/p>
&lt;p>Registry 階段負責保存與推進 image。真實流程通常需要 retention、immutability、promotion 與權限控管；production image 應來自已驗證 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff&lt;/a>，讓各環境推進同一份產物（供應鏈治理可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance&lt;/a>）。&lt;/p>
&lt;p>Runtime 階段負責把 image 轉成可運行服務。Kubernetes、Compose、ECS 或其他平台都需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit&lt;/a>、secret injection（可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>）與 rollback 設計，否則 image 成功不等於服務可用。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>&lt;code>latest&lt;/code> 不適合當 production 追溯依據。&lt;/li>
&lt;li>Base image 要有更新節奏，否則掃描結果會持續惡化。&lt;/li>
&lt;li>Build secret 不應留在 image layer。&lt;/li>
&lt;li>Scan gate 要區分阻擋門檻與可接受例外。&lt;/li>
&lt;li>Promotion 應推進同一份 image，讓 staging 與 production 的差異集中在設定與流量。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程&lt;/a>&lt;/td>
 &lt;td>Image supply chain&lt;/td>
 &lt;td>建立可追溯 tag、掃描 gate 與 registry 推進&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Image 供應鏈流程：讀 &lt;a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程&lt;/a>。&lt;/li>
&lt;li>後端部署：讀 &lt;a href="../backend-deploy/">後端部署 CI/CD&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Backend deployment platform：讀 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Docker / image 部署 CI/CD 的核心責任是把可執行環境封裝成可追溯的 image。Image 同時承載 application、runtime、OS package、dependency 與安全掃描結果，因此它是可以被推進、掃描與回溯的部署產物；而 <a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a> 提供保存與推進的供應鏈節點。</p>
<h2 id="場域定位">場域定位</h2>
<p>Image 部署常出現在後端、worker、batch job 與自架服務。它把「在哪個環境跑」前移到 build 階段，但也引入 registry、tag、base image、vulnerability scan、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 與 promotion 流程（platform 概念可對照 <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container</a>）。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Image 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Dockerfile、multi-stage build</td>
          <td>image 是否可重現、layer 是否合理</td>
      </tr>
      <tr>
          <td>Tag</td>
          <td>semver、commit SHA、release tag</td>
          <td>tag 是否能追到 source</td>
      </tr>
      <tr>
          <td>Scan</td>
          <td>vulnerability、secret、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a></td>
          <td>是否有阻擋門檻與例外流程</td>
      </tr>
      <tr>
          <td>Registry</td>
          <td>push、retention、promotion</td>
          <td>prod image 是否來自已驗證 artifact</td>
      </tr>
      <tr>
          <td>Runtime</td>
          <td>Kubernetes、Compose、ECS 等</td>
          <td>health、readiness、rollback 是否存在</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責把 application 與 runtime 封裝成 image。Multi-stage build、dependency cache、base image 與 layer 順序會影響速度、安全性與可重現性；CI 應能從 Dockerfile 與 lockfile 重建同一類產物。</p>
<p>Tag 階段負責讓 image 可追溯。Commit SHA、release tag 與 semver 各自服務不同查詢情境；production 需要能從 running image 反查 source、workflow run 與掃描結果。</p>
<p>Scan 階段負責讓 image 風險可見。Vulnerability scan、secret scan 與 SBOM 能把 base image、OS package 與 dependency 風險顯性化；阻擋門檻要和例外流程一起定義，讓掃描結果能被分流處理。</p>
<p>Registry 階段負責保存與推進 image。真實流程通常需要 retention、immutability、promotion 與權限控管；production image 應來自已驗證 <a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff</a>，讓各環境推進同一份產物（供應鏈治理可對照 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance</a>）。</p>
<p>Runtime 階段負責把 image 轉成可運行服務。Kubernetes、Compose、ECS 或其他平台都需要 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a>、secret injection（可對照 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>）與 rollback 設計，否則 image 成功不等於服務可用。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li><code>latest</code> 不適合當 production 追溯依據。</li>
<li>Base image 要有更新節奏，否則掃描結果會持續惡化。</li>
<li>Build secret 不應留在 image layer。</li>
<li>Scan gate 要區分阻擋門檻與可接受例外。</li>
<li>Promotion 應推進同一份 image，讓 staging 與 production 的差異集中在設定與流量。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程</a></td>
          <td>Image supply chain</td>
          <td>建立可追溯 tag、掃描 gate 與 registry 推進</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Image 供應鏈流程：讀 <a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程</a>。</li>
<li>後端部署：讀 <a href="../backend-deploy/">後端部署 CI/CD</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Backend deployment platform：讀 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口</a>。</li>
</ul>
]]></content:encoded></item><item><title>Serverless 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/serverless-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/serverless-deploy/</guid><description>&lt;p>Serverless 部署 CI/CD 的核心責任是把函式型服務安全推進到受管執行環境。它和長駐服務不同，風險集中在 artifact 打包、runtime 相容、權限設定、版本別名與冷啟動行為。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Serverless 發布通常以函式版本為單位，並透過 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a> 或流量權重切換。部署步驟看起來短，但對權限、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a>、重試政策與 observability 欄位要求很高。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Serverless 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>function bundle、dependency、runtime target&lt;/td>
 &lt;td>package 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>function version、alias、traffic shift&lt;/td>
 &lt;td>新舊版本是否可並存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission&lt;/td>
 &lt;td>IAM、resource policy、secret scope&lt;/td>
 &lt;td>執行是否具最小權限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event Source&lt;/td>
 &lt;td>queue/topic/http trigger 設定&lt;/td>
 &lt;td>重試與死信策略是否明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>alias rollback、disable trigger&lt;/td>
 &lt;td>故障時是否可快速止血&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>部署前要先驗證 runtime 與依賴版本，避免 deploy 成功但 invocation 失敗。&lt;/li>
&lt;li>事件觸發型函式要明確設定 retry、dead-letter 或回放策略。&lt;/li>
&lt;li>權限設定要收斂到最小範圍，避免函式擴權風險。&lt;/li>
&lt;li>冷啟動與併發上限要納入發布後觀測指標。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程&lt;/a>&lt;/td>
 &lt;td>Function version and event&lt;/td>
 &lt;td>管理版本別名、事件來源、權限與回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Serverless 發布主流程：讀 &lt;a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;li>Backend 相關概念：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Serverless / worker 相關知識卡&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Serverless 部署 CI/CD 的核心責任是把函式型服務安全推進到受管執行環境。它和長駐服務不同，風險集中在 artifact 打包、runtime 相容、權限設定、版本別名與冷啟動行為。</p>
<h2 id="場域定位">場域定位</h2>
<p>Serverless 發布通常以函式版本為單位，並透過 <a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a> 或流量權重切換。部署步驟看起來短，但對權限、<a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a>、重試政策與 observability 欄位要求很高。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Serverless 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>function bundle、dependency、runtime target</td>
          <td>package 是否可重現</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>function version、alias、traffic shift</td>
          <td>新舊版本是否可並存</td>
      </tr>
      <tr>
          <td>Permission</td>
          <td>IAM、resource policy、secret scope</td>
          <td>執行是否具最小權限</td>
      </tr>
      <tr>
          <td>Event Source</td>
          <td>queue/topic/http trigger 設定</td>
          <td>重試與死信策略是否明確</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>alias rollback、disable trigger</td>
          <td>故障時是否可快速止血</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>部署前要先驗證 runtime 與依賴版本，避免 deploy 成功但 invocation 失敗。</li>
<li>事件觸發型函式要明確設定 retry、dead-letter 或回放策略。</li>
<li>權限設定要收斂到最小範圍，避免函式擴權風險。</li>
<li>冷啟動與併發上限要納入發布後觀測指標。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程</a></td>
          <td>Function version and event</td>
          <td>管理版本別名、事件來源、權限與回復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Serverless 發布主流程：讀 <a href="function-version-event-flow/">Serverless function 版本、事件來源與回復流程</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>Backend 相關概念：讀 <a href="/blog/backend/knowledge-cards/local-worker/" data-link-title="Local Worker" data-link-desc="說明同一個 process 內的背景工作模型與其生命週期邊界">Serverless / worker 相關知識卡</a>。</li>
</ul>
]]></content:encoded></item><item><title>Data Pipeline 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/data-pipeline-deploy/</guid><description>&lt;p>Data Pipeline 部署 CI/CD 的核心責任是把資料處理邏輯推進到生產環境，同時維持資料正確性與可回復性。它和 API 部署不同，重點在 schema 相容、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a> 風險。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Data pipeline 常包含 batch job、stream processor、dbt model 或 workflow scheduler。部署判斷不只看程式可執行，還要看資料是否可追溯、可對帳、可修復。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Data pipeline 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>transform code、DAG、query model&lt;/td>
 &lt;td>版本是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation&lt;/td>
 &lt;td>schema check、sample run、contract check&lt;/td>
 &lt;td>輸出是否維持相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>job version、schedule、trigger&lt;/td>
 &lt;td>新流程是否正確接管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>&lt;/td>
 &lt;td>歷史資料補算與節流&lt;/td>
 &lt;td>是否有 checkpoint 與停損條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>rerun、rollback、forward fix&lt;/td>
 &lt;td>異常資料是否可修補&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>schema 變更要先定義相容窗口，再切換 downstream。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a> 要有節流與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a>，避免壓垮上游與儲存層。&lt;/li>
&lt;li>部署後需比對新舊輸出一致性，建立 correctness check。&lt;/li>
&lt;li>重跑流程要有 runbook，避免人工臨場判斷失誤。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程&lt;/a>&lt;/td>
 &lt;td>Backfill, checkpoint and rerun&lt;/td>
 &lt;td>控制歷史補算、重跑與資料修復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Data pipeline 發布主流程：讀 &lt;a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程&lt;/a>。&lt;/li>
&lt;li>後端資料遷移概念：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration&lt;/a>。&lt;/li>
&lt;li>資料修補與比對：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">Correctness Check&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Data Pipeline 部署 CI/CD 的核心責任是把資料處理邏輯推進到生產環境，同時維持資料正確性與可回復性。它和 API 部署不同，重點在 schema 相容、<a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a>、<a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a> 與 <a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a> 風險。</p>
<h2 id="場域定位">場域定位</h2>
<p>Data pipeline 常包含 batch job、stream processor、dbt model 或 workflow scheduler。部署判斷不只看程式可執行，還要看資料是否可追溯、可對帳、可修復。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Data pipeline 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>transform code、DAG、query model</td>
          <td>版本是否可重現</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>schema check、sample run、contract check</td>
          <td>輸出是否維持相容</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>job version、schedule、trigger</td>
          <td>新流程是否正確接管</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a></td>
          <td>歷史資料補算與節流</td>
          <td>是否有 checkpoint 與停損條件</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>rerun、rollback、forward fix</td>
          <td>異常資料是否可修補</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>schema 變更要先定義相容窗口，再切換 downstream。</li>
<li><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a> 要有節流與 <a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a>，避免壓垮上游與儲存層。</li>
<li>部署後需比對新舊輸出一致性，建立 correctness check。</li>
<li>重跑流程要有 runbook，避免人工臨場判斷失誤。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程</a></td>
          <td>Backfill, checkpoint and rerun</td>
          <td>控制歷史補算、重跑與資料修復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Data pipeline 發布主流程：讀 <a href="backfill-checkpoint-rerun-flow/">Data pipeline backfill、checkpoint 與 rerun 流程</a>。</li>
<li>後端資料遷移概念：讀 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">Migration</a>。</li>
<li>資料修補與比對：讀 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill</a> 與 <a href="/blog/backend/knowledge-cards/correctness-check/" data-link-title="Correctness Check" data-link-desc="說明遷移或重構期間如何驗證新舊結果是否符合規則">Correctness Check</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Flaky Test</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/</guid><description>&lt;p>Flaky Test 的核心概念是「同一版本在相同條件下測試結果不穩定」。它會把紅燈從有效訊號降級成噪音，直接影響 CI gate 信任度。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Flaky Test 位在 test stage 與 release gate 之間，會放大重跑成本與判讀延遲。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>同一 commit 重跑結果時好時壞。&lt;/li>
&lt;li>失敗集中在等待條件、時間假設或外部依賴。&lt;/li>
&lt;li>團隊習慣以重跑代替根因修復。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>UI 測試在動畫未完成時抓取元素，或整合測試依賴不穩定第三方 API，都容易出現 flaky pattern。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Flaky Test 治理要建立 owner、隔離策略、修復 SLA 與觀測指標，讓測試結果恢復可判讀性。&lt;/p></description><content:encoded><![CDATA[<p>Flaky Test 的核心概念是「同一版本在相同條件下測試結果不穩定」。它會把紅燈從有效訊號降級成噪音，直接影響 CI gate 信任度。</p>
<h2 id="概念位置">概念位置</h2>
<p>Flaky Test 位在 test stage 與 release gate 之間，會放大重跑成本與判讀延遲。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>同一 commit 重跑結果時好時壞。</li>
<li>失敗集中在等待條件、時間假設或外部依賴。</li>
<li>團隊習慣以重跑代替根因修復。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>UI 測試在動畫未完成時抓取元素，或整合測試依賴不穩定第三方 API，都容易出現 flaky pattern。</p>
<h2 id="設計責任">設計責任</h2>
<p>Flaky Test 治理要建立 owner、隔離策略、修復 SLA 與觀測指標，讓測試結果恢復可判讀性。</p>
]]></content:encoded></item><item><title>Backfill</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/</guid><description>&lt;p>Backfill 的核心概念是「用新邏輯受控補算既有資料」。它通常和 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a> 共享相容窗口，並依賴 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a> 保存進度。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Backfill 位在資料 schema、transform logic 或歷史資料修補之後，常出現在 data pipeline、database migration、search index rebuild 與 feature store 更新。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>新欄位需要從既有資料補值。&lt;/li>
&lt;li>歷史 partition 需要用新版邏輯重新計算。&lt;/li>
&lt;li>補算任務需要節流、停損與對帳。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>訂單報表新增 &lt;code>net_revenue&lt;/code> 欄位時，pipeline 先讓新資料寫入新欄位，再分批 backfill 過去 12 個月的 partition，並用 row count 與金額總和比對結果。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Backfill 要定義補算範圍、批次大小、checkpoint、停損條件與對帳方式，讓歷史資料修補成為可停止、可接續、可驗證的流程。&lt;/p></description><content:encoded><![CDATA[<p>Backfill 的核心概念是「用新邏輯受控補算既有資料」。它通常和 <a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a> 共享相容窗口，並依賴 <a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a> 保存進度。</p>
<h2 id="概念位置">概念位置</h2>
<p>Backfill 位在資料 schema、transform logic 或歷史資料修補之後，常出現在 data pipeline、database migration、search index rebuild 與 feature store 更新。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>新欄位需要從既有資料補值。</li>
<li>歷史 partition 需要用新版邏輯重新計算。</li>
<li>補算任務需要節流、停損與對帳。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>訂單報表新增 <code>net_revenue</code> 欄位時，pipeline 先讓新資料寫入新欄位，再分批 backfill 過去 12 個月的 partition，並用 row count 與金額總和比對結果。</p>
<h2 id="設計責任">設計責任</h2>
<p>Backfill 要定義補算範圍、批次大小、checkpoint、停損條件與對帳方式，讓歷史資料修補成為可停止、可接續、可驗證的流程。</p>
]]></content:encoded></item><item><title>IaC / Platform 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/iac-platform-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/iac-platform-deploy/</guid><description>&lt;p>IaC / Platform 部署 CI/CD 的核心責任是把基礎設施變更轉成可審查、可追溯、可回復的流程。它和應用部署不同，主要風險在 state、權限、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 與不可逆資源變更。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>IaC 流程通常分成 plan、review、apply 三段，並依環境分層推進。部署成功不只代表指令完成，還代表資源狀態符合預期且未引入漂移。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>IaC 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Plan&lt;/td>
 &lt;td>變更差異預覽與風險提示&lt;/td>
 &lt;td>是否包含高風險破壞性變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>審核資源變更與權限範圍&lt;/td>
 &lt;td>是否符合治理規範&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apply&lt;/td>
 &lt;td>狀態寫入與資源同步&lt;/td>
 &lt;td>state lock / timeout 是否可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>&lt;/td>
 &lt;td>實際環境與宣告差異檢查&lt;/td>
 &lt;td>是否存在未受控手動變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>回退或補正策略&lt;/td>
 &lt;td>失敗時是否有安全回復路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>plan 與 apply 要用同一份輸入與版本，避免結果漂移。&lt;/li>
&lt;li>state backend 要有鎖定與權限隔離，避免併發覆寫。&lt;/li>
&lt;li>高風險資源變更需要額外 gate（人工審核或變更時窗）。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 偵測要定期執行，並有修復責任人。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程&lt;/a>&lt;/td>
 &lt;td>Plan, apply, drift and recovery&lt;/td>
 &lt;td>控制基礎設施變更、漂移與回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>IaC 發布主流程：讀 &lt;a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程&lt;/a>。&lt;/li>
&lt;li>環境保護：讀 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a>。&lt;/li>
&lt;li>部署合約：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract&lt;/a>。&lt;/li>
&lt;li>變更放行：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>IaC / Platform 部署 CI/CD 的核心責任是把基礎設施變更轉成可審查、可追溯、可回復的流程。它和應用部署不同，主要風險在 state、權限、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 與不可逆資源變更。</p>
<h2 id="場域定位">場域定位</h2>
<p>IaC 流程通常分成 plan、review、apply 三段，並依環境分層推進。部署成功不只代表指令完成，還代表資源狀態符合預期且未引入漂移。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>IaC 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan</td>
          <td>變更差異預覽與風險提示</td>
          <td>是否包含高風險破壞性變更</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>審核資源變更與權限範圍</td>
          <td>是否符合治理規範</td>
      </tr>
      <tr>
          <td>Apply</td>
          <td>狀態寫入與資源同步</td>
          <td>state lock / timeout 是否可控</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>實際環境與宣告差異檢查</td>
          <td>是否存在未受控手動變更</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>回退或補正策略</td>
          <td>失敗時是否有安全回復路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>plan 與 apply 要用同一份輸入與版本，避免結果漂移。</li>
<li>state backend 要有鎖定與權限隔離，避免併發覆寫。</li>
<li>高風險資源變更需要額外 gate（人工審核或變更時窗）。</li>
<li><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 偵測要定期執行，並有修復責任人。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程</a></td>
          <td>Plan, apply, drift and recovery</td>
          <td>控制基礎設施變更、漂移與回復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>IaC 發布主流程：讀 <a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程</a>。</li>
<li>環境保護：讀 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</li>
<li>部署合約：讀 <a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract</a>。</li>
<li>變更放行：讀 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a>。</li>
</ul>
]]></content:encoded></item><item><title>Checkpoint</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/</guid><description>&lt;p>Checkpoint 的核心概念是「保存可接續的處理進度」。它讓 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a> 可以從明確位置恢復，避免每次都從頭開始。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Checkpoint 位在長時間 job、stream processor、batch pipeline 與 migration 任務之間，常以 partition、offset、run id、cursor 或 processed marker 呈現。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>任務執行時間長，失敗後需要接續。&lt;/li>
&lt;li>重跑同一區間可能造成重複寫入。&lt;/li>
&lt;li>streaming consumer 需要保存 offset 或 event position。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>資料回填每次處理一個日期 partition，完成後寫入 &lt;code>backfill_runs&lt;/code> 表。任務中斷時，下一次從最後成功 partition 的下一段開始。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Checkpoint 要定義進度格式、提交時機、失敗恢復、重跑覆寫與觀測欄位，讓長時間任務具備可恢復性。&lt;/p></description><content:encoded><![CDATA[<p>Checkpoint 的核心概念是「保存可接續的處理進度」。它讓 <a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a> 與 <a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a> 可以從明確位置恢復，避免每次都從頭開始。</p>
<h2 id="概念位置">概念位置</h2>
<p>Checkpoint 位在長時間 job、stream processor、batch pipeline 與 migration 任務之間，常以 partition、offset、run id、cursor 或 processed marker 呈現。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>任務執行時間長，失敗後需要接續。</li>
<li>重跑同一區間可能造成重複寫入。</li>
<li>streaming consumer 需要保存 offset 或 event position。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>資料回填每次處理一個日期 partition，完成後寫入 <code>backfill_runs</code> 表。任務中斷時，下一次從最後成功 partition 的下一段開始。</p>
<h2 id="設計責任">設計責任</h2>
<p>Checkpoint 要定義進度格式、提交時機、失敗恢復、重跑覆寫與觀測欄位，讓長時間任務具備可恢復性。</p>
]]></content:encoded></item><item><title>Desktop Client 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/desktop-client-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/desktop-client-deploy/</guid><description>&lt;p>Desktop Client 部署 CI/CD 的核心責任是把可安裝客戶端安全交付到使用者裝置，並維持可更新與可回退能力。它和 web 發布不同，重點在安裝包簽章、公證、更新通道與多平台相容。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Desktop client 常見於 Flutter Desktop、Electron、Tauri。部署流程通常要分平台建置（macOS、Windows、Linux），並處理安裝體驗、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel&lt;/a> 更新節奏與版本共存。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Desktop client 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>platform-specific bundle / installer&lt;/td>
 &lt;td>各平台產物是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Signing&lt;/td>
 &lt;td>code signing、notarization、timestamp&lt;/td>
 &lt;td>安裝與啟動是否受信任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release&lt;/td>
 &lt;td>channel、staged rollout、notes&lt;/td>
 &lt;td>更新節奏是否可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Update&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed&lt;/a>、delta package&lt;/td>
 &lt;td>升級是否穩定可回復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>hotfix package、rollback channel&lt;/td>
 &lt;td>失敗時是否可快速回退&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>不同 OS 的簽章與公證流程需分開治理。&lt;/li>
&lt;li>Auto-update 要有版本相容策略與 fallback feed。&lt;/li>
&lt;li>崩潰回報與更新成功率應列為發布後 gate。&lt;/li>
&lt;li>若與 Flutter App 共用程式碼，要明確區分 mobile 與 desktop 的發布管線。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="signing-notarization-update-flow/">Desktop client 簽章、公證與自動更新流程&lt;/a>&lt;/td>
 &lt;td>Signing, notarization and update&lt;/td>
 &lt;td>管理安裝包信任鏈、更新通道與回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Desktop 發布主流程：讀 &lt;a href="signing-notarization-update-flow/">Desktop client 簽章、公證與自動更新流程&lt;/a>。&lt;/li>
&lt;li>行動與客戶端通用觀念：讀 &lt;a href="../app-deploy/">App 部署 CI/CD&lt;/a>。&lt;/li>
&lt;li>簽章治理：讀 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Desktop Client 部署 CI/CD 的核心責任是把可安裝客戶端安全交付到使用者裝置，並維持可更新與可回退能力。它和 web 發布不同，重點在安裝包簽章、公證、更新通道與多平台相容。</p>
<h2 id="場域定位">場域定位</h2>
<p>Desktop client 常見於 Flutter Desktop、Electron、Tauri。部署流程通常要分平台建置（macOS、Windows、Linux），並處理安裝體驗、<a href="/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel</a> 更新節奏與版本共存。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Desktop client 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>platform-specific bundle / installer</td>
          <td>各平台產物是否可重現</td>
      </tr>
      <tr>
          <td>Signing</td>
          <td>code signing、notarization、timestamp</td>
          <td>安裝與啟動是否受信任</td>
      </tr>
      <tr>
          <td>Release</td>
          <td>channel、staged rollout、notes</td>
          <td>更新節奏是否可控</td>
      </tr>
      <tr>
          <td>Update</td>
          <td><a href="/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed</a>、delta package</td>
          <td>升級是否穩定可回復</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>hotfix package、rollback channel</td>
          <td>失敗時是否可快速回退</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>不同 OS 的簽章與公證流程需分開治理。</li>
<li>Auto-update 要有版本相容策略與 fallback feed。</li>
<li>崩潰回報與更新成功率應列為發布後 gate。</li>
<li>若與 Flutter App 共用程式碼，要明確區分 mobile 與 desktop 的發布管線。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="signing-notarization-update-flow/">Desktop client 簽章、公證與自動更新流程</a></td>
          <td>Signing, notarization and update</td>
          <td>管理安裝包信任鏈、更新通道與回復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Desktop 發布主流程：讀 <a href="signing-notarization-update-flow/">Desktop client 簽章、公證與自動更新流程</a>。</li>
<li>行動與客戶端通用觀念：讀 <a href="../app-deploy/">App 部署 CI/CD</a>。</li>
<li>簽章治理：讀 <a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a> 與 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>Rerun</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/</guid><description>&lt;p>Rerun 的核心概念是「用明確條件重新執行同一段流程」。它和 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test&lt;/a> 的治理有關，也常依賴 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a> 判斷接續位置。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Rerun 位在測試失敗、部署預演失敗、資料任務失敗或 pipeline repair 之後，負責判斷重新執行是否會改變輸出或擴大副作用。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>同一 commit 的測試結果前後不一致。&lt;/li>
&lt;li>資料任務部分成功、部分失敗。&lt;/li>
&lt;li>部署 dry run 失敗後需要確認是否可安全再跑。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>每日營收 pipeline 第三個 partition 寫入失敗。團隊先確認前兩個 partition 已完成且輸出可覆寫，再指定 run id 與 partition 範圍 rerun，避免重複計算全部歷史資料。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Rerun 要定義可重跑條件、輸出覆寫規則、idempotency、觀測結果與人工審核門檻，讓「再跑一次」成為受控恢復策略。&lt;/p></description><content:encoded><![CDATA[<p>Rerun 的核心概念是「用明確條件重新執行同一段流程」。它和 <a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test</a> 的治理有關，也常依賴 <a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a> 判斷接續位置。</p>
<h2 id="概念位置">概念位置</h2>
<p>Rerun 位在測試失敗、部署預演失敗、資料任務失敗或 pipeline repair 之後，負責判斷重新執行是否會改變輸出或擴大副作用。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>同一 commit 的測試結果前後不一致。</li>
<li>資料任務部分成功、部分失敗。</li>
<li>部署 dry run 失敗後需要確認是否可安全再跑。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>每日營收 pipeline 第三個 partition 寫入失敗。團隊先確認前兩個 partition 已完成且輸出可覆寫，再指定 run id 與 partition 範圍 rerun，避免重複計算全部歷史資料。</p>
<h2 id="設計責任">設計責任</h2>
<p>Rerun 要定義可重跑條件、輸出覆寫規則、idempotency、觀測結果與人工審核門檻，讓「再跑一次」成為受控恢復策略。</p>
]]></content:encoded></item><item><title>Package / Library Release CI/CD</title><link>https://tarrragon.github.io/blog/ci/package-library-release/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/package-library-release/</guid><description>&lt;p>Package / Library Release CI/CD 的核心責任是把可重用套件安全發佈到分發平台，並維持版本語意與相容承諾。它和應用部署不同，重點在版本管理、相容邊界、發佈簽章與撤版策略。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>套件發佈常見於 NPM、PyPI、Maven、Crates 等生態。發布後會被多個下游專案依賴，因此每次 release 都是公共契約變更。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Package release 常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>package artifact、metadata、lock input&lt;/td>
 &lt;td>產物是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation&lt;/td>
 &lt;td>API/ABI 相容性、smoke test、publish dry-run&lt;/td>
 &lt;td>破壞性變更是否被識別&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Versioning&lt;/td>
 &lt;td>semver、pre-release、changelog&lt;/td>
 &lt;td>版本語意是否與變更一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Publish&lt;/td>
 &lt;td>registry token、scope、provenance&lt;/td>
 &lt;td>發版是否可追溯且權限正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>yank/deprecate/hotfix release&lt;/td>
 &lt;td>事故時是否可快速止損&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="release-發布類型分類">Release 發布類型分類&lt;/h2>
&lt;p>「發版」在中文討論裡常被當成單一動作，但實際上有五條互不重疊的通道，每條的觸發條件、產物形式、下游取用方式都不一樣。下游使用者讀 README 時若沒分清楚自己在走哪條通道，很容易踩到「文件寫了安裝指令，但對應通道還沒被建立」的情況。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>產物形式&lt;/th>
 &lt;th>下游取用方式&lt;/th>
 &lt;th>典型觸發&lt;/th>
 &lt;th>代表生態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source release&lt;/td>
 &lt;td>git tag + tarball&lt;/td>
 &lt;td>&lt;code>git clone&lt;/code> 或 &lt;code>go install&lt;/code> 後編譯&lt;/td>
 &lt;td>tag push&lt;/td>
 &lt;td>Go module、許多 OSS 函式庫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Registry publish&lt;/td>
 &lt;td>套件清單登錄&lt;/td>
 &lt;td>&lt;code>npm install&lt;/code> / &lt;code>pip install&lt;/code> 等&lt;/td>
 &lt;td>&lt;code>publish&lt;/code> 指令&lt;/td>
 &lt;td>npm、PyPI、crates.io、Maven&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Binary release&lt;/td>
 &lt;td>預編譯多平台執行檔，掛在 GitHub Release&lt;/td>
 &lt;td>下載 binary 或 installer script&lt;/td>
 &lt;td>tag push&lt;/td>
 &lt;td>cargo-dist、goreleaser 工具鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Container image&lt;/td>
 &lt;td>OCI image&lt;/td>
 &lt;td>&lt;code>docker pull&lt;/code> / k8s manifest&lt;/td>
 &lt;td>tag 或 commit&lt;/td>
 &lt;td>Docker Hub、GHCR、ECR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OS package&lt;/td>
 &lt;td>&lt;code>.deb&lt;/code> / &lt;code>.rpm&lt;/code> / Homebrew formula&lt;/td>
 &lt;td>套件管理器 install&lt;/td>
 &lt;td>上游同步&lt;/td>
 &lt;td>apt、yum、Homebrew、winget&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這五類常常組合出現（例如同時推 source、registry、binary release）。組合愈多、上游維護成本愈高，但下游能用的入口也愈廣。判讀訊號：&lt;/p>
&lt;ul>
&lt;li>README 寫的是 &lt;code>pip install x&lt;/code> → 屬 registry，去 PyPI 確認版本&lt;/li>
&lt;li>README 寫的是 &lt;code>curl ... /releases/latest/download/...sh | sh&lt;/code> → 屬 binary release + installer，去 GitHub Releases 確認 asset 存在&lt;/li>
&lt;li>README 寫的是 &lt;code>git clone&lt;/code> 後 &lt;code>make&lt;/code> → 只走 source，沒任何打包通道&lt;/li>
&lt;li>README 寫的是 &lt;code>docker pull ghcr.io/...&lt;/code> → 屬 container image，去 registry 確認 tag&lt;/li>
&lt;/ul>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>發版前要明確區分 breaking / feature / fix，避免版本語意錯置。&lt;/li>
&lt;li>發版流程應固定化（tag 規則、changelog 來源、artifact provenance）。&lt;/li>
&lt;li>對外 SDK 要維持 contract 測試，避免下游升級破壞。&lt;/li>
&lt;li>套件來源與 token 權限要最小化，並定期輪替。&lt;/li>
&lt;li>README 安裝段落寫的通道，發版前要實際跑過一次 — 「workflow 寫好」不代表「通道已上線」。&lt;/li>
&lt;/ul>
&lt;h2 id="安裝路徑分層">安裝路徑分層&lt;/h2>
&lt;p>Package release 的文件建議同時提供兩條安裝路徑，讓不同風險場景有對應入口。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>路徑類型&lt;/th>
 &lt;th>目標讀者&lt;/th>
 &lt;th>流程&lt;/th>
 &lt;th>風險控制&lt;/th>
 &lt;th>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>快速路徑&lt;/td>
 &lt;td>本機快速試用、低風險場景&lt;/td>
 &lt;td>一行安裝命令（例如 `curl &amp;hellip;&lt;/td>
 &lt;td>sh`）&lt;/td>
 &lt;td>速度優先，依賴上游發布品質&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可審計路徑&lt;/td>
 &lt;td>生產環境、受管設備、合規場景&lt;/td>
 &lt;td>下載產物 → 驗證 checksum/provenance → 執行&lt;/td>
 &lt;td>可追溯、可驗證、可稽核&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個分層能避免單一路徑綁死全部使用者。上游維護者要確保兩條路徑都可用，且文件清楚標示使用時機。可審計路徑的具體範例可直接沿用 &lt;a href="binary-release-and-installer/">Binary release 與 installer 模式&lt;/a> 的最小安全基線。&lt;/p></description><content:encoded><![CDATA[<p>Package / Library Release CI/CD 的核心責任是把可重用套件安全發佈到分發平台，並維持版本語意與相容承諾。它和應用部署不同，重點在版本管理、相容邊界、發佈簽章與撤版策略。</p>
<h2 id="場域定位">場域定位</h2>
<p>套件發佈常見於 NPM、PyPI、Maven、Crates 等生態。發布後會被多個下游專案依賴，因此每次 release 都是公共契約變更。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Package release 常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>package artifact、metadata、lock input</td>
          <td>產物是否可重現</td>
      </tr>
      <tr>
          <td>Validation</td>
          <td>API/ABI 相容性、smoke test、publish dry-run</td>
          <td>破壞性變更是否被識別</td>
      </tr>
      <tr>
          <td>Versioning</td>
          <td>semver、pre-release、changelog</td>
          <td>版本語意是否與變更一致</td>
      </tr>
      <tr>
          <td>Publish</td>
          <td>registry token、scope、provenance</td>
          <td>發版是否可追溯且權限正確</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>yank/deprecate/hotfix release</td>
          <td>事故時是否可快速止損</td>
      </tr>
  </tbody>
</table>
<h2 id="release-發布類型分類">Release 發布類型分類</h2>
<p>「發版」在中文討論裡常被當成單一動作，但實際上有五條互不重疊的通道，每條的觸發條件、產物形式、下游取用方式都不一樣。下游使用者讀 README 時若沒分清楚自己在走哪條通道，很容易踩到「文件寫了安裝指令，但對應通道還沒被建立」的情況。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>產物形式</th>
          <th>下游取用方式</th>
          <th>典型觸發</th>
          <th>代表生態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source release</td>
          <td>git tag + tarball</td>
          <td><code>git clone</code> 或 <code>go install</code> 後編譯</td>
          <td>tag push</td>
          <td>Go module、許多 OSS 函式庫</td>
      </tr>
      <tr>
          <td>Registry publish</td>
          <td>套件清單登錄</td>
          <td><code>npm install</code> / <code>pip install</code> 等</td>
          <td><code>publish</code> 指令</td>
          <td>npm、PyPI、crates.io、Maven</td>
      </tr>
      <tr>
          <td>Binary release</td>
          <td>預編譯多平台執行檔，掛在 GitHub Release</td>
          <td>下載 binary 或 installer script</td>
          <td>tag push</td>
          <td>cargo-dist、goreleaser 工具鏈</td>
      </tr>
      <tr>
          <td>Container image</td>
          <td>OCI image</td>
          <td><code>docker pull</code> / k8s manifest</td>
          <td>tag 或 commit</td>
          <td>Docker Hub、GHCR、ECR</td>
      </tr>
      <tr>
          <td>OS package</td>
          <td><code>.deb</code> / <code>.rpm</code> / Homebrew formula</td>
          <td>套件管理器 install</td>
          <td>上游同步</td>
          <td>apt、yum、Homebrew、winget</td>
      </tr>
  </tbody>
</table>
<p>這五類常常組合出現（例如同時推 source、registry、binary release）。組合愈多、上游維護成本愈高，但下游能用的入口也愈廣。判讀訊號：</p>
<ul>
<li>README 寫的是 <code>pip install x</code> → 屬 registry，去 PyPI 確認版本</li>
<li>README 寫的是 <code>curl ... /releases/latest/download/...sh | sh</code> → 屬 binary release + installer，去 GitHub Releases 確認 asset 存在</li>
<li>README 寫的是 <code>git clone</code> 後 <code>make</code> → 只走 source，沒任何打包通道</li>
<li>README 寫的是 <code>docker pull ghcr.io/...</code> → 屬 container image，去 registry 確認 tag</li>
</ul>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>發版前要明確區分 breaking / feature / fix，避免版本語意錯置。</li>
<li>發版流程應固定化（tag 規則、changelog 來源、artifact provenance）。</li>
<li>對外 SDK 要維持 contract 測試，避免下游升級破壞。</li>
<li>套件來源與 token 權限要最小化，並定期輪替。</li>
<li>README 安裝段落寫的通道，發版前要實際跑過一次 — 「workflow 寫好」不代表「通道已上線」。</li>
</ul>
<h2 id="安裝路徑分層">安裝路徑分層</h2>
<p>Package release 的文件建議同時提供兩條安裝路徑，讓不同風險場景有對應入口。</p>
<table>
  <thead>
      <tr>
          <th>路徑類型</th>
          <th>目標讀者</th>
          <th>流程</th>
          <th>風險控制</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>快速路徑</td>
          <td>本機快速試用、低風險場景</td>
          <td>一行安裝命令（例如 `curl &hellip;</td>
          <td>sh`）</td>
          <td>速度優先，依賴上游發布品質</td>
      </tr>
      <tr>
          <td>可審計路徑</td>
          <td>生產環境、受管設備、合規場景</td>
          <td>下載產物 → 驗證 checksum/provenance → 執行</td>
          <td>可追溯、可驗證、可稽核</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>這個分層能避免單一路徑綁死全部使用者。上游維護者要確保兩條路徑都可用，且文件清楚標示使用時機。可審計路徑的具體範例可直接沿用 <a href="binary-release-and-installer/">Binary release 與 installer 模式</a> 的最小安全基線。</p>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="binary-release-and-installer/">Binary release 與 installer 模式</a></td>
          <td>Tag-driven binary release</td>
          <td>GitHub Release + cargo-dist / goreleaser 的發版鏈路</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解 binary release + installer 模式（curl &hellip; | sh）：讀 <a href="binary-release-and-installer/">Binary release 與 installer 模式</a>。</li>
<li>供應鏈與產物可信度：讀 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance</a>。</li>
<li>版本契約：讀 <a href="/blog/backend/knowledge-cards/api-contract/" data-link-title="API Contract" data-link-desc="說明 request / response 邊界如何維持相容與可驗證">API Contract</a> 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">Contract</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item><item><title>本 blog 專案部署</title><link>https://tarrragon.github.io/blog/ci/blog-project-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/blog-project-deploy/</guid><description>&lt;p>本 blog 專案部署是前端靜態站部署的一個具體案例。這個資料夾只記錄本專案實際使用的 Hugo、Pagefind、Playwright、GitHub Pages 與 Claude workflow，不把這些細節當成所有 CI/CD 場域的通用規則。&lt;/p>
&lt;h2 id="專案定位">專案定位&lt;/h2>
&lt;p>本專案的部署產物是靜態網站。Hugo 負責產生 HTML，Pagefind 負責產生搜尋索引，GitHub Pages 負責 hosting，Playwright 負責驗證搜尋與版面行為。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文件&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="github-actions-workflows/">GitHub Actions workflow&lt;/a>&lt;/td>
 &lt;td>記錄本專案 &lt;code>.github/workflows/&lt;/code> 的實際設定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="與通用-cicd-的關係">與通用 CI/CD 的關係&lt;/h2>
&lt;p>本資料夾是實例層。通用 gate 原理、不同部署場域差異與失敗處理流程放在上層文章；本資料夾只回答「這個 blog 專案現在怎麼部署、失敗時要看哪裡」。術語定義統一回連 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理 CI/CD 章節的核心術語，讓流程文章專注在判讀與決策">CI 知識卡片&lt;/a>。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>本專案 workflow：讀 &lt;a href="github-actions-workflows/">GitHub Actions workflow&lt;/a>。&lt;/li>
&lt;li>前端部署通用注意事項：讀 &lt;a href="../frontend-deploy/">前端部署 CI/CD&lt;/a>。&lt;/li>
&lt;li>CI gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Markdown CI 規則：讀 &lt;a href="https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本 blog 專案部署是前端靜態站部署的一個具體案例。這個資料夾只記錄本專案實際使用的 Hugo、Pagefind、Playwright、GitHub Pages 與 Claude workflow，不把這些細節當成所有 CI/CD 場域的通用規則。</p>
<h2 id="專案定位">專案定位</h2>
<p>本專案的部署產物是靜態網站。Hugo 負責產生 HTML，Pagefind 負責產生搜尋索引，GitHub Pages 負責 hosting，Playwright 負責驗證搜尋與版面行為。</p>
<table>
  <thead>
      <tr>
          <th>文件</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="github-actions-workflows/">GitHub Actions workflow</a></td>
          <td>記錄本專案 <code>.github/workflows/</code> 的實際設定</td>
      </tr>
  </tbody>
</table>
<h2 id="與通用-cicd-的關係">與通用 CI/CD 的關係</h2>
<p>本資料夾是實例層。通用 gate 原理、不同部署場域差異與失敗處理流程放在上層文章；本資料夾只回答「這個 blog 專案現在怎麼部署、失敗時要看哪裡」。術語定義統一回連 <a href="/blog/ci/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理 CI/CD 章節的核心術語，讓流程文章專注在判讀與決策">CI 知識卡片</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>本專案 workflow：讀 <a href="github-actions-workflows/">GitHub Actions workflow</a>。</li>
<li>前端部署通用注意事項：讀 <a href="../frontend-deploy/">前端部署 CI/CD</a>。</li>
<li>CI gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Markdown CI 規則：讀 <a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">Blog Markdown 寫作規範與 mdtools 檢查</a>。</li>
</ul>
]]></content:encoded></item><item><title>Artifact 與可重播性</title><link>https://tarrragon.github.io/blog/ci/artifact-reproducibility/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/artifact-reproducibility/</guid><description>&lt;p>Artifact 可重播性的核心責任是讓每次發布都能追到同一份被驗證的產物。CI/CD 不只是在 runner 上跑命令；它要回答「測試通過的是哪份內容」「發布出去的是哪份內容」「事故時如何找回同一份內容」。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a> 是 CI/CD 流程中的交付單位。前端可能是 &lt;code>dist/&lt;/code>，後端可能是 binary 或 image，App 可能是 IPA / AAB，資料任務可能是 DAG 或 query package；不同形式的 artifact 都承擔同一個責任：把 source change 轉成可驗證、可保存、可推進的產物。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>能力&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build once&lt;/td>
 &lt;td>同一次變更只產生一次正式 artifact&lt;/td>
 &lt;td>build job 是否保存產物&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verify once&lt;/td>
 &lt;td>測試同一份 artifact&lt;/td>
 &lt;td>test job 是否 download artifact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff&lt;/a>&lt;/td>
 &lt;td>在 job / workflow 間交接產物&lt;/td>
 &lt;td>checksum、digest、version 是否一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Promote same artifact&lt;/td>
 &lt;td>staging / production 推進同一份&lt;/td>
 &lt;td>production 是否重新 build&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recover artifact&lt;/td>
 &lt;td>事故時找回上一份可用產物&lt;/td>
 &lt;td>retention、release、registry 是否保留&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build once 的責任是降低環境漂移。若 test job 與 deploy job 各自 build，一個 lockfile、環境變數或 base image 差異就能讓兩份產物不同；此時 CI 綠燈不再能證明 production 內容可信。&lt;/p>
&lt;p>Verify once 的責任是把測試結果綁到具體產物。測試應輸出 artifact identity，例如 checksum、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a>、release asset name 或 bundle version，讓 reviewer 能確認紅綠燈對應哪份內容。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff&lt;/a> 的責任是在 job 邊界保留身分。Upload / download artifact、registry digest、release asset、package registry 與 object storage 都可以做 handoff；重點是交接時沿用既有產物。&lt;/p>
&lt;p>Promote same artifact 的責任是讓環境差異集中在設定與流量。Staging 驗證過的 image、package 或 static artifact 應被推進到 production；若 production 重新 build，就需要重新驗證 production 那份產物。&lt;/p>
&lt;p>Recover artifact 的責任是讓 rollback 有實體目標。沒有保留 artifact 的 rollback 會變成「從舊 commit 重新 build」，這會受到依賴、base image、registry、toolchain 與時間漂移影響。&lt;/p>
&lt;h2 id="可重播性檢查">可重播性檢查&lt;/h2>
&lt;p>可重播性檢查的責任是確認產物身分與建置條件足夠明確。嚴格 reproducible build 很難在所有專案做到，但 CI/CD 至少要達到「同一次 workflow 的產物可以被查詢、保存、驗證與重新部署」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>檢查項&lt;/th>
 &lt;th>判讀問題&lt;/th>
 &lt;th>常見做法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source&lt;/td>
 &lt;td>artifact 對應哪個 commit&lt;/td>
 &lt;td>embed git SHA / release version&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency&lt;/td>
 &lt;td>dependency 是否固定&lt;/td>
 &lt;td>lockfile、base image digest&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Environment&lt;/td>
 &lt;td>build 環境是否固定&lt;/td>
 &lt;td>runner image、toolchain version&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Identity&lt;/td>
 &lt;td>artifact 是否有不可變身分&lt;/td>
 &lt;td>checksum、digest、signature&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>artifact 保留多久&lt;/td>
 &lt;td>release asset、registry retention&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Provenance&lt;/td>
 &lt;td>artifact 如何被產生&lt;/td>
 &lt;td>workflow run、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a>、attestation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表讓團隊知道自己目前在哪個成熟度。初期可以先做到 source、dependency、identity；高治理場景再補 SBOM、signature 與 provenance。&lt;/p></description><content:encoded><![CDATA[<p>Artifact 可重播性的核心責任是讓每次發布都能追到同一份被驗證的產物。CI/CD 不只是在 runner 上跑命令；它要回答「測試通過的是哪份內容」「發布出去的是哪份內容」「事故時如何找回同一份內容」。</p>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a> 是 CI/CD 流程中的交付單位。前端可能是 <code>dist/</code>，後端可能是 binary 或 image，App 可能是 IPA / AAB，資料任務可能是 DAG 或 query package；不同形式的 artifact 都承擔同一個責任：把 source change 轉成可驗證、可保存、可推進的產物。</p>
<table>
  <thead>
      <tr>
          <th>能力</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build once</td>
          <td>同一次變更只產生一次正式 artifact</td>
          <td>build job 是否保存產物</td>
      </tr>
      <tr>
          <td>Verify once</td>
          <td>測試同一份 artifact</td>
          <td>test job 是否 download artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff</a></td>
          <td>在 job / workflow 間交接產物</td>
          <td>checksum、digest、version 是否一致</td>
      </tr>
      <tr>
          <td>Promote same artifact</td>
          <td>staging / production 推進同一份</td>
          <td>production 是否重新 build</td>
      </tr>
      <tr>
          <td>Recover artifact</td>
          <td>事故時找回上一份可用產物</td>
          <td>retention、release、registry 是否保留</td>
      </tr>
  </tbody>
</table>
<p>Build once 的責任是降低環境漂移。若 test job 與 deploy job 各自 build，一個 lockfile、環境變數或 base image 差異就能讓兩份產物不同；此時 CI 綠燈不再能證明 production 內容可信。</p>
<p>Verify once 的責任是把測試結果綁到具體產物。測試應輸出 artifact identity，例如 checksum、<a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a>、release asset name 或 bundle version，讓 reviewer 能確認紅綠燈對應哪份內容。</p>
<p><a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact handoff</a> 的責任是在 job 邊界保留身分。Upload / download artifact、registry digest、release asset、package registry 與 object storage 都可以做 handoff；重點是交接時沿用既有產物。</p>
<p>Promote same artifact 的責任是讓環境差異集中在設定與流量。Staging 驗證過的 image、package 或 static artifact 應被推進到 production；若 production 重新 build，就需要重新驗證 production 那份產物。</p>
<p>Recover artifact 的責任是讓 rollback 有實體目標。沒有保留 artifact 的 rollback 會變成「從舊 commit 重新 build」，這會受到依賴、base image、registry、toolchain 與時間漂移影響。</p>
<h2 id="可重播性檢查">可重播性檢查</h2>
<p>可重播性檢查的責任是確認產物身分與建置條件足夠明確。嚴格 reproducible build 很難在所有專案做到，但 CI/CD 至少要達到「同一次 workflow 的產物可以被查詢、保存、驗證與重新部署」。</p>
<table>
  <thead>
      <tr>
          <th>檢查項</th>
          <th>判讀問題</th>
          <th>常見做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>artifact 對應哪個 commit</td>
          <td>embed git SHA / release version</td>
      </tr>
      <tr>
          <td>Dependency</td>
          <td>dependency 是否固定</td>
          <td>lockfile、base image digest</td>
      </tr>
      <tr>
          <td>Environment</td>
          <td>build 環境是否固定</td>
          <td>runner image、toolchain version</td>
      </tr>
      <tr>
          <td>Identity</td>
          <td>artifact 是否有不可變身分</td>
          <td>checksum、digest、signature</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>artifact 保留多久</td>
          <td>release asset、registry retention</td>
      </tr>
      <tr>
          <td>Provenance</td>
          <td>artifact 如何被產生</td>
          <td>workflow run、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a>、attestation</td>
      </tr>
  </tbody>
</table>
<p>這張表讓團隊知道自己目前在哪個成熟度。初期可以先做到 source、dependency、identity；高治理場景再補 SBOM、signature 與 provenance。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是讓「綠燈」失去指向性。當綠燈不知道對應哪份產物，CI/CD 只剩下命令執行紀錄。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>test 與 deploy 各自 build</td>
          <td>測試與發布內容漂移</td>
          <td>build once，artifact handoff</td>
      </tr>
      <tr>
          <td>rollback 重新 build 舊 commit</td>
          <td>舊 commit 可能產出不同內容</td>
          <td>保留上一份 release artifact</td>
      </tr>
      <tr>
          <td>只用人類可讀 tag</td>
          <td>tag 可被覆寫或語意不精準</td>
          <td>搭配 checksum / digest</td>
      </tr>
      <tr>
          <td>artifact retention 太短</td>
          <td>事故時找不到可回復版本</td>
          <td>對 release artifact 設長期保留</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Artifact 術語：讀 <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a>。</li>
<li>Artifact handoff：讀 <a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff</a>。</li>
<li>Gate 邊界：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>Flaky test 治理</title><link>https://tarrragon.github.io/blog/ci/flaky-test-governance/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/flaky-test-governance/</guid><description>&lt;p>Flaky test 治理的核心責任是保護 CI gate 的信任度。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky test&lt;/a> 會讓團隊開始用重跑取代判讀，最後讓紅燈失去阻擋意義。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Flaky test 是非決定性的 gate 訊號。它的危害不只在延遲 merge，而是在心理上訓練團隊忽略紅燈；當真回歸出現時，大家也可能先按 rerun。治理目標是把 flaky 分類、隔離、修復，並保持 required checks 的語意可信。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Detect&lt;/td>
 &lt;td>找出非決定性失敗&lt;/td>
 &lt;td>同 commit 重跑結果不一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Classify&lt;/td>
 &lt;td>區分測試、環境、資料與產品問題&lt;/td>
 &lt;td>failure pattern、log、trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Contain&lt;/td>
 &lt;td>降低對主線 gate 的污染&lt;/td>
 &lt;td>quarantine、owner、expiry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fix&lt;/td>
 &lt;td>修掉根因&lt;/td>
 &lt;td>timing、isolation、mock、resource&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Re-admit&lt;/td>
 &lt;td>恢復 gate 信任&lt;/td>
 &lt;td>連續穩定、觀測窗口、owner sign-off&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Detect 階段負責證明 flakiness。單次失敗不應直接貼 flaky 標籤；要看同一 commit、同一測試、相近環境下是否出現 pass / fail 不一致，並保存 log、trace、screenshot 或 seed。&lt;/p>
&lt;p>Classify 階段負責找根因方向。常見來源包含時間競態、測試順序依賴、共享狀態、外部服務、隨機資料、資源不足、瀏覽器 layout timing、網路模擬與 CI runner 差異；不同來源需要不同修法。&lt;/p>
&lt;p>Contain 階段負責保護主線。高價值但暫時 flaky 的測試可以進 quarantine workflow，但必須有 owner、issue、到期日與 replacement gate；直接從 required checks 移除而不追蹤，等於降低品質基線。&lt;/p>
&lt;p>Fix 階段負責消除非決定性。常見修法是移除固定 sleep、改用可觀察條件等待、隔離資料、固定 random seed、避免測試共享全域狀態、mock 不穩定外部依賴或調整資源限制。&lt;/p>
&lt;p>Re-admit 階段負責把測試放回 gate。測試修完後應在多次 workflow、不同 runner 或足夠時間窗口中穩定通過，再恢復 required checks；否則 gate 會反覆被污染。&lt;/p>
&lt;h2 id="分類矩陣">分類矩陣&lt;/h2>
&lt;p>分類矩陣的責任是讓 flaky issue 有明確修復路由。沒有分類時，團隊容易只留下「偶發失敗」這種不可執行標籤。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>常見訊號&lt;/th>
 &lt;th>修復方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Timing&lt;/td>
 &lt;td>sleep 不足、元素尚未出現&lt;/td>
 &lt;td>等待可觀察條件、移除固定 sleep&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared state&lt;/td>
 &lt;td>單跑通過、整批失敗&lt;/td>
 &lt;td>隔離資料、清理全域狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Order&lt;/td>
 &lt;td>測試順序改變後失敗&lt;/td>
 &lt;td>移除順序依賴、獨立 setup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>External&lt;/td>
 &lt;td>第三方 API、網路或時間服務不穩&lt;/td>
 &lt;td>mock、contract fixture、retry boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resource&lt;/td>
 &lt;td>CI runner 負載高時失敗&lt;/td>
 &lt;td>降低 parallelism、設定 resource&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Product race&lt;/td>
 &lt;td>真實功能存在競態&lt;/td>
 &lt;td>回到產品修復，不只改測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的邊界是：flaky 可能來自測試，也可能來自產品 race condition。若測試揭露的是產品 race condition，它應該被當成真 bug 處理。&lt;/p>
&lt;h2 id="quarantine-契約">Quarantine 契約&lt;/h2>
&lt;p>Quarantine 的責任是暫時隔離污染，並維持 gate 的長期品質基線。隔離測試時，要把責任、期限與替代風險控制寫清楚。&lt;/p>
&lt;ol>
&lt;li>每個 quarantine test 必須有 issue 與 owner。&lt;/li>
&lt;li>每個 issue 必須標明分類、失敗證據與修復方向。&lt;/li>
&lt;li>Required checks 若移除測試，要補 replacement gate 或風險說明。&lt;/li>
&lt;li>Quarantine workflow 仍需定期跑，並回報趨勢。&lt;/li>
&lt;li>到期未修復時要重新評估：修、刪、改寫或降級測試責任。&lt;/li>
&lt;/ol>
&lt;p>這個契約讓 quarantine 成為治理工具。沒有期限與 owner 的 quarantine 會變成測試墓地，讓主線 gate 永久失去一部分覆蓋。&lt;/p>
&lt;h2 id="tripwire">Tripwire&lt;/h2>
&lt;p>Tripwire 的責任是提示 flaky 已經從局部問題變成流程問題。&lt;/p>
&lt;ul>
&lt;li>團隊看到紅燈第一反應是 rerun：暫停重跑習慣，要求先分類失敗。&lt;/li>
&lt;li>同一測試一週內多次 quarantine：提升到測試架構或產品 race 檢討。&lt;/li>
&lt;li>Required checks 常因環境問題失敗：檢查 runner、resource、cache 與外部依賴。&lt;/li>
&lt;li>Flaky issue 沒 owner 或沒期限：把 quarantine 視為未完成修復，不視為已處理。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Flaky 術語：讀 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test&lt;/a>。&lt;/li>
&lt;li>Failure routing：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;li>Gate 邊界：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Flaky test 治理的核心責任是保護 CI gate 的信任度。<a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky test</a> 會讓團隊開始用重跑取代判讀，最後讓紅燈失去阻擋意義。</p>
<h2 id="概念定位">概念定位</h2>
<p>Flaky test 是非決定性的 gate 訊號。它的危害不只在延遲 merge，而是在心理上訓練團隊忽略紅燈；當真回歸出現時，大家也可能先按 rerun。治理目標是把 flaky 分類、隔離、修復，並保持 required checks 的語意可信。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Detect</td>
          <td>找出非決定性失敗</td>
          <td>同 commit 重跑結果不一致</td>
      </tr>
      <tr>
          <td>Classify</td>
          <td>區分測試、環境、資料與產品問題</td>
          <td>failure pattern、log、trace</td>
      </tr>
      <tr>
          <td>Contain</td>
          <td>降低對主線 gate 的污染</td>
          <td>quarantine、owner、expiry</td>
      </tr>
      <tr>
          <td>Fix</td>
          <td>修掉根因</td>
          <td>timing、isolation、mock、resource</td>
      </tr>
      <tr>
          <td>Re-admit</td>
          <td>恢復 gate 信任</td>
          <td>連續穩定、觀測窗口、owner sign-off</td>
      </tr>
  </tbody>
</table>
<p>Detect 階段負責證明 flakiness。單次失敗不應直接貼 flaky 標籤；要看同一 commit、同一測試、相近環境下是否出現 pass / fail 不一致，並保存 log、trace、screenshot 或 seed。</p>
<p>Classify 階段負責找根因方向。常見來源包含時間競態、測試順序依賴、共享狀態、外部服務、隨機資料、資源不足、瀏覽器 layout timing、網路模擬與 CI runner 差異；不同來源需要不同修法。</p>
<p>Contain 階段負責保護主線。高價值但暫時 flaky 的測試可以進 quarantine workflow，但必須有 owner、issue、到期日與 replacement gate；直接從 required checks 移除而不追蹤，等於降低品質基線。</p>
<p>Fix 階段負責消除非決定性。常見修法是移除固定 sleep、改用可觀察條件等待、隔離資料、固定 random seed、避免測試共享全域狀態、mock 不穩定外部依賴或調整資源限制。</p>
<p>Re-admit 階段負責把測試放回 gate。測試修完後應在多次 workflow、不同 runner 或足夠時間窗口中穩定通過，再恢復 required checks；否則 gate 會反覆被污染。</p>
<h2 id="分類矩陣">分類矩陣</h2>
<p>分類矩陣的責任是讓 flaky issue 有明確修復路由。沒有分類時，團隊容易只留下「偶發失敗」這種不可執行標籤。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>常見訊號</th>
          <th>修復方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Timing</td>
          <td>sleep 不足、元素尚未出現</td>
          <td>等待可觀察條件、移除固定 sleep</td>
      </tr>
      <tr>
          <td>Shared state</td>
          <td>單跑通過、整批失敗</td>
          <td>隔離資料、清理全域狀態</td>
      </tr>
      <tr>
          <td>Order</td>
          <td>測試順序改變後失敗</td>
          <td>移除順序依賴、獨立 setup</td>
      </tr>
      <tr>
          <td>External</td>
          <td>第三方 API、網路或時間服務不穩</td>
          <td>mock、contract fixture、retry boundary</td>
      </tr>
      <tr>
          <td>Resource</td>
          <td>CI runner 負載高時失敗</td>
          <td>降低 parallelism、設定 resource</td>
      </tr>
      <tr>
          <td>Product race</td>
          <td>真實功能存在競態</td>
          <td>回到產品修復，不只改測試</td>
      </tr>
  </tbody>
</table>
<p>這張表的邊界是：flaky 可能來自測試，也可能來自產品 race condition。若測試揭露的是產品 race condition，它應該被當成真 bug 處理。</p>
<h2 id="quarantine-契約">Quarantine 契約</h2>
<p>Quarantine 的責任是暫時隔離污染，並維持 gate 的長期品質基線。隔離測試時，要把責任、期限與替代風險控制寫清楚。</p>
<ol>
<li>每個 quarantine test 必須有 issue 與 owner。</li>
<li>每個 issue 必須標明分類、失敗證據與修復方向。</li>
<li>Required checks 若移除測試，要補 replacement gate 或風險說明。</li>
<li>Quarantine workflow 仍需定期跑，並回報趨勢。</li>
<li>到期未修復時要重新評估：修、刪、改寫或降級測試責任。</li>
</ol>
<p>這個契約讓 quarantine 成為治理工具。沒有期限與 owner 的 quarantine 會變成測試墓地，讓主線 gate 永久失去一部分覆蓋。</p>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提示 flaky 已經從局部問題變成流程問題。</p>
<ul>
<li>團隊看到紅燈第一反應是 rerun：暫停重跑習慣，要求先分類失敗。</li>
<li>同一測試一週內多次 quarantine：提升到測試架構或產品 race 檢討。</li>
<li>Required checks 常因環境問題失敗：檢查 runner、resource、cache 與外部依賴。</li>
<li>Flaky issue 沒 owner 或沒期限：把 quarantine 視為未完成修復，不視為已處理。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Flaky 術語：讀 <a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test</a>。</li>
<li>Failure routing：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>Gate 邊界：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
</ul>
]]></content:encoded></item><item><title>CI/CD 教學</title><link>https://tarrragon.github.io/blog/ci/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/</guid><description>&lt;p>CI/CD 教學的核心目標是把「變更如何被驗證、建置、交付」寫成可重播流程。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline&lt;/a> 負責驗證變更是否可信，&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/cd-pipeline/" data-link-title="CD Pipeline" data-link-desc="說明持續交付如何把已驗證產物推進到目標環境">CD Pipeline&lt;/a> 負責把可信 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a> 交付到目標環境；兩者共享 gate、artifact、環境與回復路徑，但不同部署場域的細節差異很大。&lt;/p>
&lt;p>CI/CD 的責任是提供一致的判讀入口。當 workflow 顯示失敗時，團隊需要能快速判斷是 lint、test、build、package、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff&lt;/a>、deploy 還是 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段出問題，並知道下一步該回到本機重現、修正、重新提交，還是暫停發布。&lt;/p>
&lt;h3 id="前置知識卡片">&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理 CI/CD 章節的核心術語，讓流程文章專注在判讀與決策">前置知識卡片&lt;/a>&lt;/h3>
&lt;p>用原子化卡片整理 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview Environment&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test&lt;/a> 等核心術語。流程文章專注情境判讀與決策順序，術語背景交由卡片維持一致。&lt;/p>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>&lt;/td>
 &lt;td>Failure routing&lt;/td>
 &lt;td>從失敗 workflow 判斷下一步路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>&lt;/td>
 &lt;td>Workflow boundary&lt;/td>
 &lt;td>說明 required checks、needs 與 artifact handoff&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="frontend-deploy/">前端部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Frontend deployment&lt;/td>
 &lt;td>靜態站、SPA、CDN 與 preview environment&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="backend-deploy/">後端部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Backend deployment&lt;/td>
 &lt;td>API / worker 的 migration、rollout 與 rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="app-deploy/">App 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>App deployment&lt;/td>
 &lt;td>mobile / desktop app 的簽章、審核與版本發布&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="docker-deploy/">Docker / Image 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Image deployment&lt;/td>
 &lt;td>image build、scan、tag、registry 與 runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="serverless-deploy/">Serverless 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Serverless deployment&lt;/td>
 &lt;td>function 版本、權限、事件觸發與 alias rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="data-pipeline-deploy/">Data Pipeline 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Data pipeline deployment&lt;/td>
 &lt;td>schema 相容、backfill、checkpoint 與 rerun&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="iac-platform-deploy/">IaC / Platform 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>IaC deployment&lt;/td>
 &lt;td>plan/apply、drift、state 與環境治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="desktop-client-deploy/">Desktop Client 部署 CI/CD&lt;/a>&lt;/td>
 &lt;td>Desktop client deployment&lt;/td>
 &lt;td>桌面安裝包簽章、公證、更新通道與回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="package-library-release/">Package / Library Release CI/CD&lt;/a>&lt;/td>
 &lt;td>Package release deployment&lt;/td>
 &lt;td>SDK / NPM / PyPI 的版本、契約與發版供應鏈治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="blog-project-deploy/">本 blog 專案部署&lt;/a>&lt;/td>
 &lt;td>Project case&lt;/td>
 &lt;td>Hugo、Pagefind、GitHub Pages 與本專案 workflow&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="artifact-reproducibility/">Artifact 與可重播性&lt;/a>&lt;/td>
 &lt;td>Artifact reproducibility&lt;/td>
 &lt;td>讓 CI 產物能被測試與發布共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="flaky-test-governance/">Flaky test 治理&lt;/a>&lt;/td>
 &lt;td>Flaky governance&lt;/td>
 &lt;td>把不穩定測試從雜訊變成可處理任務&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>學習路線先從失敗處理與 gate 邊界開始，因為 CI/CD 的價值會在紅燈時最清楚。當讀者能判讀失敗位置與下一步路由，再依部署場域進入前端、後端、App、Docker 或本 blog 專案案例。&lt;/p></description><content:encoded><![CDATA[<p>CI/CD 教學的核心目標是把「變更如何被驗證、建置、交付」寫成可重播流程。<a href="/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline</a> 負責驗證變更是否可信，<a href="/blog/ci/knowledge-cards/cd-pipeline/" data-link-title="CD Pipeline" data-link-desc="說明持續交付如何把已驗證產物推進到目標環境">CD Pipeline</a> 負責把可信 <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a> 交付到目標環境；兩者共享 gate、artifact、環境與回復路徑，但不同部署場域的細節差異很大。</p>
<p>CI/CD 的責任是提供一致的判讀入口。當 workflow 顯示失敗時，團隊需要能快速判斷是 lint、test、build、package、<a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff</a>、deploy 還是 <a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段出問題，並知道下一步該回到本機重現、修正、重新提交，還是暫停發布。</p>
<h3 id="前置知識卡片"><a href="/blog/ci/knowledge-cards/" data-link-title="Knowledge Cards" data-link-desc="用原子化卡片整理 CI/CD 章節的核心術語，讓流程文章專注在判讀與決策">前置知識卡片</a></h3>
<p>用原子化卡片整理 <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a>、<a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks</a>、<a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff</a>、<a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>、<a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview Environment</a>、<a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a>、<a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a>、<a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a>、<a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a>、<a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a>、<a href="/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel</a>、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a>、<a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a> 與 <a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test</a> 等核心術語。流程文章專注情境判讀與決策順序，術語背景交由卡片維持一致。</p>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="github-actions-failure-flow/">CI 失敗到修復發布流程</a></td>
          <td>Failure routing</td>
          <td>從失敗 workflow 判斷下一步路由</td>
      </tr>
      <tr>
          <td><a href="ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a></td>
          <td>Workflow boundary</td>
          <td>說明 required checks、needs 與 artifact handoff</td>
      </tr>
      <tr>
          <td><a href="frontend-deploy/">前端部署 CI/CD</a></td>
          <td>Frontend deployment</td>
          <td>靜態站、SPA、CDN 與 preview environment</td>
      </tr>
      <tr>
          <td><a href="backend-deploy/">後端部署 CI/CD</a></td>
          <td>Backend deployment</td>
          <td>API / worker 的 migration、rollout 與 rollback</td>
      </tr>
      <tr>
          <td><a href="app-deploy/">App 部署 CI/CD</a></td>
          <td>App deployment</td>
          <td>mobile / desktop app 的簽章、審核與版本發布</td>
      </tr>
      <tr>
          <td><a href="docker-deploy/">Docker / Image 部署 CI/CD</a></td>
          <td>Image deployment</td>
          <td>image build、scan、tag、registry 與 runtime</td>
      </tr>
      <tr>
          <td><a href="serverless-deploy/">Serverless 部署 CI/CD</a></td>
          <td>Serverless deployment</td>
          <td>function 版本、權限、事件觸發與 alias rollback</td>
      </tr>
      <tr>
          <td><a href="data-pipeline-deploy/">Data Pipeline 部署 CI/CD</a></td>
          <td>Data pipeline deployment</td>
          <td>schema 相容、backfill、checkpoint 與 rerun</td>
      </tr>
      <tr>
          <td><a href="iac-platform-deploy/">IaC / Platform 部署 CI/CD</a></td>
          <td>IaC deployment</td>
          <td>plan/apply、drift、state 與環境治理</td>
      </tr>
      <tr>
          <td><a href="desktop-client-deploy/">Desktop Client 部署 CI/CD</a></td>
          <td>Desktop client deployment</td>
          <td>桌面安裝包簽章、公證、更新通道與回退</td>
      </tr>
      <tr>
          <td><a href="package-library-release/">Package / Library Release CI/CD</a></td>
          <td>Package release deployment</td>
          <td>SDK / NPM / PyPI 的版本、契約與發版供應鏈治理</td>
      </tr>
      <tr>
          <td><a href="blog-project-deploy/">本 blog 專案部署</a></td>
          <td>Project case</td>
          <td>Hugo、Pagefind、GitHub Pages 與本專案 workflow</td>
      </tr>
      <tr>
          <td><a href="artifact-reproducibility/">Artifact 與可重播性</a></td>
          <td>Artifact reproducibility</td>
          <td>讓 CI 產物能被測試與發布共用</td>
      </tr>
      <tr>
          <td><a href="flaky-test-governance/">Flaky test 治理</a></td>
          <td>Flaky governance</td>
          <td>把不穩定測試從雜訊變成可處理任務</td>
      </tr>
  </tbody>
</table>
<p>學習路線先從失敗處理與 gate 邊界開始，因為 CI/CD 的價值會在紅燈時最清楚。當讀者能判讀失敗位置與下一步路由，再依部署場域進入前端、後端、App、Docker 或本 blog 專案案例。</p>
<h2 id="與其他教學的分工">與其他教學的分工</h2>
<p>CI/CD 教學負責日常工作流程與部署場域差異，Backend 可靠性模組負責系統層可靠性判斷。讀者想知道 workflow 失敗後怎麼修、發布 gate 怎麼切、前端與後端部署流程差在哪裡，讀本系列；想知道 CI 在 release gate、SLO、load test 與可靠性治理中的位置，回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">模組六：可靠性驗證流程</a>。</p>
<p>Go、Python 或其他語言教材只需要保留測試寫法與本機命令。當內容開始涉及 workflow event、required checks、preview deployment、container registry、mobile signing、artifact、cache 或 branch protection，就應該移到本系列，讓不同語言共用同一套 CI/CD 操作語意。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>GitHub Actions 紅燈後，不知道該看哪個 job。</li>
<li>本機測試通過，但 CI 失敗。</li>
<li>測試失敗後仍有部署 workflow 啟動。</li>
<li>deploy 失敗時，團隊分不清 build artifact、部署權限與測試 gate 的責任。</li>
<li>前端、後端、App 與 Docker 使用同一套發布說明，導致場域細節混在一起。</li>
<li>workflow 只有命令清單，沒有說明失敗後的處理路由與部署場域邊界。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想處理 GitHub Actions 紅燈：讀 <a href="github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
<li>想理解 CI gate 原理：讀 <a href="ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>想理解前端部署：讀 <a href="frontend-deploy/">前端部署 CI/CD</a>。</li>
<li>想理解後端部署：讀 <a href="backend-deploy/">後端部署 CI/CD</a>。</li>
<li>想理解 App 發布：讀 <a href="app-deploy/">App 部署 CI/CD</a>。</li>
<li>想理解 Docker / image 流程：讀 <a href="docker-deploy/">Docker / Image 部署 CI/CD</a>。</li>
<li>想理解 Serverless 發布：讀 <a href="serverless-deploy/">Serverless 部署 CI/CD</a>。</li>
<li>想理解資料處理任務發布：讀 <a href="data-pipeline-deploy/">Data Pipeline 部署 CI/CD</a>。</li>
<li>想理解 IaC / 平台變更發布：讀 <a href="iac-platform-deploy/">IaC / Platform 部署 CI/CD</a>。</li>
<li>想理解 Flutter/Electron/Tauri 類客戶端發布：讀 <a href="desktop-client-deploy/">Desktop Client 部署 CI/CD</a>。</li>
<li>想理解 SDK / NPM / PyPI 發版：讀 <a href="package-library-release/">Package / Library Release CI/CD</a>。</li>
<li>想維護本 blog 的 workflow：讀 <a href="blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>想讓測試與發布共用同一份產物：讀 <a href="artifact-reproducibility/">Artifact 與可重播性</a>。</li>
<li>想治理不穩定測試：讀 <a href="flaky-test-governance/">Flaky test 治理</a>。</li>
<li>想理解可靠性層的 CI 分層：讀 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">6.1 CI pipeline</a>。</li>
<li>想理解發布 gate：讀 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate 與變更節奏</a>。</li>
<li>想理解 infra 變更的 plan / apply 流程怎麼走 CI：讀 <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">Infra 走 PR 流程與自動化護欄</a>。</li>
</ul>
]]></content:encoded></item><item><title>Log 時間真空是 silent hang 訊號、happy log 是 anti-signal</title><link>https://tarrragon.github.io/blog/report/time-vacuum-in-logs-signals-silent-hang/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/time-vacuum-in-logs-signals-silent-hang/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 blog CI 的 Playwright install step 反覆 timeout 事件。Playwright 1.59 在 Node.js 24.16.0 上 extract-zip silent hang，表面看是「下載太慢 / timeout 太緊」，實際是 upstream regression。limitation：evidence 來自單一 CI 事件，但 silent hang 模式在 Docker build、cron job、database migration 等場景都出現過。&lt;/p>
&lt;p>完整 case study 見 &lt;a href="https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/" data-link-title="CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal" data-link-desc="CI step 跑很久才 timeout、最後一行卻是「下載 100% / build succeeded」這種 happy log 時回來。判讀：別急著加 timeout，先算最後一行到 cancel 的時間真空、確認是 silent hang，再用症狀詞查 upstream issue。同方向修法連 fail 2 次就是停手回資料層的訊號。">CI step silent hang&lt;/a>。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>非互動 process 的 log 輸出中，最後一行成功訊息（happy log）到被外部 cancel 之間的大段時間無輸出（時間真空），是 silent hang 的判讀訊號。&lt;/p>
&lt;p>技術人員習慣在 log 裡搜尋 error keyword 找失敗原因。但 silent hang 沒有 error keyword — process 沒 crash，只是不再做任何事。辨識 silent hang 需要轉換訊號類型：從「訊息內容」轉到「訊息時序」。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>CI step 跑了 15 分鐘被 timeout cancel。最後一行 log 是「chromium 下載 100% 完成」— 這是 happy log，直覺判斷是「下載慢、timeout 太緊」。加了 cache + bump timeout 到 25 分鐘，仍然頂到上限被 cancel。&lt;/p>
&lt;p>回頭看 detailed log 的 timestamp：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">2026-05-27T09:59:44.110Z | 100% of 170.4 MiB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2026-05-27T10:24:15.201Z ##[error]The operation was canceled.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>24 分 31 秒的時間真空。下載 2 秒完成，之後 process 完全沒有任何 log 輸出直到被 cancel。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>CI step timeout 時，先抓四個 timestamp 判斷是否 silent hang，再決定修法：&lt;/p>
&lt;ol>
&lt;li>Step 開始的 timestamp&lt;/li>
&lt;li>Step 結束（cancel / fail）的 timestamp&lt;/li>
&lt;li>最後一行有意義輸出的 timestamp&lt;/li>
&lt;li>計算 #3 到 #2 之間的時間真空&lt;/li>
&lt;/ol>
&lt;p>真空相對該 step 正常輸出節奏明顯異常（CI extract 類場景通常秒級輸出、真空超過數分鐘即可疑）且最後一行是 happy log → silent hang 嫌疑高 → 用症狀詞查 upstream issue tracker，不是加 timeout。&lt;/p>
&lt;p>三類 timeout 模式的修法不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>進度持續、最後階段到 timeout&lt;/td>
 &lt;td>時間真的不夠&lt;/td>
 &lt;td>bump timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有失敗訊息之後 timeout&lt;/td>
 &lt;td>code 邏輯錯&lt;/td>
 &lt;td>看訊息修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最後一行 happy log 之後大段時間真空&lt;/td>
 &lt;td>silent hang&lt;/td>
 &lt;td>查 upstream issue tracker&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>反覆加 timeout&lt;/strong>：每次都「差一點」（頂到上限），每次都以為「timeout 不夠」，實際上 process 永遠不會自己結束&lt;/li>
&lt;li>&lt;strong>Cache 是假瓶頸&lt;/strong>：直覺判斷「下載慢 → 加 cache」，但瓶頸在 extract hang（下載只花 2 秒）&lt;/li>
&lt;li>&lt;strong>False positive 越雕越精緻&lt;/strong>：cache key 調整、timeout 微調、retry 策略 — 每一步單看合理，合起來是把錯誤假設越做越細&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>兩個訊號同時出現時，應該先排除 silent hang 再提其他解法：&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 blog CI 的 Playwright install step 反覆 timeout 事件。Playwright 1.59 在 Node.js 24.16.0 上 extract-zip silent hang，表面看是「下載太慢 / timeout 太緊」，實際是 upstream regression。limitation：evidence 來自單一 CI 事件，但 silent hang 模式在 Docker build、cron job、database migration 等場景都出現過。</p>
<p>完整 case study 見 <a href="/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/" data-link-title="CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal" data-link-desc="CI step 跑很久才 timeout、最後一行卻是「下載 100% / build succeeded」這種 happy log 時回來。判讀：別急著加 timeout，先算最後一行到 cancel 的時間真空、確認是 silent hang，再用症狀詞查 upstream issue。同方向修法連 fail 2 次就是停手回資料層的訊號。">CI step silent hang</a>。</p>
<h2 id="核心原則">核心原則</h2>
<p>非互動 process 的 log 輸出中，最後一行成功訊息（happy log）到被外部 cancel 之間的大段時間無輸出（時間真空），是 silent hang 的判讀訊號。</p>
<p>技術人員習慣在 log 裡搜尋 error keyword 找失敗原因。但 silent hang 沒有 error keyword — process 沒 crash，只是不再做任何事。辨識 silent hang 需要轉換訊號類型：從「訊息內容」轉到「訊息時序」。</p>
<h2 id="情境">情境</h2>
<p>CI step 跑了 15 分鐘被 timeout cancel。最後一行 log 是「chromium 下載 100% 完成」— 這是 happy log，直覺判斷是「下載慢、timeout 太緊」。加了 cache + bump timeout 到 25 分鐘，仍然頂到上限被 cancel。</p>
<p>回頭看 detailed log 的 timestamp：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">2026-05-27T09:59:44.110Z  | 100% of 170.4 MiB
</span></span><span class="line"><span class="ln">2</span><span class="cl">2026-05-27T10:24:15.201Z  ##[error]The operation was canceled.</span></span></code></pre></div><p>24 分 31 秒的時間真空。下載 2 秒完成，之後 process 完全沒有任何 log 輸出直到被 cancel。</p>
<h2 id="理想做法">理想做法</h2>
<p>CI step timeout 時，先抓四個 timestamp 判斷是否 silent hang，再決定修法：</p>
<ol>
<li>Step 開始的 timestamp</li>
<li>Step 結束（cancel / fail）的 timestamp</li>
<li>最後一行有意義輸出的 timestamp</li>
<li>計算 #3 到 #2 之間的時間真空</li>
</ol>
<p>真空相對該 step 正常輸出節奏明顯異常（CI extract 類場景通常秒級輸出、真空超過數分鐘即可疑）且最後一行是 happy log → silent hang 嫌疑高 → 用症狀詞查 upstream issue tracker，不是加 timeout。</p>
<p>三類 timeout 模式的修法不同：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>根因</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>進度持續、最後階段到 timeout</td>
          <td>時間真的不夠</td>
          <td>bump timeout</td>
      </tr>
      <tr>
          <td>有失敗訊息之後 timeout</td>
          <td>code 邏輯錯</td>
          <td>看訊息修</td>
      </tr>
      <tr>
          <td>最後一行 happy log 之後大段時間真空</td>
          <td>silent hang</td>
          <td>查 upstream issue tracker</td>
      </tr>
  </tbody>
</table>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>反覆加 timeout</strong>：每次都「差一點」（頂到上限），每次都以為「timeout 不夠」，實際上 process 永遠不會自己結束</li>
<li><strong>Cache 是假瓶頸</strong>：直覺判斷「下載慢 → 加 cache」，但瓶頸在 extract hang（下載只花 2 秒）</li>
<li><strong>False positive 越雕越精緻</strong>：cache key 調整、timeout 微調、retry 策略 — 每一步單看合理，合起來是把錯誤假設越做越細</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>兩個訊號同時出現時，應該先排除 silent hang 再提其他解法：</p>
<ol>
<li>非互動 process 跑的時間接近或等於 timeout 上限（「頂到上限」模式）</li>
<li>最後一行 log 是成功訊息（下載完成 / build succeeded / tests passed）</li>
</ol>
<p>另一個後設訊號：同方向修法（加 timeout / 加 cache / 加 retry）2 次都仍頂到上限 — 這時候問題幾乎確定不是「時間不夠」。對應 <a href="/blog/report/failure-direction-pivot-point/" data-link-title="同方向反覆失敗的轉折點" data-link-desc="第 2 次同方向失敗就停下來回報「假設可能錯了、要不要換思路」、不要等第 4 次失敗才被使用者打斷。本文展開失敗計數與方向切換的判斷。">#20 同方向反覆失敗的轉折點</a>。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/failure-direction-pivot-point/" data-link-title="同方向反覆失敗的轉折點" data-link-desc="第 2 次同方向失敗就停下來回報「假設可能錯了、要不要換思路」、不要等第 4 次失敗才被使用者打斷。本文展開失敗計數與方向切換的判斷。">#20 同方向反覆失敗的轉折點</a>：本案例是 #20 在 CI timeout 場景的 evidence — 第二次 bump timeout 仍 fail 時就該停下來換思路</li>
<li>→ <a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a>：本卡的來源文章原本放在 <code>posts/</code>，實際是 debugging case study，搬到 <code>work-log/</code> 後從中抽出本卡，是 #199 拆分動作的實例</li>
</ul>
]]></content:encoded></item><item><title>CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal</title><link>https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/</link><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心議題&lt;/strong>：CI step 看起來「跑了很久才 timeout」時，要分辨「真的時間不夠」跟「silent hang 占滿時間」 — 兩者修法完全不同。Silent hang 的訊號是「最後一行 happy log 到 cancel 之間有大段時間真空」、不是「最後一行錯誤訊息」。第一次歸因錯誤後、第二次 fail 不該再加 timeout、該停下來重看 detailed log。
&lt;strong>案例骨幹&lt;/strong>：本 blog 的 Playwright CI 一直 timeout、初診「cache 缺失 + timeout 太緊」加了 cache + bump timeout、仍 timeout。重看 detailed log 發現 chromium 下載 2 秒完成、之後 24 分 31 秒&lt;strong>完全沒任何 log&lt;/strong> 才被 cancel — Playwright 1.59 在 Node.js 24.16.0 的 extract-zip regression（&lt;a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright#41000&lt;/a>、上游 &lt;a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487&lt;/a>）。升 Playwright 1.60.0 後該 step 從 25 分鐘卡死降到 22 秒。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-silent-hang-是-happy-log-的-anti-signal">1. Silent hang 是 happy log 的 anti-signal&lt;/h2>
&lt;p>CI step timeout 時、第一個本能是看「step 跑了多久」。15 分鐘 timeout 然後被砍、直覺判斷是「時間不夠、bump timeout」。這個直覺對應的失敗模式是「step 真的需要 16 分鐘才能跑完」。&lt;/p>
&lt;p>但有另一種失敗模式長得很像、修法完全不同：&lt;strong>silent hang&lt;/strong> — step 在某個點之後就不再輸出任何 log、process 仍在執行（沒有 crash）、直到外部 timeout 才被砍。表面看跟「時間不夠」一樣（step 跑很久才被 cancel）、但根因是 process 本身卡死、給多少時間都跑不完。&lt;/p>
&lt;p>辨識 silent hang 的關鍵訊號是「最後一行 happy log 到 cancel 訊息之間有大段時間真空」。&lt;strong>「Happy log」指的是看起來成功的訊息&lt;/strong>（例：下載 100% 完成、build succeeded、X tests passed）— 這類訊息特別會誤導判斷、因為它讓人以為任務在進展。Silent hang 開始之前的最後一行通常正是這種 happy log、是正常結束訊號的反面。&lt;/p>
&lt;h3 id="三類-timeout-模式的對照">三類 timeout 模式的對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>可能根因&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>整個 step 進度持續、最後階段加速到 timeout&lt;/td>
 &lt;td>時間真的不夠&lt;/td>
 &lt;td>bump timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有失敗訊息（exception / non-zero exit）之後 timeout&lt;/td>
 &lt;td>code 邏輯錯&lt;/td>
 &lt;td>看訊息修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>最後一行 log 之後有大段時間真空、然後 cancel&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>Silent hang&lt;/strong>、可能 upstream bug&lt;/td>
 &lt;td>&lt;strong>查 upstream issue tracker、不是加 timeout&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第三種最容易誤判、因為「log 之間沒輸出」沒被當成訊號 — 但&lt;strong>訊息真空本身就是訊號&lt;/strong>。寫 debug log 的人會記得補 error 訊息、但 silent hang 通常發生在工具內部的某個沒輸出 log 的等待點、所以沒有 error 訊息可看。&lt;/p>
&lt;hr>
&lt;h2 id="2-為什麼cache-缺失--bump-timeout的初診是-false-positive">2. 為什麼「cache 缺失 + bump timeout」的初診是 false positive&lt;/h2>
&lt;p>第一次看 CI fail log 時、有三件容易抓到的事：&lt;/p>
&lt;ol>
&lt;li>workflow YAML 裡的 &lt;code>timeout-minutes: 15&lt;/code>&lt;/li>
&lt;li>step 跑了 &lt;code>15m 6s&lt;/code>（幾乎等於 timeout 上限）&lt;/li>
&lt;li>step 名稱是 &lt;code>Install Playwright browsers&lt;/code>（要下載 170 MiB）&lt;/li>
&lt;/ol>
&lt;p>直覺合成的結論：「cache 缺失 + timeout 太緊」。這結論看起來「應該對」 — 因為這兩個都是「Install Playwright browsers」眾所周知的優化點。修法：加 &lt;code>actions/cache&lt;/code> + bump timeout 25 min。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心議題</strong>：CI step 看起來「跑了很久才 timeout」時，要分辨「真的時間不夠」跟「silent hang 占滿時間」 — 兩者修法完全不同。Silent hang 的訊號是「最後一行 happy log 到 cancel 之間有大段時間真空」、不是「最後一行錯誤訊息」。第一次歸因錯誤後、第二次 fail 不該再加 timeout、該停下來重看 detailed log。
<strong>案例骨幹</strong>：本 blog 的 Playwright CI 一直 timeout、初診「cache 缺失 + timeout 太緊」加了 cache + bump timeout、仍 timeout。重看 detailed log 發現 chromium 下載 2 秒完成、之後 24 分 31 秒<strong>完全沒任何 log</strong> 才被 cancel — Playwright 1.59 在 Node.js 24.16.0 的 extract-zip regression（<a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright#41000</a>、上游 <a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487</a>）。升 Playwright 1.60.0 後該 step 從 25 分鐘卡死降到 22 秒。</p></blockquote>
<hr>
<h2 id="1-silent-hang-是-happy-log-的-anti-signal">1. Silent hang 是 happy log 的 anti-signal</h2>
<p>CI step timeout 時、第一個本能是看「step 跑了多久」。15 分鐘 timeout 然後被砍、直覺判斷是「時間不夠、bump timeout」。這個直覺對應的失敗模式是「step 真的需要 16 分鐘才能跑完」。</p>
<p>但有另一種失敗模式長得很像、修法完全不同：<strong>silent hang</strong> — step 在某個點之後就不再輸出任何 log、process 仍在執行（沒有 crash）、直到外部 timeout 才被砍。表面看跟「時間不夠」一樣（step 跑很久才被 cancel）、但根因是 process 本身卡死、給多少時間都跑不完。</p>
<p>辨識 silent hang 的關鍵訊號是「最後一行 happy log 到 cancel 訊息之間有大段時間真空」。<strong>「Happy log」指的是看起來成功的訊息</strong>（例：下載 100% 完成、build succeeded、X tests passed）— 這類訊息特別會誤導判斷、因為它讓人以為任務在進展。Silent hang 開始之前的最後一行通常正是這種 happy log、是正常結束訊號的反面。</p>
<h3 id="三類-timeout-模式的對照">三類 timeout 模式的對照</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能根因</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整個 step 進度持續、最後階段加速到 timeout</td>
          <td>時間真的不夠</td>
          <td>bump timeout</td>
      </tr>
      <tr>
          <td>有失敗訊息（exception / non-zero exit）之後 timeout</td>
          <td>code 邏輯錯</td>
          <td>看訊息修</td>
      </tr>
      <tr>
          <td><strong>最後一行 log 之後有大段時間真空、然後 cancel</strong></td>
          <td><strong>Silent hang</strong>、可能 upstream bug</td>
          <td><strong>查 upstream issue tracker、不是加 timeout</strong></td>
      </tr>
  </tbody>
</table>
<p>第三種最容易誤判、因為「log 之間沒輸出」沒被當成訊號 — 但<strong>訊息真空本身就是訊號</strong>。寫 debug log 的人會記得補 error 訊息、但 silent hang 通常發生在工具內部的某個沒輸出 log 的等待點、所以沒有 error 訊息可看。</p>
<hr>
<h2 id="2-為什麼cache-缺失--bump-timeout的初診是-false-positive">2. 為什麼「cache 缺失 + bump timeout」的初診是 false positive</h2>
<p>第一次看 CI fail log 時、有三件容易抓到的事：</p>
<ol>
<li>workflow YAML 裡的 <code>timeout-minutes: 15</code></li>
<li>step 跑了 <code>15m 6s</code>（幾乎等於 timeout 上限）</li>
<li>step 名稱是 <code>Install Playwright browsers</code>（要下載 170 MiB）</li>
</ol>
<p>直覺合成的結論：「cache 缺失 + timeout 太緊」。這結論看起來「應該對」 — 因為這兩個都是「Install Playwright browsers」眾所周知的優化點。修法：加 <code>actions/cache</code> + bump timeout 25 min。</p>
<p>修完仍 timeout、但這次跑 <code>25m 6s</code>（一樣頂到上限）。</p>
<p><strong>這時的訊號應該是「同樣的 step 在 1.67 倍的 timeout 下仍頂到上限」</strong> — 如果是時間不夠、bump 之後該往中間靠（譬如完成在 18-20 min）；如果一直頂到上限、意思是 step 不會自己結束、是 hang。</p>
<p>但初診時很容易略過這個訊號、轉而繼續想「是不是 cache step 設定有問題？」。這個歸因方向是錯的、因為前置假設「cache 是瓶頸」本身就沒驗證過。</p>
<h3 id="一輪-false-positive-的-anatomy">一輪 false positive 的 anatomy</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>容易做的</th>
          <th>該做的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看到 timeout</td>
          <td>假設「時間不夠」</td>
          <td>先區分「時間不夠」vs「silent hang」</td>
      </tr>
      <tr>
          <td>看 high-level log</td>
          <td>假設「下載慢」</td>
          <td>應該看下載前後 timestamp 比對</td>
      </tr>
      <tr>
          <td>提解法</td>
          <td>加 cache + bump timeout</td>
          <td>應該先確認瓶頸真的在下載</td>
      </tr>
      <tr>
          <td>解法仍 fail</td>
          <td>假設「cache 沒 hit」</td>
          <td>應該意識到「同個 step 又頂到上限」是 hang 訊號</td>
      </tr>
  </tbody>
</table>
<p>每一步單看都合理、合起來就是把 false positive 越雕越精緻。這個 anatomy 對任何「初診沒驗證就改」的場景都適用、不限 CI。</p>
<hr>
<h2 id="3-wrap-的-r-在第二次-fail-時是-stop-訊號">3. WRAP 的 R 在第二次 fail 時是 stop 訊號</h2>
<p>WRAP 決策框架的 R（Reality Test）原則是「需要什麼事證才能證明這個方法可行？」。它不只是決策前的檢查、更是<strong>連續失敗後的 stop 訊號</strong>。</p>
<p>第二次 fail 時、繼續同方向加 timeout 是自動駕駛模式。WRAP 在這個位置該提醒的事：</p>
<ul>
<li>「兩次同類修法都沒解、是不是前置假設錯了？」</li>
<li>「我有沒有資料去判斷真正卡哪？」（資料充足度閘門）</li>
<li>「同類問題的 base rate 是什麼？」（基本率思考）</li>
</ul>
<p><strong>Stop 訊號的觸發條件是「同方向修法連續 fail 2 次」、不是「fail 3 次」</strong>。第二次就該回到資料層；第三次已經是浪費 cycle 而且強化錯誤假設。</p>
<p>實際上第二次 fail 後做的對的事是停下來、grep detailed log 的 timestamp 序列、發現「下載完成」跟「cancel」之間有 24 分鐘空白 — 這時才確認是 silent hang。如果第二次沒做這個轉折、第三次大概率是「換更大的 timeout」或「換不同的 cache key」、仍 fail。</p>
<hr>
<h2 id="4-detailed-log-的關鍵讀法找沒輸出的時間段">4. Detailed log 的關鍵讀法：找「沒輸出的時間段」</h2>
<p>CI 平台的 step log 通常很長、人眼掃容易跳過。看 silent hang 嫌疑時、讀法不是順序讀、是抓四個 timestamp：</p>
<ol>
<li><strong>Step 開始的 timestamp</strong>（log header 通常有）</li>
<li><strong>Step 結束（cancel / fail）的 timestamp</strong></li>
<li><strong>最後一行有意義輸出的 timestamp</strong></li>
<li>計算 #3 到 #2 之間的時間真空</li>
</ol>
<p>真空夠大（&gt; 1 分鐘）+ #3 是 happy log = silent hang 嫌疑高。</p>
<p>GitHub Actions 用 <code>gh</code> CLI 的具體做法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 取某個 step 的所有 log（filter step 名稱）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gh run view &lt;run-id&gt; --log --job &lt;job-id&gt; <span class="p">|</span> rg <span class="s2">&#34;Install Playwright browsers&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 抓最後幾行看真空尾巴</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gh run view &lt;run-id&gt; --log --job &lt;job-id&gt; <span class="p">|</span> rg <span class="s2">&#34;Install Playwright browsers&#34;</span> <span class="p">|</span> tail -3</span></span></code></pre></div><p>本案例的最後 3 行（簡化過）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">2026-05-27T09:59:44.110Z  | 100% of 170.4 MiB
</span></span><span class="line"><span class="ln">2</span><span class="cl">2026-05-27T10:24:15.201Z  ##[error]The operation was canceled.</span></span></code></pre></div><p>24 分 31 秒真空、最後一行 happy log 是「下載 100% 完成」 — silent hang 確認。</p>
<p>這個讀法的核心是「<strong>時間真空優先於訊息內容</strong>」。技術人員習慣讀訊息內容找 error keyword、但 silent hang 沒有 error keyword 可找、只有時間真空。轉個訊號類型才看得到。</p>
<hr>
<h2 id="5-upstream-issue-搜尋的優先序">5. Upstream issue 搜尋的優先序</h2>
<p>Silent hang 確認後、下一步通常<strong>不是繼續 reason 根因</strong>、是去查 upstream issue tracker。Silent hang 多半是工具 / 依賴的 bug、而非自己 config 錯 — 因為 config 錯通常有 error message、不會 silent。</p>
<p>查詢策略：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">gh api <span class="s1">&#39;search/issues?q=repo:&lt;upstream&gt;/&lt;repo&gt;+&lt;symptom keywords&gt;+is:issue&amp;per_page=10&amp;sort=updated&#39;</span></span></span></code></pre></div><p>關鍵是 <strong>keyword 選擇用「症狀詞」而不是「猜測詞」</strong>。症狀詞描述讀者實際觀察到的現象（<code>hangs after download</code>、<code>stuck during extract</code>），猜測詞描述讀者推測的根因（<code>slow</code>、<code>timeout</code>、<code>network issue</code>）。猜測詞會找到大量無關 issue；症狀詞通常直接命中。</p>
<p>本案例查詢 <code>playwright install hangs chromium</code> 第二筆結果就是 issue #41000、標題完全匹配「<code>playwright install chromium</code> hangs after download completes on Node.js 24.16.0 (extract-zip)」。Issue 詳情指向上游 <a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487</a>、給出兩個 workaround（升 Playwright 1.60.0 或 pin Node 24.15.0）。從查詢到確認根因、全程不到 5 分鐘。</p>
<h3 id="為什麼-issue-tracker-該優先於-self-reasoning">為什麼 issue tracker 該優先於 self-reasoning</h3>
<p>技術人員的 instinct 是「自己想出根因」。但 CI silent hang 這類問題、根因通常在工具版本、runtime 版本、OS、container image 的微妙交互、不在自己的 codebase。<strong>Reasoning 找不到的東西、社群 issue tracker 經常已經有人回報過</strong>。</p>
<p>「先 reason 再查」跟「先查再 reason」的取捨：</p>
<table>
  <thead>
      <tr>
          <th>問題範圍</th>
          <th>哪個優先</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自己 codebase 內的邏輯 bug</td>
          <td>reason</td>
          <td>自己最熟、reasoning 通常較快</td>
      </tr>
      <tr>
          <td>Upstream tool / runtime / OS / container 範圍</td>
          <td>查 issue</td>
          <td>自己沒上游知識、reasoning 容易卡在錯誤前置假設</td>
      </tr>
      <tr>
          <td>兩者交界（自己 config 觸發 upstream bug）</td>
          <td>並行</td>
          <td>先查找 known issue、同時 reason 自己 config</td>
      </tr>
  </tbody>
</table>
<p>Silent hang 預設屬於第二類、應該優先查 issue tracker。</p>
<hr>
<h2 id="6-整合訊號--行動-mapping">6. 整合：訊號 → 行動 mapping</h2>
<p>把本案例的經驗整理成可重用的訊號表：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Step timeout 且最後一行是 happy log</td>
          <td>計算 timestamp 真空、確認是否 silent hang</td>
      </tr>
      <tr>
          <td>同方向修法 2 次都 fail</td>
          <td>停止、回到資料層、不再加 timeout / retry</td>
      </tr>
      <tr>
          <td>Silent hang 確認</td>
          <td>用症狀詞查 upstream issue tracker</td>
      </tr>
      <tr>
          <td>Issue 命中且有 workaround</td>
          <td>套 workaround、不要先 reason</td>
      </tr>
      <tr>
          <td>Issue 沒命中</td>
          <td>才回到 self-debug、加 verbose log（<code>DEBUG=</code> env）</td>
      </tr>
  </tbody>
</table>
<p>這張表的順序很重要：每一步的「該做的事」是下一步的「前置條件」。略過任一步、後面的判斷會建立在錯誤假設上。</p>
<hr>
<h2 id="適用範圍">適用範圍</h2>
<p>「Silent log 是 happy log 的 anti-signal」這個原則對所有非互動 process（CI、cron job、background worker、container init）都適用：</p>
<ul>
<li><strong>Docker build 卡住</strong>（特別是 RUN apt-get / npm install / pip install）— 同類 silent hang 模式</li>
<li><strong>CI cache restore 卡住</strong> — 大量小檔案的 cache 操作可能 silent hang</li>
<li><strong>Database migration 卡住</strong> — schema 變更 + 長 transaction 可能 silent hang</li>
<li><strong>任何 process 跑時間接近 timeout 上限被 cancel</strong> — 先檢查是否 silent hang 才提解法</li>
</ul>
<p>「WRAP R 在第二次 fail 時是 stop 訊號」這條原則不限 CI、適用所有「同方向修法重複 fail」的場景：debug、設定調校、效能優化。</p>
<hr>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright issue #41000</a> — 本案例的 upstream issue（Playwright 1.57-1.59 在 Node 24.16.0 extract-zip hang）</li>
<li><a href="https://github.com/nodejs/node/issues/63487">nodejs/node issue #63487</a> — Node 24.16 extract-zip / yauzl regression 上游</li>
<li>同 blog 文章：<a href="/blog/skills/wrap-decision/" data-link-title="WRAP 決策框架 — 認知偏誤防護與決策品質" data-link-desc="WRAP 決策框架的 blog 好讀版：用錨點確認、資料充足度、選項擴增、現實檢驗、機會成本、行前預想與絆腳索防止自動駕駛式決策。">WRAP 決策框架的 R 階段操作</a> — Reality Test 詳細用法</li>
</ul>
]]></content:encoded></item><item><title>Knowledge Cards</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/</guid><description>&lt;p>CI/CD 知識卡片的核心責任是建立共同語言。流程文章會使用 pipeline、gate、artifact、rollout、rollback、environment protection 等術語；卡片負責定義它們在系統中的位置、可觀察訊號與設計責任。&lt;/p>
&lt;h2 id="核心術語">核心術語&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡片&lt;/th>
 &lt;th>核心問題&lt;/th>
 &lt;th>常見出現位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline&lt;/a>&lt;/td>
 &lt;td>變更如何在合併前被自動驗證&lt;/td>
 &lt;td>lint、test、build、security check&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/cd-pipeline/" data-link-title="CD Pipeline" data-link-desc="說明持續交付如何把已驗證產物推進到目標環境">CD Pipeline&lt;/a>&lt;/td>
 &lt;td>驗證後產物如何被安全推進到目標環境&lt;/td>
 &lt;td>deploy、promotion、release workflow&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks&lt;/a>&lt;/td>
 &lt;td>PR 合併條件如何由檢查結果定義&lt;/td>
 &lt;td>branch protection、status checks&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a>&lt;/td>
 &lt;td>交付產物如何被追溯、保存與發布&lt;/td>
 &lt;td>build output、image、app bundle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff&lt;/a>&lt;/td>
 &lt;td>測試與發布如何共用同一份產物&lt;/td>
 &lt;td>build artifact、package、deploy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration&lt;/a>&lt;/td>
 &lt;td>狀態變更如何在相容窗口內受控推進&lt;/td>
 &lt;td>schema change、backfill、release&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/branch-protection/" data-link-title="Branch Protection" data-link-desc="說明主線分支如何以規則保護合併與發布前置條件">Branch Protection&lt;/a>&lt;/td>
 &lt;td>主線合併條件如何由規則強制保護&lt;/td>
 &lt;td>required checks、review policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check&lt;/a>&lt;/td>
 &lt;td>部署放行如何區分存活與可接流量訊號&lt;/td>
 &lt;td>rollout、probe、traffic switch&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry&lt;/a>&lt;/td>
 &lt;td>image 供應鏈如何被保存與推進&lt;/td>
 &lt;td>push、retention、promotion&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing&lt;/a>&lt;/td>
 &lt;td>行動與桌面發版能力如何由簽章維持&lt;/td>
 &lt;td>certificate、profile、keystore&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test&lt;/a>&lt;/td>
 &lt;td>非決定性測試如何影響 gate 信任度&lt;/td>
 &lt;td>rerun noise、test governance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a>&lt;/td>
 &lt;td>目標環境如何設置審核與發布保護&lt;/td>
 &lt;td>production、staging、review gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview Environment&lt;/a>&lt;/td>
 &lt;td>PR 變更如何在隔離環境中被提前驗證&lt;/td>
 &lt;td>frontend preview URL、review app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy&lt;/a>&lt;/td>
 &lt;td>新版本如何分批推進以控制風險&lt;/td>
 &lt;td>rolling、canary、phased rollout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>發布異常時如何回到已知可用狀態&lt;/td>
 &lt;td>deploy rollback、hotfix、forward fix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/deployment-dry-run/" data-link-title="Deployment Dry Run" data-link-desc="說明發布前如何用預演檢查部署條件與風險">Deployment Dry Run&lt;/a>&lt;/td>
 &lt;td>發布前如何先驗證流程條件與權限&lt;/td>
 &lt;td>preflight check、artifact check、permission&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill&lt;/a>&lt;/td>
 &lt;td>歷史資料如何受控補算&lt;/td>
 &lt;td>migration、data pipeline、repair&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint&lt;/a>&lt;/td>
 &lt;td>長時間任務如何保存接續位置&lt;/td>
 &lt;td>backfill、stream processor、rerun&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun&lt;/a>&lt;/td>
 &lt;td>重跑流程如何避免擴大副作用&lt;/td>
 &lt;td>flaky test、data repair、pipeline recovery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a>&lt;/td>
 &lt;td>container image 如何取得不可變身分&lt;/td>
 &lt;td>registry、scan、runtime handoff&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a>&lt;/td>
 &lt;td>artifact 內含元件如何被揭露&lt;/td>
 &lt;td>image scan、release evidence、compliance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel&lt;/a>&lt;/td>
 &lt;td>版本如何依使用者範圍分流&lt;/td>
 &lt;td>app、desktop、beta、stable&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed&lt;/a>&lt;/td>
 &lt;td>已安裝客戶端如何取得新版本&lt;/td>
 &lt;td>desktop auto-update、rollback channel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>&lt;/td>
 &lt;td>真實環境與 IaC 宣告如何分叉&lt;/td>
 &lt;td>Terraform、Pulumi、manual hotfix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock&lt;/a>&lt;/td>
 &lt;td>IaC apply 如何避免併發覆寫 state&lt;/td>
 &lt;td>Terraform backend、workspace、apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias&lt;/a>&lt;/td>
 &lt;td>serverless 入口如何指向特定版本&lt;/td>
 &lt;td>alias rollback、traffic shift&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source&lt;/a>&lt;/td>
 &lt;td>事件來源如何影響 retry 與回復&lt;/td>
 &lt;td>queue、topic、HTTP trigger、scheduler&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>卡片與流程文章分工清楚。卡片負責名詞與邊界，流程文章負責情境判讀與操作路由。&lt;/p></description><content:encoded><![CDATA[<p>CI/CD 知識卡片的核心責任是建立共同語言。流程文章會使用 pipeline、gate、artifact、rollout、rollback、environment protection 等術語；卡片負責定義它們在系統中的位置、可觀察訊號與設計責任。</p>
<h2 id="核心術語">核心術語</h2>
<table>
  <thead>
      <tr>
          <th>卡片</th>
          <th>核心問題</th>
          <th>常見出現位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/ci-pipeline/" data-link-title="CI Pipeline" data-link-desc="說明持續整合如何在合併前自動驗證變更品質與相容性">CI Pipeline</a></td>
          <td>變更如何在合併前被自動驗證</td>
          <td>lint、test、build、security check</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/cd-pipeline/" data-link-title="CD Pipeline" data-link-desc="說明持續交付如何把已驗證產物推進到目標環境">CD Pipeline</a></td>
          <td>驗證後產物如何被安全推進到目標環境</td>
          <td>deploy、promotion、release workflow</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/required-checks/" data-link-title="Required Checks" data-link-desc="說明 pull request 的必要檢查如何作為合併 gate">Required Checks</a></td>
          <td>PR 合併條件如何由檢查結果定義</td>
          <td>branch protection、status checks</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a></td>
          <td>交付產物如何被追溯、保存與發布</td>
          <td>build output、image、app bundle</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff</a></td>
          <td>測試與發布如何共用同一份產物</td>
          <td>build artifact、package、deploy</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明資料或結構變更如何在服務不中斷前提下受控推進">Migration</a></td>
          <td>狀態變更如何在相容窗口內受控推進</td>
          <td>schema change、backfill、release</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/branch-protection/" data-link-title="Branch Protection" data-link-desc="說明主線分支如何以規則保護合併與發布前置條件">Branch Protection</a></td>
          <td>主線合併條件如何由規則強制保護</td>
          <td>required checks、review policy</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/readiness-health-check/" data-link-title="Readiness / Health Check" data-link-desc="說明服務存活與可接流量判斷在部署中的不同責任">Readiness / Health Check</a></td>
          <td>部署放行如何區分存活與可接流量訊號</td>
          <td>rollout、probe、traffic switch</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a></td>
          <td>image 供應鏈如何被保存與推進</td>
          <td>push、retention、promotion</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/app-signing/" data-link-title="App Signing" data-link-desc="說明行動與桌面應用的簽章憑證如何影響發布能力">App Signing</a></td>
          <td>行動與桌面發版能力如何由簽章維持</td>
          <td>certificate、profile、keystore</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/flaky-test/" data-link-title="Flaky Test" data-link-desc="說明非決定性測試如何降低 CI gate 信任度與治理方式">Flaky Test</a></td>
          <td>非決定性測試如何影響 gate 信任度</td>
          <td>rerun noise、test governance</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a></td>
          <td>目標環境如何設置審核與發布保護</td>
          <td>production、staging、review gate</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview Environment</a></td>
          <td>PR 變更如何在隔離環境中被提前驗證</td>
          <td>frontend preview URL、review app</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollout-strategy/" data-link-title="Rollout Strategy" data-link-desc="說明新版本如何以可控節奏推進到全部流量">Rollout Strategy</a></td>
          <td>新版本如何分批推進以控制風險</td>
          <td>rolling、canary、phased rollout</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>發布異常時如何回到已知可用狀態</td>
          <td>deploy rollback、hotfix、forward fix</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/deployment-dry-run/" data-link-title="Deployment Dry Run" data-link-desc="說明發布前如何用預演檢查部署條件與風險">Deployment Dry Run</a></td>
          <td>發布前如何先驗證流程條件與權限</td>
          <td>preflight check、artifact check、permission</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明資料處理與 migration 中如何受控補算歷史資料">Backfill</a></td>
          <td>歷史資料如何受控補算</td>
          <td>migration、data pipeline、repair</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間任務如何記錄進度以支援接續、重跑與事故修復">Checkpoint</a></td>
          <td>長時間任務如何保存接續位置</td>
          <td>backfill、stream processor、rerun</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rerun/" data-link-title="Rerun" data-link-desc="說明 CI/CD 與 data pipeline 中重跑任務前需要判斷的輸出語意與副作用">Rerun</a></td>
          <td>重跑流程如何避免擴大副作用</td>
          <td>flaky test、data repair、pipeline recovery</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a></td>
          <td>container image 如何取得不可變身分</td>
          <td>registry、scan、runtime handoff</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a></td>
          <td>artifact 內含元件如何被揭露</td>
          <td>image scan、release evidence、compliance</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/release-channel/" data-link-title="Release Channel" data-link-desc="說明 stable、beta、internal 等發行通道如何控制 artifact 接觸到的使用者範圍">Release Channel</a></td>
          <td>版本如何依使用者範圍分流</td>
          <td>app、desktop、beta、stable</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/update-feed/" data-link-title="Update Feed" data-link-desc="說明桌面與客戶端應用如何透過更新來源取得已簽章版本與回復路徑">Update Feed</a></td>
          <td>已安裝客戶端如何取得新版本</td>
          <td>desktop auto-update、rollback channel</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>真實環境與 IaC 宣告如何分叉</td>
          <td>Terraform、Pulumi、manual hotfix</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a></td>
          <td>IaC apply 如何避免併發覆寫 state</td>
          <td>Terraform backend、workspace、apply</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/function-alias/" data-link-title="Function Alias" data-link-desc="說明 serverless function alias 如何把穩定入口指向特定版本並支援流量切換與回復">Function Alias</a></td>
          <td>serverless 入口如何指向特定版本</td>
          <td>alias rollback、traffic shift</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/event-source/" data-link-title="Event Source" data-link-desc="說明 serverless 與事件驅動流程中觸發來源如何影響 retry、dead-letter 與回復策略">Event Source</a></td>
          <td>事件來源如何影響 retry 與回復</td>
          <td>queue、topic、HTTP trigger、scheduler</td>
      </tr>
  </tbody>
</table>
<p>卡片與流程文章分工清楚。卡片負責名詞與邊界，流程文章負責情境判讀與操作路由。</p>
]]></content:encoded></item><item><title>用 Claude Code GitHub Actions 自動除錯 CI 建置失敗</title><link>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E7%94%A8-claude-code-github-actions-%E8%87%AA%E5%8B%95%E9%99%A4%E9%8C%AF-ci-%E5%BB%BA%E7%BD%AE%E5%A4%B1%E6%95%97/</guid><description>&lt;h2 id="這是什麼">這是什麼&lt;/h2>
&lt;p>&lt;a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions&lt;/a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>互動式助手&lt;/strong> — 在 PR/Issue 留言 &lt;code>@claude&lt;/code>，Claude 會分析程式碼並回覆&lt;/li>
&lt;li>&lt;strong>自動 Code Review&lt;/strong> — PR 開啟時自動審查變更&lt;/li>
&lt;li>&lt;strong>CI 除錯修復&lt;/strong> — build 失敗時自動分析錯誤並修復&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="這是什麼">這是什麼</h2>
<p><a href="https://github.com/anthropics/claude-code-action">Claude Code GitHub Actions</a> 讓 Claude 直接參與你的 GitHub 工作流程，主要功能：</p>
<ul>
<li><strong>互動式助手</strong> — 在 PR/Issue 留言 <code>@claude</code>，Claude 會分析程式碼並回覆</li>
<li><strong>自動 Code Review</strong> — PR 開啟時自動審查變更</li>
<li><strong>CI 除錯修復</strong> — build 失敗時自動分析錯誤並修復</li>
</ul>
<p>完整功能說明參考 <a href="https://code.claude.com/docs/en/github-actions">官方文件</a>。</p>
<h2 id="設定方式">設定方式</h2>
<h3 id="install-github-app推薦"><code>/install-github-app</code>（推薦）</h3>
<p>在 Claude Code 終端執行 <code>/install-github-app</code>，它會引導你完成所有設定。</p>
<p>流程中的關鍵步驟：</p>
<ol>
<li><strong>選擇 repo</strong> — 指定要安裝的 GitHub repository</li>
<li><strong>安裝 Claude GitHub App</strong> — 自動安裝到指定 repo，授予 Contents、Issues、Pull requests 的 Read &amp; Write 權限</li>
<li><strong>選擇認證方式</strong> — 選擇 <strong>long-life token</strong> 會產生 OAuth token，自動寫入 GitHub Secrets 為 <code>CLAUDE_CODE_OAUTH_TOKEN</code></li>
<li><strong>建立 workflow 檔案</strong> — 自動建立並 push 兩個 workflow：
<ul>
<li><code>claude.yml</code> — <code>@claude</code> 互動回覆</li>
<li><code>claude-code-review.yml</code> — PR 自動 code review</li>
</ul>
</li>
</ol>
<p>完成後不需要額外設定。</p>
<h3 id="手動設定使用-anthropic-api-key">手動設定（使用 Anthropic API Key）</h3>
<p>如果不想用 <code>/install-github-app</code>，可以手動操作：</p>
<ol>
<li>前往 <a href="https://github.com/apps/claude">github.com/apps/claude</a> 安裝 App 到你的 repo</li>
<li>到 repo 的 <strong>Settings → Secrets and variables → Actions</strong>，新增 <code>ANTHROPIC_API_KEY</code></li>
<li>手動建立 workflow 檔案到 <code>.github/workflows/</code></li>
</ol>
<p>兩種認證方式的差異：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>Secret 名稱</th>
          <th>適用對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><code>CLAUDE_CODE_OAUTH_TOKEN</code></td>
          <td>Pro/Max 用戶，<code>/install-github-app</code> 自動設定</td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><code>ANTHROPIC_API_KEY</code></td>
          <td>直接使用 Anthropic API，需手動到 <a href="https://console.anthropic.com">console.anthropic.com</a> 取得</td>
      </tr>
  </tbody>
</table>
<h2 id="加入-ci-自動除錯">加入 CI 自動除錯</h2>
<p><code>/install-github-app</code> 建立的 workflow 只處理 <code>@claude</code> 互動和 code review。如果你想在 <strong>build 失敗時自動觸發 Claude 修復</strong>，需要修改既有的 deploy workflow。</p>
<p>首先，補上 Claude 需要的權限（原本可能只有 <code>contents: read</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># Claude 需要寫入修復後的檔案</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write  </span><span class="w"> </span><span class="c"># Claude 可能需要建立 PR</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">issues</span><span class="p">:</span><span class="w"> </span><span class="l">write         </span><span class="w"> </span><span class="c"># Claude 回報結果</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">pages</span><span class="p">:</span><span class="w"> </span><span class="l">write          </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write       </span><span class="w"> </span><span class="c"># 原本的 deploy 權限</span></span></span></code></pre></div><p>然後在 build 步驟加入 Claude 除錯邏輯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 在原本的 build step 加上 continue-on-error 和 id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">hugo-build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify 2&gt;&amp;1 | tee hugo-build-output.txt</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">continue-on-error</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c"># Build 失敗時觸發 Claude 除錯</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Claude Debug on Build Failure</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">anthropics/claude-code-action@v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="c"># 依你的認證方式擇一</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">claude_code_oauth_token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="c"># anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">prompt</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="sd">      Hugo build failed. Here is the error output:
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="sd">      $(cat hugo-build-output.txt)
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="sd">      Please analyze the error, find the problematic file(s),
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="sd">      fix the YAML front matter or content issue, and commit the fix.</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">claude_args</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;--max-turns 10&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="c"># 修復後重新 build 驗證</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Retry build after fix</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">steps.hugo-build.outcome == &#39;failure&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify</span></span></span></code></pre></div><p>核心設計：</p>
<ol>
<li><code>continue-on-error: true</code> — build 失敗不中斷流程，讓後續 Claude 步驟有機會執行</li>
<li><code>if: steps.hugo-build.outcome == 'failure'</code> — 只在失敗時觸發，正常 build 不消耗 API 額度</li>
<li>修復後重新 <code>hugo --minify</code> 驗證是否成功</li>
</ol>
<h2 id="計費方式">計費方式</h2>
<p>計費取決於你使用哪種認證方式：</p>
<table>
  <thead>
      <tr>
          <th>認證方式</th>
          <th>計費來源</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OAuth Token</td>
          <td><strong>訂閱額度</strong>（Pro/Max）</td>
          <td>跟 claude.ai 網頁、Claude Code CLI、Claude Desktop <strong>共用同一個額度池</strong></td>
      </tr>
      <tr>
          <td>API Key</td>
          <td><strong>獨立 API 計費</strong></td>
          <td>按 token 用量付費，與訂閱額度完全分開</td>
      </tr>
  </tbody>
</table>
<p>OAuth token 的額度是共用的，GitHub Actions 跑多了會擠壓你日常在 claude.ai 和 CLI 的使用額度。如果 CI 觸發頻繁，建議改用 API Key 避免互相影響。</p>
<p>詳細的費率可參考 <a href="https://www.anthropic.com/pricing">Claude 定價頁面</a>。</p>
<h3 id="降低成本的設定">降低成本的設定</h3>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--max-turns 10</code></td>
          <td>限制迭代次數，避免無限循環</td>
      </tr>
      <tr>
          <td>只在 <code>failure</code> 時觸發</td>
          <td>正常 build 不消耗 API 額度</td>
      </tr>
      <tr>
          <td><code>@claude</code> 觸發詞</td>
          <td>互動模式只在明確呼叫時才啟動</td>
      </tr>
  </tbody>
</table>
<h2 id="搭配-claudemd">搭配 CLAUDE.md</h2>
<p>在 repo 根目錄建立 <code>CLAUDE.md</code>，Claude 會自動讀取作為上下文，提升修復準確度。</p>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://code.claude.com/docs/en/github-actions">Claude Code GitHub Actions 官方文件</a></li>
<li><a href="https://github.com/anthropics/claude-code-action">claude-code-action GitHub Repo</a></li>
<li><a href="https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md">Setup Guide</a></li>
</ul>]]></content:encoded></item></channel></rss>