FR-173 · 設計文件 · 2026-11-03 修訂(出包時蓋章、測試包模式)

設計 v1.0(定稿):三條 CI、一個簽章金庫

BE、Agent、FE 各一支活的 pipeline;發版起點是 GitLab 的 Run pipeline;出包時向自架 Vault 要簽名,試完在 GitLab 按「發布」;另有不掃描、不上站的測試包模式

🔴 這份是設計決策,不是現況。 1.29 起走這條路出版,1.28.x 不出正式版。推翻 FR-167 兩條裁示:①「印章私鑰只在決策者 Mac」改為「Vault 簽、Mac 章備用」;②「先給內部機器裝草稿、再蓋章發布」改為「出包時就蓋章、發布只是公開」——客戶端一律拒收沒蓋章的清單,舊順序在內部機器那一步就走不通。update-center 與 License Center 不進管線。

6 張卡 3 批、5 repo,預估 2 個工作天 決策者功課分三段,各在對應批次前做 Ed25519 不改、客戶端驗章零改動 私鑰不進 GitLab 變數

📑 本頁是 v1.0(定稿)。 實作中有設計異動,見 設計 v2.0(定稿)(正式發版、直接部署、測試版發布三條路;正式發版自動發布;內部三台皆可部署與測試升級)。本頁內容保留定稿原樣,不回頭改。

0. 分工概述(30 秒版)

整案一句話:每個會出貨的 repo 一支自己的 CI 對自己負責;發版的人在 GitLab 按「Run pipeline」,CI 跑完測試、編譯、排練、打包,出包時就向自架 Vault 要簽名,把蓋好章的版本登記成只有 190 看得到的草稿;190 試過沒問題,回 GitLab 按「發布」,客戶才看得到。全程不用 Mac。

兩種跑法,同一顆按鈕,差在勾不勾 internal_test:

正式包 測試包
給誰 客戶 只給 190 自己測
從哪跑 受保護的 tag(例 v1.29.0) 任何 branch
跑什麼 測試、編譯、190 升級排練、SBOM+弱掃、推 Harbor、上 R2、登記更新站 測試、編譯、打包;跳過排練、SBOM+弱掃、Harbor、R2、更新站
章 Vault 正式章 guidant-release Vault 測試章 guidant-test(客戶機不認得)
怎麼到 190 190 更新中心「立即檢查」→ 更新 CI 直接 scp 到 190 升級
主系統約多久(188 實測推估) 約 50 分 約 30 分;配 skip_be(不重編)約 10 分
階段 做什麼 卡 完成怎麼判定(決策者檢查法)
決策者功課 分三段,各在對應批次前做,見 §5 — 各段的檢查指令見 §5
V 簽章 蓋章工具接 Vault;更新站收「蓋好章的草稿」、發布端點認 GitLab 身分 V-1 172 簽一份清單,190 驗章過;Mac 私鑰仍能簽(備用)
C 三條 CI FE 自己的 CI;Agent 新 CI;BE 改觸發 FE、出包時蓋章、手動「發布」、測試包模式;jedi 套件 CI C-3 C-2 C-1 C-4 從 tag 跑一次:190 看得到新版、升級成功 → 按「發布」→ 更新站 published、kid 是 Vault 那把;勾 internal_test 跑一次:190 被直接升上去
D 文件 FINAL-SPEC、三 repo 分工表、仍要人按的步驟、鑰匙輪換 SOP D-1 —

依賴一句:批 1(V-1、C-3)→ 批 2(C-2、C-1)→ 批 3(C-4、D-1),每批前決策者做完那一段功課。


1. 決策定案表

# 題目 定案 被排除的 理由
D1 CI 自動簽章 同意;私鑰進金庫、CI 用 OIDC 短效身分要簽名 維持 Mac 手簽 出差就出不了版;與無人值守方向相反
D2 金庫 自架 HashiCorp Vault Transit(Community 版),專用 VM 192.168.50.172、apt 裝成 systemd 服務、單節點 Raft;架法見 vault-setup-runbook.md Azure Key Vault/AWS KMS(要換 P-256、改三處驗章);update-center Worker Secret(鑰匙程式讀得到,不算不可匯出);GitLab CI 變數(反模式);Cloudflare 無 KMS;GitLab Secrets Manager(要 Premium、只存祕密不開放簽章);OpenBao(功能同、文件少,日後授權有變再換,指令與 API 相同) Ed25519 不改、鑰匙不可匯出、建置期祕密順便集中;業界共識是 KMS/Vault+OIDC,私鑰不放 CI 變數
D3 三 repo CI 分不分 BE、Agent、FE 各一支;BE 發版 pipeline trigger: FE 下游(strategy: mirror,官方已不建議 depend);Agent 是自己的按鈕,不被 BE 觸發 全塞 BE 一支 各 repo 對自己負責、FE push 就掃不用等出包
D4 runner BE、Agent、FE、jedi 都用 188 shell runner(24 核、concurrent=3);只有 Nuitka 那段用 188 主機鎖串行(resource_group 不跨專案);188 沒裝 node,FE 的 npm 在 node 容器裡跑;jedi 套件另議 FE 用 171 上 2024 年留下的 docker runner(#37407054) 兩條 Nuitka 同跑 linker crash;171 只有 2 核且跑 Nexus、Harbor,FE build 會拖慢它們;188 上 BE CI 本來就在打包 FE,環境現成
D5 FE 三支舊 CI 刪(ECR/ECS/ansible 是 SaaS 時代);換 verify(lint、vitest、npm 弱掃)+tag build 推 Harbor 留著 沒人維護、推錯地方
D6 release note 與最低相容版 從 repo 檔讀:BE docs/release_notes/v<版>.md 檔尾、Agent RELEASE_NOTES.md 檔尾、min_agent_version/min_host_version 一行檔 GitLab 表單欄位 進版控、有 review
D7 update-center、LC 不進管線,各自部署 納入 獨立系統,範圍太大
D8 客戶主機預設密碼(DB、Redis、Bugsink 每台同組) 另案:Vault 解不了(客戶內網連不到);正解是裝機時每台隨機、compose secrets: 掛檔、首次登入強制改 塞進本案 與 CI 無關
D9 仍要人按的 ①打受保護 tag 並在 GitLab 按 Run pipeline;②190 試過後按 pipeline 最後一個手動 job「發布」 全自動;推 tag 自動跑 「這個 commit 要出貨」是決策;BE CI 一直是人按才跑,維持
D10 什麼時候蓋章 出包時就蓋:CI 打包後向 Vault 要簽名,登記「蓋好章的草稿」,只有內部先裝機器看得到;發布只把狀態改成公開,不再蓋章 先給內部機器裝草稿、試完才蓋章 客戶端一律拒收沒蓋章的清單(package_verifier.py),舊順序內部機器裝不上,發布前的「內部先裝成功」檢查永遠過不了
D11 誰能按發布 更新站多認一種身分:GitLab 每個 job 的 ID token,只收 BE、Agent 兩專案、受保護 ref;決策者本人的 Cloudflare 登入照舊可用(備用) 發一把長效 CI 通行碼加「發布」權限 不留長效祕密;與 Vault 用同一張身分證、同一套信任條件
D12 測試包 擴充 CI 既有 internal_test:跳排練、SBOM+弱掃、Harbor、R2、更新站;用 Vault 第二把測試章 guidant-test 簽;scp 直升 190 另開第三種模式;測試包也上更新站 測試不需要的步驟約 20 分;測試章公鑰只裝在 190,包流出去別台(含 STG、POC、客戶機)都裝不了;要測「更新中心看到新版 → 按更新」那段就跑正式模式(草稿只有 190 看得到,本來就安全)
D13 GitLab 免費版 拿 Vault 祕密用 id_tokens+script 內登入(secrets: 是付費版) 升 Premium 官方教學的手動登入寫法免費版可用,效果相同
D14 Agent 主控台前端怎麼來 比照主系統前端:Agent CI 在 188 把 /opt/guidant-ai-fe 切到同名 branch,先跑 FE sbom_fe.sh 掃該 commit 的 lockfile,再用 FE Dockerfile 的 console-dist 目標 docker build 輸出 dist-console/ 從 FE pipeline 下載 artifact(免費版 CI_JOB_TOKEN 跨專案下載回 404、needs:project 付費);FE 傳套件庫;長效唯讀通行碼;188 共用資料夾 跟主系統前端同一份 Dockerfile、同一個 node 版本、同一份 lockfile;掃描與打包同一個 commit;不多一把祕密

2. 目標管線

2.1 正式發版:從按鈕到客戶看得到

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF','fontSize':'20px'},'sequence':{'actorFontSize':20,'messageFontSize':19,'noteFontSize':18,'actorMargin':60,'messageMargin':42,'boxMargin':12,'useMaxWidth':false}}}%%
sequenceDiagram
  autonumber
  actor P as 發版的人
  participant GL as GitLab BE pipeline(188 runner)
  participant FE as FE pipeline(188 runner)
  participant V as Vault(172)
  participant H as Harbor/R2
  participant US as 更新站
  participant T as 190 測試機
  P->>GL: 打受保護 tag v1.29.0,Run pipeline
  GL->>GL: verify:守衛、pytest、弱掃 lockfile
  GL->>FE: trigger(mirror),等 FE 綠
  FE-->>GL: FE 映像 tag
  GL->>GL: build:Nuitka、BE/init 映像
  GL->>T: rehearse:190 清掉重灌舊版、灌 STG dump、用新包升級
  GL->>GL: package:SBOM+弱掃放行門、打包
  GL->>V: 用 GitLab job 身分登入,讀 R2/Harbor/更新站祕密
  GL->>H: 推映像、上傳安裝包
  GL->>V: transit/sign guidant-release(canonical 清單)
  V-->>GL: 簽名(鑰匙不出 Vault)
  GL->>US: register-draft,附蓋好章的清單;草稿只給 190 看
  P->>T: 更新中心「立即檢查」→ 更新到 1.29.0
  T->>US: 回報升級成功
  P->>GL: 按手動 job「發布」
  GL->>US: 帶 GitLab job 身分呼叫 publish
  US->>US: 核:BE 專案、受保護 tag、190 已回報成功
  US-->>GL: published
  Note over US: 客戶的「立即檢查」從這一刻看得到 1.29.0
圖 1 — 正式發版時序。人按兩次:Run pipeline、發布;中間 190 自己測。

為什麼章在第 10~11 步就蓋:客戶端看到沒蓋章的清單一律拒收,190 要能在第 13 步裝上去,拿到的就必須是蓋好章的版本。發布(第 15~18 步)只是把草稿改成公開。

從 branch 跑會怎樣:Vault 不給正式章(角色只認受保護 ref),pipeline 在第 10 步紅;「發布」那顆 job 也不會出現。故意的:避免從開發分支誤發。

2.2 測試包:只給 190 測

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF','fontSize':'20px'},'sequence':{'actorFontSize':20,'messageFontSize':19,'noteFontSize':18,'actorMargin':60,'messageMargin':42,'boxMargin':12,'useMaxWidth':false}}}%%
sequenceDiagram
  autonumber
  actor P as 開發者
  participant GL as GitLab BE pipeline(188 runner)
  participant V as Vault(172)
  participant T as 190 測試機
  P->>GL: 任一 branch,勾 internal_test(可加 skip_be),Run pipeline
  GL->>GL: verify:守衛、pytest
  GL->>GL: build(skip_be 時沿用上一顆 BE 映像)
  GL->>GL: 打包(不跑 SBOM/弱掃)
  GL->>V: transit/sign guidant-test
  V-->>GL: 測試章簽名
  GL->>T: scp 安裝包+清單,install.sh --upgrade --force
  T-->>GL: 升級結果
  Note over T: 測試章只有 190 認得;包流出去別台都裝不了
圖 2 — 測試包時序。不碰 Harbor、R2、更新站。

2.3 三條 pipeline 誰觸發誰

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph trig["人按"]
    T1["BE:Run pipeline(tag 或 branch)"]
    T2["Agent:Run pipeline(tag 或 branch)"]
    T3["FE/jedi:任意 push"]
  end
  subgraph be["BE pipeline(188 shell runner)"]
    B1["verify"]
    B2["trigger FE(mirror)"]
    B4["build:Nuitka、BE/init 映像"]
    B5["rehearse:190 舊庫升級排練(測試包跳過)"]
    B6["package:SBOM 放行門 → 打包 → Harbor → R2(測試包只打包)"]
    B6s["sign:Vault 正式章或測試章"]
    B6d["正式:register-draft 帶章/測試:scp 升 190"]
    B7["publish(手動、只在受保護 tag)"]
    B1 --> B2 --> B4 --> B5 --> B6 --> B6s --> B6d --> B7
  end
  subgraph fe["FE pipeline(188 shell runner)"]
    F1["verify:lint、vitest、npm 弱掃"]
    F2["build:FE 映像 → Harbor;dist-console artifact"]
  end
  subgraph ag["Agent pipeline(188 shell runner,與 BE 串行)"]
    G1["verify"] --> G2["build:Nuitka、拉 FE dist-console、Agent 映像"] --> G3["rehearse"] --> G4["package+sign+register-draft product=agent"] --> G5["publish(手動)"]
  end
  T1 --> B1
  T2 --> G1
  T3 --> F1
  B2 -.-> F1
圖 3 — 三條 pipeline。Agent 那條是自己的按鈕。
項 主系統 CI Agent CI FE CI jedi CI
runner 188 shell(id 56998011);Nuitka 段用 188 上的 flock /home/jedi/.guidant-build.lock 排隊 188 shell,同一把鎖 188 shell(同一台;不用 Nuitka,不進 resource_group,188 一次可同跑 3 個 job) 待決策者開給套件群組
觸發 人按 Run pipeline 人按 Run pipeline push(verify)、BE 觸發(build) push(test)、BE 觸發(分類器映像)
弱掃 pip-audit、syft+grype lockfile、trivy+grype 映像、vuln_gate.py;測試包跳映像掃描 同左 syft+grype package-lock pip-audit
祕密 id_tokens → Vault JWT 登入 → KV 同左 同左(Nexus npm 認證) 同左
簽章 正式:guidant-release;測試:guidant-test 同左 --product agent 無 無
人按的 Run pipeline、發布 同左 無 無

2.4 Agent 怎麼發

Agent 是另一個專案(auditmanager/evidence-agent)、另一顆按鈕,流程與圖 1 相同,蓋同一把正式章(客戶機只認一批公鑰;190 的 Agent 已認得)。和主系統不同的三處:

主系統 Agent
190 怎麼試 更新中心升主系統 更新中心 Agent 區「推送 Agent」
離線檔(不出網的客戶從 Agent 主控台上傳) 無 蓋章時一起產 guidant-agent-<版>.offline.tar.gz(包+簽好的清單+.sha256),留在 188 .build/compose/,與主系統安裝包同樣不上 artifacts;不出網的客戶從 188 拿
打包時另一個簽章 無 Agent 映像內檔案清單送 License Center 簽(sign_agent_manifest.sh,與出版章無關),要 LICENSE_CENTER_API_TOKEN;改從 Vault kv/ci/license-center 拿,188 /opt/guidant-ai-agent/.env 不再放

兩條同時按時,後到的在 188 主機鎖(/home/jedi/.guidant-build.lock)排隊,不會兩個 Nuitka 同跑;GitLab 的 resource_group 只在同一專案內有效,擋不了跨專案。主系統這一版要求新 Agent 時,先發 Agent、再發主系統(主系統清單的 min_agent_version 會指到它)。


3. 簽章金庫

項 定案
鑰匙 正式章 guidant-release(已建,kid 05086455d42ca0d6);測試章 guidant-test(待建)。都是 Ed25519、exportable=false、deletion_allowed=false
身分 GitLab id_tokens(aud=Vault)→ Vault JWT auth 角色。正式角色 release-ci:綁 BE、Agent 兩專案、ref_type=tag、ref_protected=true、user_id=決策者,policy 給正式章簽章+kv/ci/* 讀;測試角色 test-ci:綁兩專案、ref_type=branch、user_id=決策者,policy 給測試章簽章+Nexus、Harbor、License Center 三筆讀;FE 角色 fe-ci 只綁 FE 專案、只讀 Nexus。只有決策者能發版、能跑測試包靠 user_id(按 Run pipeline 的人)擋:GitLab 免費版受保護 tag 只能選角色、不能指定人,五位成員都是 Maintainer 以上。要加人就把他的 user_id 加進兩個角色,重寫角色即可,不改程式、不出版
稽核 Vault audit log 每次簽章記 job id、project、ref;D-1 寫進 SOP 怎麼查
公鑰 正式章公鑰在 BE scripts/installer/release-keys/ 與 PROD_KIDS,隨 1.28.0 裝上客戶機;客戶端本來就支援多把 kid,零改動。客戶機要先升到 1.28.x 才認得這把(190 已有;189 POC 還在 1.27.x,要先升)。測試章公鑰只放 190(188 STG、189 POC 不放:任何 branch 都能簽測試章,信任它的機器就等於信任未經審查的程式碼),不進安裝包,只手動複製到 190 的 /etc/guidant/release-keys/
備用 Mac 根印章保留第二把 kid;Vault 出事時 publish --signer file 照舊
輪換 加新鑰 → 新 kid 進 PROD_KIDS 出一版 → 舊 kid 標 retiring → 下一版拔;SOP 在 D-1
建置期祕密 Nexus npm 認證、R2 金鑰、更新站 CI token、Harbor 帳密、License Center API token(Agent 打包用)進 Vault KV;188 上 ~/.npmrc、.env 內對應鍵刪掉;job 執行時 secrets: 拿、結束即失效

188 工作區共用的風險與擋法:BE、Agent CI 都直接在 188 的 /opt/guidant-ai-* 跑(GIT_STRATEGY: none),測試包(任何 branch)與正式包共用同一個工作區、.venv、映像快取。任何 branch 的 pipeline 都可能留下被改過的快取,下一次正式包把它包進去再蓋正式章。擋法(C-1、C-2 的必做項):正式模式不准 skip_be/--reuse-images;.build/image-cache/ 正式模式只讀不寫,讀入後 docker load 比對 image ID,不符就丟掉重匯;.venv 正式模式先 poetry install --sync 再跑。殘餘風險:能推 branch 的人就能讓 188 以 jedi(在 docker 群組,等同 root)跑任意指令——BE、Agent 是人按 Run pipeline 時、FE 是每次 push 時;讀得到 188 上的明文 .env、Harbor 登入,也能動共用快取。目前 BE、Agent、FE 三個專案成員是同一批五位 Maintainer,FE 改成 push 就跑並沒有多開放給新的人。Vault 裡的章與 License Center token 有 user_id 擋住,188 上的明文副本在第二批 CI 驗完後清掉(STATE §3)。要徹底隔離得另開一台只收受保護 ref 的 runner,本案不做,記在 §8;FE 專案日後若加入非 Maintainer 成員,要先把 FE 的 verify 移出 188 或改成人按才跑。

不解的:客戶主機執行期密碼(D8,另案)。


4. 拆卡表

由 handoff/cards.py 產。每批開工前決策者做完 §5 對應那段功課。

批 卡 階段 repo 做什麼 reuse/extend/new 改既有檔 新開檔 依賴 驗收(小弟本機) model/effort
1 V-1 簽章金庫 update-center 出包時就蓋章、GitLab 身分發布:蓋章 CLI 接 Vault Transit(正式章 guidant-release、測試章 guidant-test),新增 sign-draft 子命令讓 CI 登記草稿時帶上簽好的清單(Agent 版同時產離線檔);更新站 Worker 收簽好的草稿、發布端點多認 GitLab 的 job 身分(只限 BE/Agent 專案受保護 tag),發布不再帶章;Mac 私鑰留備用 extend:cli/keystore.ts 加 VaultSigner(與既有檔案私鑰 signer 同介面);cli/envelope.ts 簽章函式吃 signer;cli/commands.ts 加 sign-draft(--product agent 時 reuse cli/offline.ts 產離線檔)、`publish --signer vault file;Worker src/access.ts照verifyAccessJwt 的寫法加 GitLab ID token 驗證、register-draft收signed_manifest+kid、publish` 對 GitLab 身分只改狀態不收章 cli/keystore.ts
cli/envelope.ts
cli/commands.ts
cli/README.md
src/access.ts
src/routes/admin.ts
src/releases.ts
test/
cli/vault_signer.ts
test/cli/vault_signer.spec.ts
test/gitlab_identity.spec.ts
— vitest:VaultSigner 對 mock 的 Vault 組出合法簽名、verifyRelease 驗過;file signer 行為不變;缺 Vault 身分明確錯誤不退回 file;register-draft 帶簽好清單後內部先裝機器 check 拿得到 signed_manifest;GitLab ID token 不是 BE/Agent 專案、不是受保護 ref、過期、aud 不對都 403;Cloudflare 本人登入的 publish 路徑照舊可用。用開發帳號對 172 以測試章實簽一份清單並驗過;正式章只讀公鑰、核 kid=05086455d42ca0d6(開發帳號不能簽正式章,正式章實簽在 C-1 由 CI 驗)
1 C-3 三條 CI FE FE 自己的 CI:刪三支舊檔(ECR/ECS/ansible),換一支 verify+build:push 跑 lint、vitest、npm 弱掃(syft+grype 對 package-lock);BE 觸發或 tag 時 build FE image 推 Harbor;主控台前端同一支 pipeline build 成 artifact 給 Agent CI 拿 new(照 BE CI 分段,跑 188 的 guidantai-build shell runner;188 沒裝 node,npm 一律在 node 容器裡跑):弱掃 reuse BE scripts/build/sbom.sh 的 lockfile 掃法搬成 FE repo 自己的 scripts/ci/sbom_fe.sh;Nexus 認證從 Vault KV 拿(job 內用 id_tokens 登入 Vault 再讀 KV;GitLab 免費版不能用 secrets: 關鍵字),不再 GitLab Variable .gitlab-ci.yml(重寫) scripts/ci/sbom_fe.sh
scripts/ci/vuln-allowlist.txt
— 刪 .gitlab-ci-on-premises.yml、.gitlab-ci-terraform.yml;push feature branch 跑 verify 綠(有白名單外 Critical 會紅);tag 跑 build 推 Harbor guidant-ai-fe:<版>;artifacts 有 dist-console/ opus/medium
2 C-3b 三條 CI FE FE Dockerfile 多一個主控台打包目標 console-dist:docker build 就能產出 Agent 主控台 dist-console/,不必在 Mac 打包(從 C-2 拆出,CM-3480) extend:既有 Dockerfile 拆出共用 deps stage,主系統與主控台各一個 build stage;不帶 --target 時產物不變 Dockerfile — — docker build --target console-dist --output type=local 產出 index.html+assets/(≤2 MB);主系統映像照舊通過 build_image.sh --no-push 檢查 opus/medium
2 C-2 三條 CI evidence-agent Agent 自己的 CI(新):verify → build(主控台前端從 FE pipeline 拿)→ rehearse → package(SBOM 放行門、build_compose_bundle、Harbor、sign-draft 蓋 Vault 章並產離線檔、登記草稿 product=agent、R2 agent/v1/)→ 手動 publish;License Center 送簽 token 改從 Vault 拿;測試包模式同 C-1;188 shell runner、resource_group 與 BE 串行 new:照 BE .gitlab-ci.yml 五段與 .sync_repo/.in_repo/.collect 片段;publish_draft.sh 從 BE 複製成 Agent 版加 --product agent(先複製,不跨 repo 共用);vault_login.sh 照 C-1 那支複製;測試包模式沿用既有 --internal-test scripts/build/build_all.sh(CI 模式與既有 --internal-test 併用)
scripts/build/README.md
.gitlab-ci.yml
scripts/build/publish_draft.sh
scripts/build/vault_login.sh
C-3, C-3b 正式模式:五段綠、更新站草稿 product=agent 帶 Vault 章、R2 agent/v1/<版>/ 三檔、188 .build/compose/ 有離線檔、按 publish 後 published;188 /opt/guidant-ai-agent/.env 不再需要 LICENSE_CENTER_API_TOKEN;測試包模式:測試章簽、不碰 Harbor/R2/更新站;與 BE pipeline 同時觸發時第二條在 resource_group 排隊不併跑 opus/high
2 C-1 三條 CI BE BE 發版 pipeline 改版:trigger FE 下游(strategy mirror)、不再在 188 build FE、祕密從 Vault 拿、package 段用 V-1 的 sign-draft 向 Vault 蓋章後登記草稿、最後一個手動 job「發布」用 GitLab 身分叫更新站公開;internal_test 擴成「測試包」模式(跳排練、SBOM/弱掃、Harbor、R2、更新站,測試章簽、scp 直升 190);release note 檔尾自動送更新站;runbook 改寫 extend:.gitlab-ci.yml 的 build-fe 改 trigger:;package 段加 sign-draft、新 job publish(manual、只在受保護 tag);internal_test input 改成測試包模式(既有開關擴充,不另開);publish_draft.sh 帶簽好清單與 release note 摘要;runbook 步驟 2.2/2.3/4 重寫 .gitlab-ci.yml
scripts/build/publish_draft.sh(帶簽好清單+release note 摘要)
docs/features/FR-167-2610-online-update-channel/release-runbook.md
scripts/build/release_notes_tail.sh
scripts/build/vault_login.sh
V-1, C-3 正式模式(受保護 tag):FE 下游綠才進 build-be;草稿登記時已帶 Vault 章、只有 190 check 看得到;190 升上去後按 publish job,更新站轉 published、kid 是 Vault 正式章;從 branch 跑的 pipeline 要不到正式章、publish job 不出現。測試包模式:不跑排練/SBOM/Harbor/R2/更新站,包用測試章簽、scp 升 190 成功,總時間比正式模式少 20 分以上。188 上 grep -r R2_SECRET ~/.npmrc /opt/*/.env 為 0;runbook 無「Mac 蓋章」步驟(備用章段除外) opus/high
3 C-4 三條 CI jedi 套件 jedi 套件 CI:push 跑各套件 pytest 與 pip-audit;分類器 image 由 BE 下游 trigger build 推 Harbor(runner 待決策者開給套件群組) extend:既有 .gitlab-ci.yml(若有)加 job;分類器 image build reuse BE build_classifier_image.sh 搬到套件 repo .gitlab-ci.yml jedi-evidence-classification/docker/ci.sh C-1 push 跑測試綠;BE tag 觸發時分類器 image 在 Harbor 有新 tag sonnet/medium
3 D-1 文件 BE 文件:補齊獨立文件 docs/system-design/release-pipeline/出版管線總覽.md 八段(全景、機器、服務、祕密、連線、流程、從零重建、出事怎麼辦),含 Vault 鑰匙輪換與備用章啟用 SOP;FR-173 的 FINAL-SPEC 只放指向它的摘要 new 文件 docs/system-design/release-pipeline/出版管線總覽.md
docs/system-design/README.md
docs/features/FR-173-2611-release-pipeline/FINAL-SPEC.md C-1, C-2 總覽八段沒有「待填」;每台機器、每個服務都實查過(ssh/API 讀回),不照抄 design;FINAL-SPEC 五段齊;grep -n "原本|改判|第 [0-9] 棒" 為 0 sonnet/low

共 7 張、3 批;update-center 1、FE 2、evidence-agent 1、BE 2、jedi 套件 1。


5. 決策者功課

什麼時候前 做什麼 狀態
派批 1 前 Vault 測試章 guidant-test、開發帳號 release-dev(只能簽測試章)、Mac CA 憑證、update-center .env 四個 VAULT_ 鍵 ✅ 2026-11-03,首腦實測登入、簽章、越權 403
派批 1(C-3)前 Vault JWT 登入(認 gitlab.com)、kv/ci/ 五筆祕密、ci-fe+fe-ci;FE 專案 CI 設定檔路徑清空(原指已刪的舊檔);188 信任 Vault CA ✅ 2026-11-03,FE pipeline 3022544900 實跑通過
派批 2 前 ci-release/ci-test 政策、release-ci/test-ci 角色(綁 user_id);測試章公鑰只放 190;BE、Agent v* 受保護 tag;FE job token 允許清單加 Agent ✅ 2026-11-03,首腦讀回確認
第二批 CI 驗完後 刪 188 上的明文帳密(/opt/guidant-ai-*/.env 的 License Center token、~/.fr167-r2.env、~/.npmrc)、BE 專案 GitLab 變數的 R2 與 Bugsink;License Center 換新 token 放進 Vault ⬜
派批 3 前 jedi 套件群組(commonservice3)開一個 runner ⬜
正式出 1.29 前 Vault 每日快照+異地備份、防火牆(只放 188、決策者 Mac);189 POC 先升到 1.28.x;部署更新站 Worker 後在後台登記 05086455d42ca0d6 為 primary、Cloudflare Access 放行 GitLab 打發布端點 ⬜

指令逐行寫在 vault-setup-runbook.md 第 14 段。


6. 端到端驗收

  1. 從 branch 勾 internal_test 跑 BE:190 被直接升上去;Harbor、R2、更新站都沒有這次的東西。
  2. 從受保護 tag 跑 BE:FE 下游紅 → BE 停在 trigger 不往下;FE 綠 → 一路到 register-draft。
  3. 190「立即檢查」看得到草稿、升級成功;別台看不到。
  4. 按「發布」:更新站 published、kid 是 05086455d42ca0d6;Vault 稽核 log 有一筆 sign。
  5. 從 branch 跑正式模式:在 sign 那步紅,沒有「發布」job。
  6. Agent 與 BE 同時按:第二條排隊不併跑。
  7. 188:grep -r "R2_SECRET\|_auth=\|LICENSE_CENTER_API_TOKEN" ~/.npmrc /opt/guidant-ai-*/.env 為 0。
  8. 從 Agent 受保護 tag 跑:190 推送 Agent 成功;188 .build/compose/ 有 guidant-agent-<版>.offline.tar.gz,拿一台不出網的 Agent 從主控台上傳驗過。
  9. 拔掉 Vault:決策者 Mac publish --signer file 照樣能發(備用路徑)。

7. 時程

以 FR-171、172 實際節奏推估(工作坊一批 2~3 張平行,一張卡 2~4 小時):

段 內容 預估
批 1 V-1(蓋章工具+更新站三處,本案最大一張)、C-3 半天
批 2 C-1、C-2;要真跑 pipeline 才驗得到,一輪 30~50 分,算兩三輪修 半天到一天
批 3 C-4、D-1 2~3 小時
總驗 §6 八項,從 tag 跑一次正式、一次測試包 半天
合計 約 2 個工作天

會拉長的地方:決策者功課沒在批次前做完,小弟會停下等;GitLab 免費版的 artifact 下載方式(C-2)若不通要換做法。


8. 不做什麼

update-center 與 LC 進管線(update-center 本身的部署照舊手動)、BE/Agent 的 Nuitka 塞進 Dockerfile 變純容器 build、Sigstore keyless(客戶離線驗不了、簽章公開)、雲端 KMS(要換演算法)、客戶主機預設密碼(另案)、推 tag 自動跑(維持人按)、正式包專用 runner(只收受保護 ref 的第二台 runner,見 §3 工作區共用的殘餘風險)。