FR-173 · 設計文件 v2.0 · 定稿 · 2026-11-04
正式發版選定受保護 tag 執行後全自動發布給客戶;直接部署把任一 branch 快速裝到內部三台;測試版發布(Beta)讓內部三台走更新中心測升級。三條路共用一支主設定檔分流,各自一組子設定與參數
🔴 這份是設計決策,不是現況。 本版是完整的設計文件,不必對照 v1.0 閱讀;v1.0(design.md,2026-11-03 定稿)保留供稽核查閱。決策者 2026-11-04 定稿;內文的數字與現況停在當天。
出版管線負責把 Guidant AI 的原始碼變成可以安裝的版本,並送到需要它的主機。本案把這件事從「188 手動出包、決策者 Mac 手動簽章與發布」改成 GitLab CI 自動執行;簽章私鑰存放在自架 Vault,任何人(含管理員)都無法取出。
出版管線有三條路。三條路都從同一支 .gitlab-ci.yml 觸發,由主設定檔依參數 pipeline 分流到各自的子設定檔(§4.9),每條路有自己的參數與流程圖,互不共用 job:
| 路 | 用途 | 入口 | 跑哪些檢查 | 終點 | 人工操作 |
|---|---|---|---|---|---|
| ① 正式發版 | 給客戶 | Run pipeline 選受保護 v* tag,pipeline=release |
全部:測試、弱點掃描;勾選時加跑升級演練 | 自動發布,客戶的更新中心看得到 | 執行 pipeline |
| ② 直接部署 | 改完程式馬上在內部主機看結果 | Run pipeline 選任一 branch,pipeline=deploy,選一台目標主機 |
不跑 | 安裝到所選的那一台(190、STG 或 POC,一次一台) | 執行 pipeline;POC 要再按 ▶ 確認 |
| ③ 測試版發布(Beta) | 測試「透過更新中心升級」是否正常 | Run pipeline 選任一 branch,pipeline=test-release |
不跑 | 更新站上一筆只有內部三台看得到的版本 | 執行 pipeline;到目標主機更新中心按更新 |
內部三台:192.168.50.190(測試機,可隨意使用)、192.168.50.188(STG)、192.168.50.189(POC)。三台都是公司自己的主機。STG、POC 用於內部 Demo 與 UAT,要更新它們代表這個版本已經準備給內部展示;Demo 與 UAT 完成後才會走 ① 正式發版給客戶。
設計目標:
version-bump)建立。不在範圍內:update-center 與 License Center 本身的部署;客戶主機執行期密碼(另案);Nuitka 編譯加速。
FR-167 建立了線上更新通道:188 的 CI 出包、上傳 R2、在更新站登記草稿;決策者在 Mac 上執行 guidant-release publish,以 Mac 上的私鑰簽章並發布。開發期間要讓 190 透過更新中心測升級,由決策者手動執行 .fr167-dev/publish/publish2.sh:從 188 取清單、Mac 以測試金鑰簽章、上傳 R2、直接寫入更新站資料庫。1.27.3~1.28.3、Agent 1.5.1~1.5.5 都是這樣測的。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 G as 188 CI
participant U as 更新站
participant M as 決策者 Mac
P->>G: 手動執行 pipeline
G->>G: 建置、打包、上傳 R2
G->>U: 登記草稿(CI token,未簽章)
P->>M: guidant-release publish <版號>
M->>M: 讀取私鑰(passphrase)、簽署清單
M->>U: 送回已簽章清單,草稿轉為公開
簽章私鑰只在決策者電腦,決策者不在就無法發版;每個版本都要人工簽章與發布,與「自動化」的目標相反。
v1.0(2026-11-03 定稿)把簽章移進 CI、私鑰移進 Vault。實作完成後,決策者於 2026-11-04 審閱時指出三個問題。
正式發版沒有自動化。 v1.0 的正式發版要經過:打 tag、到更新站後台貼版本說明、指定 190 為內部先裝、到 190 更新中心手動升級、回 GitLab 按發布,共五個人工步驟。原因有三:發布被設計成需要人工確認的手動 job;更新站與 CLI 實作時加了「發布前內部先裝清單不能為空、每台都要成功」的硬性檢查(FR-167 設計原本只寫「可指定」);版本說明自動送出沒有完成。
開發測試太慢。 v1.0 的測試包是正式發版 pipeline 的一個參數(internal_test),與正式發版共用 verify、build 等段落。改一個小地方要等完整流程。
內部主機被限縮成只有 190。 v1.0 認為「測試金鑰任何 branch 都能簽,信任它的機器等於信任未經審查的程式碼」,因此測試金鑰公鑰只放 190,deploy_e2e.sh 也禁止測試包裝到 STG、POC。實際上 STG、POC 是公司內部的 Demo 與 UAT 環境,決策者要更新它們時,就代表該版本已準備給內部展示,不需要用金鑰擋。
此外,BE 的 .gitlab-ci.yml 以一個入口、六個參數組合出五種情境(正式、測試包、直接部署 STG/POC、只部署、弱點資料每日發布),每個 job 都要寫規則排除不該跑的情境,難以維護。
| 元件 | 現況(2026-11-04) |
|---|---|
| GitLab | gitlab.com 免費版。secrets:、needs:project 為付費功能,不可用 |
| 建置主機 192.168.50.188 | 24 核、31 GB;shell runner 兩個(auditmanager、commonservice3 群組),concurrent=3;五個 repo clone 在 /opt/;沒有 node |
| Vault 192.168.50.172 | HashiCorp Vault 2.2.1 Community,Transit 引擎;正式簽章金鑰 guidant-release(kid 05086455d42ca0d6)、測試簽章金鑰 guidant-test(kid 128cfec6fc9877b8),皆 exportable=false;JWT 登入已認 gitlab.com;角色 release-ci/test-ci/fe-ci/classifier-ci |
| 更新站 | Cloudflare Worker+D1+R2;版本狀態為草稿/已發布/已撤回;草稿只對「內部先裝清單」上的主機指紋可見;發布端點認 GitLab ID token;只接受印章表登記為 primary/backup 的 kid |
| 客戶端 | 以 /etc/guidant/release-keys/*.pub 驗章,支援多把 kid;正式金鑰公鑰自 1.28.0 起隨安裝包裝入 |
| 主系統建置時間(188 實測) | 完整約 51 分:Nuitka 約 14~19 分、映像弱點掃描與 SBOM 約 9 分、封包壓縮約 13 分 |
| 編號 | 需求 |
|---|---|
| R1 | 正式發版由 Run pipeline 選定受保護 v* tag 觸發,自動執行到客戶可見為止,不需其他人工操作 |
| R2 | 正式發版的版本說明(release note 檔尾三段)由 CI 隨版本送至更新站 |
| R3 | 正式發版可選擇是否執行升級演練(從指定舊版、帶真實資料升級到新版),預設不執行 |
| R4 | 正式簽章只在受保護 v* tag、且由決策者觸發的 pipeline 可取得 |
| R5 | 直接部署:任何 branch 可執行,不跑測試與弱點掃描,一次安裝到一台(190、STG 或 POC);換下一台不必重新建置 |
| R6 | 測試版發布:任何 branch 可執行,不跑測試與弱點掃描,以測試金鑰簽章、登記到更新站,只有內部三台看得到,客戶看不到 |
| R7 | 內部三台皆安裝測試金鑰公鑰;客戶主機不安裝 |
| R8 | 三條路各自一個入口與一組參數,互不影響 |
| R9 | Vault 不可用時,決策者仍可在 Mac 以備用金鑰發布 |
| R10 | Agent 有同樣的三條路 |
| 編號 | 限制 | 來源 |
|---|---|---|
| C1 | GitLab 免費版:無 secrets:、無 needs:project、受保護 tag 只能選角色不能指定人 |
gitlab.com 方案 |
| C2 | 客戶端驗章程式不改;簽章演算法維持 Ed25519 | 已出貨客戶機 |
| C3 | 客戶端一律拒收未簽章的清單 | package_verifier.py |
| C4 | 188 沒有 node;BE 與 Agent 的 Nuitka 不能同時執行 | 188 實測 |
| C5 | 安裝包(主系統約 2.5 GB、Agent 約 1.3 GB)超過 gitlab.com artifacts 1 GB 上限,只能留在 188 | gitlab.com 限制 |
| C6 | 188 工作區由三條路共用(GIT_STRATEGY: none) |
建置時間與磁碟成本 |
| C7 | 更新站以(產品、版號)為鍵,同一版號只有一筆紀錄 | product_releases 表 |
問題:私鑰要離開決策者 Mac 才能無人值守,但不能落到 CI 變數或任何人能讀到的地方。
| 方案 | 做法 | 缺點 |
|---|---|---|
| 自架 HashiCorp Vault Transit | 專用 VM,Transit 引擎存 Ed25519 金鑰,exportable=false;CI 以 OIDC 短效身分請求簽署 |
多一台要維運的主機;要自己做快照 |
| 雲端 KMS(Azure Key Vault/AWS KMS) | 託管金鑰服務 | 不支援 Ed25519,要換 P-256 並改驗章程式,違反 C2 |
| update-center Worker Secret | 私鑰放 Worker 環境變數 | 程式讀得到完整私鑰,不算不可匯出 |
| GitLab CI 變數 | 私鑰放 CI/CD 變數 | 任何 Maintainer 可讀 |
| GitLab Secrets Manager | GitLab 內建 | 需 Premium;不提供簽章 |
決定:自架 Vault Transit,Community 版,單節點 Raft,VM 192.168.50.172。
理由:唯一同時滿足 C2 與「不可匯出」的選項。多一台主機的維運以每日快照與 runbook 承擔。OpenBao(Vault 開源分支)功能相同、API 相容,日後 Vault 授權有變再換。
問題:GitLab 免費版受保護 tag 只能選角色(C1),而五位成員都是 Maintainer 以上,用 GitLab 權限擋不住「別人打 tag 發版」。
決定:Vault 角色 release-ci(受保護 tag)與 test-ci(任何 branch)都以 bound_claims 綁決策者的 GitLab user_id。其他人觸發的 pipeline 在 Vault 登入時即被拒。加人只要改角色的 user_id 陣列,不改程式。
理由:免費版唯一可行的「指定人」做法,判定發生在簽章那一步,正好是最該守的地方。升級 GitLab Premium 可在 GitLab 端指定人,但仍需 Vault 做簽章,費用不划算。
問題:FR-167 的順序是「先登記未簽章草稿、試裝後再簽章發布」。但客戶端一律拒收未簽章的清單(C3),內部主機也是客戶端,未簽章的版本根本裝不上。
決定:打包後立即向 Vault 請求簽署,登記到更新站的就是已簽章的版本。正式發版以正式金鑰簽,測試版發布以測試金鑰簽。
理由:唯一讓「內部主機裝的」與「客戶拿到的」完全是同一份簽章的做法;簽章時機也與「發布」脫鉤,自動發布才可能。
問題:v1.0 在正式發版 pipeline 尾端留一個手動 job「publish」,並要求發布前內部主機先透過更新中心裝過。這讓正式發版多出四個人工步驟,違背本案「自動化」的目的。
| 方案 | 做法 | 缺點 |
|---|---|---|
| 自動發布 | 登記版本後立即呼叫發布,客戶即可見 | 客戶看到之前沒有最後一次人工確認 |
| 保留手動發布 job | v1.0 | 執行之後還要回 GitLab 按一次;pipeline 長時間處於未完成 |
| 發布前強制內部試裝 | v1.0 實作 | 三個人工步驟 |
決定:自動發布。更新站「發布前內部先裝清單不能為空、每台都要成功」的檢查移除。
理由:選定版本 tag 執行正式發版,本身就是「這個版本要給客戶」的決定。在此之前,版本已經過開發期間的直接部署與測試版發布驗證,並在 STG、POC 完成內部 Demo 與 UAT;之後再要求人工確認,等於同一個決定做兩次。發布前的自動把關仍在:只有決策者觸發的受保護 tag 能取得正式簽章;測試與弱點掃描任一失敗就不會走到發布;勾選演練時演練失敗也不會發布。發布後發現問題,可在更新站後台撤回,尚未升級的客戶就不會再看到。
問題:v1.0 每次正式發版都在 190 清機、安裝舊版、匯入 STG 資料、以新包升級、比對資料筆數,約 15~20 分鐘。決策者指出:開發期間已在內部主機測過升級,正式發版時強制再跑一次不合理。
演練模擬的是「客戶停在某個舊版、一次跳到新版、帶真實資料」這條路。開發期間的內部主機是一版接一版升上來,每次只跨一小步,兩者覆蓋的範圍不同;1.21.0 出包前就是靠演練抓到 migration 順序依賴(fr093-4d)。
決定:可選。正式發版參數 rehearse(預設關),勾選時以 rehearse_base(預設 1.27.0)為起點演練,成功才繼續簽章與發布。演練在打包之後執行,使用與出貨相同的安裝包。
理由:要不要花 15~20 分鐘驗跨版升級,是發版人對這一版風險的判斷。建議在本版新增了 migration、且有客戶停在較舊版本時勾選,起點設為客戶目前最舊的版本。
問題:v1.0 的測試包掛在正式發版 pipeline 裡,與正式發版共用段落與參數;而且只能裝 190。決策者要的是兩件不同的事:改完程式馬上在內部主機看結果;以及測試「透過更新中心升級」這條客戶實際會走的路是否正常。
| 方案 | 做法 | 缺點 |
|---|---|---|
| 兩條獨立的路:直接部署、測試版發布 | 各自一個入口;都不跑測試與弱點掃描;三台皆可 | 多兩個入口 |
| 一條路,以參數切換「直接裝」或「上更新站」 | — | 又回到一個入口多種情境 |
| 維持 v1.0 | 測試包是正式發版的參數 | 見 §2.2 |
決定:兩條獨立的路。
install.sh --upgrade 直接安裝到所選主機。不簽章(install.sh 不驗發布清單簽章),不經更新站。最快。理由:兩者驗的東西不同。直接部署驗的是「新功能對不對」,要快;測試版發布驗的是「更新中心升級這條路對不對」,要跟客戶走同一條路。合在一條會讓每次開發測試都多一段上傳與登記。
問題:v1.0 只把測試金鑰公鑰裝在 190,理由是「任何 branch 都能簽測試金鑰,信任它等於信任未經審查的程式碼」,連帶讓開發期間的部署與測試只能用 190。
決定:內部三台(190、STG、POC)都安裝測試金鑰公鑰,直接部署與測試版發布三台都可用。客戶主機不安裝(測試金鑰公鑰不進安裝包)。
理由:三台都是公司的機器。190 可隨意使用;STG、POC 用於內部 Demo 與 UAT,決策者要更新它們本身就是「這版要給內部看」的決定,誰能觸發仍由 Vault 角色綁 user_id 把關。客戶與測試金鑰的隔離不變:測試金鑰簽的版本在更新站只對內部三台可見,且更新站不允許測試金鑰的版本被發布(測試金鑰的 kid 不登記在印章表)。
問題:更新站以(產品、版號)為鍵(C7)。測試版發布若和正式版用同一個版號,兩者會在更新站撞成同一筆紀錄。2026-11-04 實測:以 1.28.3 執行測試版發布,更新站回 409 version_already_published(1.28.3 已在前一天以 Mac 測試章發布過)。
| 方案 | 做法 | 缺點 |
|---|---|---|
測試版用預發版號 X.Y.Z-beta.N |
正式版 X.Y.Z、測試版 X.Y.Z-beta.N,兩者格式不同 |
開發期間每出一次測試版要遞增 N |
| 測試版用尚未正式發布的版號 | 測試版與正式版共用 X.Y.Z,正式發版覆蓋同版號測試草稿 |
版號一旦正式發布就不能再出測試版,要先進版;容易撞號 |
決定:測試版一律用預發版號 X.Y.Z-beta.N,正式版就是 X.Y.Z。
1.29.0-beta.1。PEP 440 的 1.29.0b1 視為格式錯誤(更新站只認 semver)。-beta.N 就停止;正式發版的版號帶 -beta 也停止。1.29.0-beta.1 < 1.29.0-beta.2 < … < 1.29.0;N 以數字比較(beta.10 > beta.9)。主系統升級(upgrade_common.sh)、Agent 升級(upgrade_order.sh)、主系統判斷 Agent 包新舊(package_verifier.py)、更新站(versions.ts)四處採同一套排序。內部主機從 beta.N 升到正式版 X.Y.Z 會被判定為升級。409 version_already_published。理由:正式版與測試版格式不同,永遠不會撞號,也不必為了測一次升級去動正式版號。版號本身就看得出這一版是不是測試版,客戶與內部人員都不會混淆。
問題:v1.0 一支 .gitlab-ci.yml 以六個參數組合五種情境,每個 job 都要寫規則排除不該跑的情境。
決定:GitLab 一個專案只有一支 CI 入口 .gitlab-ci.yml,所以「三個入口」的做法是在主設定檔分流:主設定檔宣告一個下拉式參數 spec.inputs.pipeline(release/deploy/test-release,預設 deploy),Run pipeline 時在畫面上選;主設定檔的 include: 以 rules: 依這個值一次只載入一支路的子設定檔。子設定檔裡的 job 不寫「我屬於哪條路」的規則,因為沒被選到的那幾支根本不會載入。BE、Agent 各自如此。
# .gitlab-ci.yml(示意)
spec:
inputs:
pipeline: { options: [release, deploy, test-release], default: deploy }
---
include:
- local: .gitlab/ci/common.yml # 一律載入
- local: .gitlab/ci/release.yml
rules: [ { if: '"$[[ inputs.pipeline ]]" == "release" && $CI_PIPELINE_SOURCE != "schedule"' } ]
- local: .gitlab/ci/deploy.yml
rules: [ { if: '"$[[ inputs.pipeline ]]" == "deploy" && $CI_PIPELINE_SOURCE != "schedule"' } ]
- local: .gitlab/ci/test-release.yml
rules: [ { if: '"$[[ inputs.pipeline ]]" == "test-release" && $CI_PIPELINE_SOURCE != "schedule"' } ]
- local: .gitlab/ci/vulndb.yml
rules: [ { if: '$CI_PIPELINE_SOURCE == "schedule"' } ]子設定檔放在 .gitlab/ci/,共五支:
| 檔案 | 內容 |
|---|---|
common.yml |
三條路共用的片段(.vault、.sync_repo、.in_repo、.collect):Vault 登入、同步 188 工作樹、進工作樹算版號、收集產物。無條件載入,本身不含 job |
release.yml |
正式發版。「只准受保護 tag」的檢查在這支的 verify job 與 Vault release-ci 角色兩處,不在主設定檔 |
deploy.yml |
直接部署 |
test-release.yml |
測試版發布 |
vulndb.yml |
弱點資料每日發布。只在排程觸發時載入;排程時三條路的子設定檔都不載入 |
觸發方式一律是 GitLab「Run pipeline」(網頁或 glab ci run),推 tag、push 都不會觸發 BE/Agent 的 pipeline(workflow:rules 只收 web/api)。正式發版不用「推 tag 觸發」,原因有二:推 tag 觸發的 pipeline 沒有參數畫面,rehearse 等參數只能用預設;FE 沒有同名 tag,fe_ref 預設的「同名」會找不到。
| 路 | 參數 |
|---|---|
| 正式發版 | rehearse、rehearse_base、fe_ref |
| 直接部署 | target(e2e/stg/poc 單選,預設 e2e)、skip_be、fe_ref、reuse_bundle(不建置,直接拿 188 上同版號的包;換下一台時用) |
| 測試版發布 | skip_be、fe_ref、note(選填,500 字內,一句話說明這版要測什麼) |
直接部署一次一台:target 是單選。一條 pipeline 同時升多台,失敗時看不出是哪一台、也不好追蹤;改成一次一台,要換下一台就再跑一次並勾 reuse_bundle,拿 188 上已打好的同一份包,約 2~5 分。target=poc 時部署 job 是手動 job,pipeline 跑到那一步會停下,按 ▶ 才升級 POC。
版本說明的來源:正式發版一律讀 release note 檔尾「更新中心顯示文字」三段,缺就停止。測試版發布的版本說明是選填:填了 note 就以它為唯一一條版本說明;沒填就讀 release note 檔尾;兩者都沒有就送空白並印警告,照常登記。測試版只給內部看,不必為了測一次升級去寫正式的版本說明。
理由:分流集中在主設定檔一處,同一支子設定檔只有一種情境,job 不必寫排除規則;修改一條路不會影響另一條。另一種做法是所有 job 都載入、每個 job 各自寫 rules: 判斷 pipeline 的值,那就回到 v1.0「每個 job 都要寫一排不跑條件」的問題,因此不採用。用下拉式參數而不是手打變數,是為了在 Run pipeline 畫面上直接選,不會打錯字。
問題:FE pipeline 在 188 建好 FE 映像、推上 Harbor 後隨即刪除本機那份;BE 再從 Harbor 拉回。推、刪、拉都在同一台主機,白走一趟 Harbor,Harbor 不可用時 BE 卡住。盤點 188 上所有跨 pipeline 交接,只有這一處如此。
決定:本機優先、registry 備查。FE 建完不刪本機映像;BE 先查本機有無 guidant-ai-fe:<版號>-<FE commit>,有就直接使用,沒有才拉 Harbor。推 Harbor 照舊。
理由:改動小,不動 pipeline 結構;本機沒有時仍能從 Harbor 取得,runner 日後分機也能運作。
決定:BE、Agent、FE、jedi 各一條 CI,全部跑在 188 shell runner。BE 以 trigger:(strategy: mirror)觸發 FE 建置映像;Agent 主控台前端在 188 對 /opt/guidant-ai-fe 同名 branch 以 FE Dockerfile console-dist 目標打包;jedi 套件 push 時測試、BE 觸發時建置分類映像。BE 與 Agent 的 Nuitka 段以 188 主機鎖 /home/jedi/.guidant-build.lock 串行(GitLab resource_group 只在同一專案內有效,C4)。
理由:各 repo 對自己負責;188 是唯一有 24 核的主機;171 只有 2 核且跑 Nexus、Harbor。
| 項目 | 原因 |
|---|---|
| update-center、License Center 進管線 | 獨立系統,範圍過大 |
| 客戶主機執行期密碼 | Vault 解不了(客戶內網連不到),另案 |
| Nuitka 編譯加速 | 直接部署的主要耗時在此(約 14 分),另案評估 |
| 正式發版專用 runner | 隔離 188 工作區共用風險的根本解法,本案不做;見 §9 |
| Sigstore keyless | 客戶離線無法驗章,且簽章公開 |
| 客製化客戶專屬版本 | 同一套「草稿+指紋清單」機制可讓特定客戶自動拿到專屬版本:專屬版本登記為草稿、清單填該客戶的機器指紋,該客戶的更新中心就會自動看到並升級,其他客戶看不到。差別在簽章要用正式金鑰(客戶機沒有測試公鑰),且更新站要允許正式金鑰的草稿長期只對指定指紋可見。待有客製化客戶時另案 |
本節只做索引,理由在 §4。
| 編號 | 題目 | 定案 | 需求 |
|---|---|---|---|
| D1 | 簽章金鑰 | 自架 Vault Transit,Ed25519,exportable=false |
R4、C2 |
| D2 | 簽章權限 | Vault 角色綁決策者 user_id;release-ci 只認受保護 v* tag |
R4 |
| D3 | 簽章時機 | 打包後立即簽章 | C3 |
| D4 | 正式發版的發布 | 自動發布;移除發布前內部先裝檢查 | R1 |
| D5 | 升級演練 | 正式發版可選,勾選 rehearse 才跑 |
R3 |
| D6 | 開發期間 | 直接部署、測試版發布兩條獨立的路,皆不跑測試與弱點掃描;直接部署一次一台,POC 需按 ▶ 確認 | R5、R6 |
| D7 | 內部主機 | 190、STG、POC 皆可用;三台皆裝測試金鑰公鑰 | R5、R6、R7 |
| D8 | 測試版版號 | 測試版一律 X.Y.Z-beta.N,正式版 X.Y.Z,CI 互相擋 |
C7 |
| D9 | 入口分離 | 主設定檔依 pipeline 參數分流到子設定檔;觸發一律 Run pipeline |
R8 |
| D10 | 版本說明 | CI 隨版本登記一併送出 | R2 |
| D11 | 產物交接 | 同一主機本機優先,registry 備查 | — |
| D12 | 四 repo CI | 各一條;BE 觸發 FE;188 主機鎖串行 Nuitka | R10、C4 |
| D13 | 備用路徑 | Mac 備用金鑰 guidant-release publish --signer file |
R9 |
| D14 | 建置期憑證 | Nexus、Harbor、R2、更新站 CI token、License Center token 全部進 Vault KV;188 磁碟不留明文 | — |
%%{init: {'theme':'base','themeVariables':{'darkMode':false,'background':'#FBFCFC','primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','secondaryColor':'#EEF2F3','tertiaryColor':'#FBFCFC','lineColor':'#4A5A5C','textColor':'#14201F','titleColor':'#14201F','edgeLabelBackground':'#FBFCFC','clusterBkg':'#F6F7F5','clusterBorder':'#C7D1D2','fontSize':'18px'},'flowchart':{'useMaxWidth':false}}}%%
flowchart LR
P["決策者"]
subgraph cloud["雲端"]
GL["GitLab<br/>pipeline、ID token 簽發"]
US["更新站<br/>Worker+D1"]
R2[("R2<br/>安裝包")]
end
subgraph lan["公司內網"]
B["188 建置主機<br/>runner、五個 repo、編譯鎖"]
V["172 Vault<br/>簽章金鑰、建置期憑證"]
N["171 Nexus/Harbor"]
L["189 License Center<br/>防竄改簽章私鑰"]
I["內部三台<br/>190/188 STG/189 POC<br/>正式+測試公鑰"]
end
C["客戶主機<br/>只有正式公鑰"]
P -->|選 tag 或 branch 執行 pipeline| GL
B -->|領取 job| GL
B -->|ID token 登入、請求簽署、讀取憑證| V
V -->|取 JWKS 驗 ID token| GL
B -->|裝套件、推映像| N
B -->|送防竄改清單簽署(防安裝後程式被改)| L
B -->|上傳安裝包| R2
B -->|登記版本、發布(Cloudflare Access 服務憑證)| US
B -->|直接部署:install.sh| I
I -->|檢查更新:已發布+內部草稿| US
C -->|檢查更新:只有已發布| US
I -.->|下載| R2
C -.->|下載| R2
style V fill:#FDECEC,stroke:#B42318,color:#7A1A12
style L fill:#FDECEC,stroke:#B42318,color:#7A1A12
兩種簽章:管線裡有兩種不同用途的簽章,私鑰放在不同地方。
| 防竄改簽章(FR-064) | 出版簽章(本案) | |
|---|---|---|
| 簽什麼 | 映像內每個程式檔的 SHA-256 清單(integrity-manifest.json、Agent 為 manifest.json) |
安裝包的雜湊、大小、版本資訊(發布清單) |
| 誰簽 | License Center(189)的私鑰;建置時送簽 | Vault(172)的正式或測試金鑰 |
| 誰驗、何時驗 | 主系統:每次啟動時驗,執行期間每 4 小時抽查一次。Agent:每次啟動時驗;升級時由舊版先驗過新版才執行 | 主系統或 Agent 安裝前(更新中心下載後、離線上傳時) |
| 防什麼 | 安裝後程式檔被修改 | 安裝包被調包或來源不明 |
| 哪幾條路需要 | 三條路只要重新編譯就需要 | 正式發版(正式金鑰)、測試版發布(測試金鑰);直接部署不需要 |
防竄改清單沒有簽章時,build_image.sh 拒絕打成映像,因此三條路只要重新編譯 BE 或 Agent,都要以 Vault 取 License Center 憑證送簽。
為什麼防竄改清單用 License Center 簽、不用 Vault:FR-064 決策 D2 規定授權檔、防竄改清單、解鎖碼三種憑證共用 License Center 同一把私鑰,產品端用同一把公鑰、同一支驗章程式(verify_payload)驗。要繞過防竄改就得改驗章程式,而改壞它授權也一起失效,系統直接鎖住(「拆一個等於拆兩個」)。若防竄改改由 Vault 簽,就變成兩把鑰匙、兩條驗章路徑,可以只拆防竄改而授權照常運作;已出貨客戶機也要先升一版才認得新鑰。因此維持 License Center 簽。提高這把鑰匙保護的方向見 §6.7。
CI 呼叫更新站後台端點:更新站的 /admin/*(登記版本、發布)前面有 Cloudflare Access。CI 呼叫時除了原本的身分(登記用 CI token、發布用 GitLab ID token)外,還要帶 Cloudflare Access 服務憑證(Service Token,請求標頭 CF-Access-Client-Id、CF-Access-Client-Secret),才通得過 Access。服務憑證存在 Vault kv/ci/update-site,Access 應用程式加一條「服務驗證」原則,指定 token guidant-ci。更新站程式收到 Authorization 時只認 CI token 或 GitLab ID token,不看 Access 傳來的身分,所以程式不必改。不用 Access 的 Bypass(整條路徑放行)規則,是因為 Bypass 會讓任何人都打得到後台端點,只剩更新站程式那一層把關。
更新站對兩種主機回不同的版本:客戶主機只看得到已發布的版本;內部三台的主機指紋在內部清單上,額外看得到測試版發布登記的草稿。內部三台同時信任正式與測試兩把公鑰,客戶主機只信任正式公鑰。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 G as GitLab 正式發版 pipeline(188 runner)
participant F as FE pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant L as License Center(189)
participant H as Harbor/R2
participant U as 更新站
participant T as 190 測試機
participant C as 客戶主機
P->>G: Run pipeline,選受保護 tag v1.29.0(pipeline=release,可填 rehearse/rehearse_base/fe_ref)
G->>G: verify:確認受保護 tag、同步 188 工作樹、守衛測試、pytest、原始碼與相依套件弱點掃描
G->>F: 觸發 FE pipeline(strategy: mirror)
F-->>G: FE 映像建於 188 本機(tag 帶 FE commit),並推 Harbor
G->>V: 以 ID token 登入(role=release-ci),讀取 License Center/Harbor/R2/更新站憑證
G->>G: Nuitka 編譯(188 編譯鎖)
G->>L: 送防竄改清單簽署(FR-064)
L-->>G: 清單簽章
Note over G,L: 防竄改:程式啟動時逐檔比對雜湊,被改過就拒絕啟動
G->>G: BE/init/classifier 映像、取本機 FE 映像
G->>G: 檢查版本說明:release note 檔尾要有「更新中心顯示文字」三段,缺就停
G->>G: 映像弱點掃描(Trivy、Grype):有白名單外的 Critical 漏洞就停
G->>G: 打包:映像+安裝腳本+設定 → 客戶安裝用的 guidant-ai-<版號>.tar.gz
opt 發版人勾選 rehearse
G->>T: 演練:清機、安裝 rehearse_base 版、匯入 STG 資料、以新包升級、比對筆數
T-->>G: 升級成功
end
G->>H: 推映像(tag 帶 commit)、上傳安裝包
G->>V: transit/sign guidant-release(canonical 清單)
V-->>G: 簽名(私鑰不離開 Vault)
G->>U: 登記版本:已簽章清單+版本說明三段
G->>U: 發布(GitLab ID token)
U->>U: 核:BE 專案、受保護 tag、tag=v+版號、kid 為印章表 primary/backup
U-->>G: published
C->>U: 檢查更新
U-->>C: 1.29.0(正式金鑰簽章)
正式發版只有第 1 步是人工。演練與出貨用的是同一份安裝包。任一段失敗(測試、弱點放行門、演練、簽章、上傳),pipeline 即停止,不會走到發布。發布後若發現問題,到更新站後台撤回該版,尚未升級的客戶就不會再看到。
權限把關:從 branch 觸發時,verify 在第 2 步失敗(「正式發版只能從受保護的 v* tag 執行」);即使繞過,Vault 的 release-ci 角色只認受保護 tag,登入被拒。非決策者觸發時同樣在 Vault 登入被拒,訊息為「這個 pipeline 不是由允許發版的人觸發」。
188 工作區共用的防護(C6):三條路共用 /opt/guidant-ai-be、.venv、.build/image-cache/。開發期間任一 branch 都可能留下被改過的快取,正式發版不信任它們:不允許 skip_be;.venv 先 poetry sync 回 lock 檔;image-cache 只讀不寫,載入後比對 image ID,不符即丟棄重匯。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 D as 開發者
participant G as GitLab 直接部署 pipeline(188 runner)
participant F as FE pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant L as License Center(189)
participant I as 目標主機(190、188 STG 或 189 POC 其中一台)
D->>G: 任一 branch,pipeline=deploy,target 選一台(e2e/stg/poc);可選 skip_be、reuse_bundle
alt reuse_bundle
G->>G: 取 188 上同版號既有安裝包
else skip_be
G->>G: 同步 188 工作樹;沿用 188 上同版號 BE 映像
G->>F: 觸發 FE pipeline
F-->>G: FE 映像建於 188 本機
G->>G: 打包(不掃描、不檢查正式公鑰)
else 一般
G->>G: 同步 188 工作樹
G->>F: 觸發 FE pipeline
F-->>G: FE 映像建於 188 本機
alt 188 已有本次程式內容的 BE 映像(標記 src-<commit>)
G->>G: 沿用,不重新編譯
else 程式有改
G->>V: 以 ID token 登入(role=test-ci),讀取 License Center 憑證
G->>G: Nuitka 編譯(188 編譯鎖)
G->>L: 送防竄改清單簽署(FR-064)
L-->>G: 清單簽章
Note over G,L: 防竄改:程式啟動時逐檔比對雜湊,被改過就拒絕啟動
end
G->>G: BE/init/classifier 映像、打包(不掃描、不檢查正式公鑰)
end
opt target=poc
G-->>D: 停在部署 job,等待按 ▶
D->>G: 按 ▶ 確認
end
opt 目標為 STG 或 POC
G->>I: 停 api/socketio/worker;確認現行授權 kid 在新版公鑰表內
end
G->>I: scp 安裝包、install.sh --upgrade --force
I-->>G: 升級結果
直接部署走 install.sh --upgrade,不驗發布清單簽章,所以不需要出版簽章。重新編譯 BE 時仍要送 License Center 簽防竄改清單(沒有這份簽章的映像無法啟動),因此要以 test-ci 登入 Vault 取 License Center 憑證;skip_be 與 reuse_bundle 不需要。STG、POC 升級前的兩道檢查沿用 deploy_e2e.sh 既有邏輯:先停 api 等服務(避免 migration 與舊版 api 搶鎖死鎖)、確認現行授權的 kid 在新版公鑰表內(避免升級後整站鎖住)。
同一份程式不重編:直接部署與測試版發布建置 BE 映像時,會多標一個 src-<最後一次改到程式的 commit>(不計 docs、.gitlab/、.gitlab-ci.yml、test 的改動)。188 本機已有同標記的映像,就跳過 Nuitka 編譯與防竄改送簽,直接沿用。只改文件或 CI 設定時不必重編。skip_be 優先於這個判斷。正式發版不吃這個判斷,永遠重新編譯。
時間估計(主系統):程式有改約 30 分(2026-11-04 實跑 pipeline 3025779481,190 升到 1.28.3;Nuitka 約 14~17 分、封包約 13 分);程式沒改(src- 命中)或 skip_be 約 10~13 分;reuse_bundle(包已存在,只換目標主機)約 2~5 分。
STG、POC 的 Agent 不另外停服務:Agent 直接部署時,升級腳本遇到執行中的工作會自動取消,所以不比照主系統先停 api。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 D as 開發者
participant G as GitLab 測試版發布 pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant L as License Center(189)
participant H as R2
participant U as 更新站
participant I as 內部主機(190/188 STG/189 POC)
D->>G: 任一 branch,pipeline=test-release,版號須為 X.Y.Z-beta.N(可選 skip_be、fe_ref、note)
G->>V: 以 ID token 登入(role=test-ci),讀取 License Center/R2/更新站憑證
G->>G: 同步 188 工作樹、觸發 FE;BE 映像同直接部署(src- 標記命中或 skip_be 時不重編)
opt 重新編譯 BE
G->>L: 送防竄改清單簽署(FR-064)
L-->>G: 清單簽章
Note over G,L: 防竄改:程式啟動時逐檔比對雜湊,被改過就拒絕啟動
end
G->>G: 映像、打包(不掃描、不檢查正式公鑰)
G->>H: 上傳安裝包
G->>V: transit/sign guidant-test
V-->>G: 測試金鑰簽名
G->>U: 登記版本:測試金鑰簽章清單+版本說明(note,或 release note 檔尾,或空白);內部清單自動帶入
Note over U: 草稿,只有內部三台看得到;測試金鑰的版本不可發布
D->>I: 到要測的那台更新中心,按「更新到 X」
I->>U: 檢查更新、取得下載網址
I->>H: 下載安裝包
I->>I: 以本機測試公鑰驗章、八步升級(失敗自動退回)
I->>U: 回報升級結果
命名:對外(PM、業務)稱「Beta 版」。它不是給客戶試用的公開測試版,目前只開放給公司內部三台。
誰拿得到:主機向更新站檢查更新時,帶上更新站身分票(update_ticket,License Center 在授權開通時另簽的簽章信封,只含授權編號、機器指紋、主版號、kid,不含客戶名稱;FR-167 決策 D9)與機器指紋;還沒有身分票的舊主機改送授權檔原文。更新站先驗身分票簽章、確認身分票記載的指紋與送來的一致(防冒用別台指紋)、確認授權編號不在拒絕名單,再用指紋比對每個草稿的「內部主機清單」:指紋在清單上,回傳的版本包含 Beta 版;不在清單上,只回傳正式版(update-center/src/routes/client.ts authenticate、src/releases.ts visibleReleases)。身分票只用來證明「你是這台機器」,不需要另發 Beta 專用憑證。清單存在更新站資料表 internal_hosts,在後台「內部主機」分頁維護;只有測試金鑰簽的草稿會自動套用清單,正式金鑰的版本不套。
現況限制:190、188、189 三台的 /etc/machine-id 相同,算出的主機指紋都是同一個,所以清單目前只要登記一筆三台就都看得到;但三台的升級結果回報會互相覆蓋,後台無法分辨是哪一台。正式出 1.29.0 前要在三台重新產生 machine-id(見 §7.3),之後清單改為三筆。
測試版發布測的是客戶實際的升級路徑:下載、驗章、停接新工作、複製資料庫、在複本上 migration、健康檢查、換接、清理,以及失敗時的自動退回。直接部署與正式發版的演練都走 install.sh,測不到這段。
客戶看不到、裝不了、也發布不出去:版本狀態是草稿,更新站只回給內部清單上的主機指紋;即使草稿外流,客戶主機沒有測試金鑰公鑰,驗章失敗拒絕安裝;更新站也拒絕發布測試金鑰簽章的版本(kid_not_registered)。
版號:一律 X.Y.Z-beta.N(§4.8),每出一次測試版遞增 N,內部主機更新中心就會把新的 beta.N 判定為升級。同一個 beta.N 重跑時覆蓋同一筆草稿;內部主機要重裝同一版,用「同版重套」開關(PRODUCT_UPDATE_ALLOW_SAME_VERSION=1)。
Agent(auditmanager/evidence-agent)有自己的三條路,入口與參數比照主系統,各自獨立觸發。與主系統最大的差別在升級方式:Agent 不直接連更新站。客戶與內部主機的 Agent 一律由所屬的主系統向更新站下載 Agent 安裝包、驗章,再推送給 Agent;Agent 收到後寫升級單,由 Agent 主機的 systemd 執行升級。因此 Agent 的「透過更新中心升級」是在主系統的更新中心 Agent 區操作。
| 項目 | 主系統 | Agent |
|---|---|---|
| 本機資料庫與 migration | PostgreSQL;升級時先複製資料庫,在複本上跑 migration,失敗丟棄複本 | SQLite 單檔 agent.db;migration 由各模組 migrations.py 申報、以 schema_version 記版本,新容器啟動時直接在原檔上執行,沒有複本 |
| 升級演練 | 正式發版可選 | 正式發版可選(同主系統,勾選 rehearse) |
| 主控台前端 | FE pipeline 建置映像 | 188 對 /opt/guidant-ai-fe 同名 branch(或 fe_ref)以 FE Dockerfile console-dist 目標打包 |
| 離線檔 | 無 | 正式發版簽章後產 guidant-agent-<版號>.offline.tar.gz,與安裝包一起上傳 R2 agent/v1/<版號>/。內容與 R2 上的安裝包、清單、雜湊三個檔相同,只是包成一個檔:Agent 與主系統斷線時,現場人員在 Agent 主控台「離線上傳更新包」只能上傳一個檔。需要時由更新站後台取得 15 分鐘有效的預簽下載網址給客戶(沿用客戶主機下載安裝包用的同一套 presignDownload,不新增金鑰或機制) |
| 直接部署的安裝方式 | install.sh --upgrade(deploy_e2e.sh) |
guidant-agent-compose upgrade <包>(deploy_agent_e2e.sh,目標由寫死 190 改為 target 單選三台之一) |
| 更新站的版本線 | product=host |
product=agent;R2 路徑 agent/v1/<版號>/ |
| 測試版發布的版本說明 | 選填,note 或 release note 檔尾 |
同主系統:選填,note 或 RELEASE_NOTES.md 本版段 |
| 發布順序 | — | 主系統某版要求新 Agent 時,先發 Agent、再發主系統(主系統清單的 min_agent_version 指向它) |
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 G as GitLab Agent 正式發版 pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant L as License Center(189)
participant H as Harbor/R2
participant U as 更新站
participant T as 190 測試機
participant M as 客戶主系統
participant A as 客戶 Agent
P->>G: Run pipeline,選受保護 tag v1.6.0(pipeline=release,可填 rehearse/rehearse_base/fe_ref)
G->>G: verify:同步 188 工作樹、守衛測試、pytest、相依套件弱點掃描
G->>G: build:主控台前端(FE console-dist)、Nuitka 編譯(188 編譯鎖)
G->>V: 以 ID token 登入(role=release-ci),讀取 License Center/Harbor/R2/更新站憑證
G->>L: 送防竄改清單簽署(FR-064)
L-->>G: 清單簽章
Note over G,L: 防竄改:程式啟動時逐檔比對雜湊,被改過就拒絕啟動
G->>G: 建 Agent 映像
G->>G: 映像弱點掃描(Trivy、Grype):有白名單外的 Critical 漏洞就停
G->>G: 打包:映像+compose 設定+安裝腳本 → 客戶安裝用的 guidant-agent-compose-<版號>.tar.gz
G->>G: 檢查版本說明:RELEASE_NOTES.md 要有這一版的條目,缺就停
opt 發版人勾選 rehearse
G->>T: 演練:清除 Agent、安裝 rehearse_base 版、放入帶資料的 agent.db、以新包升級
T-->>G: 新容器 healthy、schema_version 到最新、資料筆數不變
end
G->>V: transit/sign guidant-release
V-->>G: 簽名
G->>G: 產離線檔 guidant-agent-1.6.0.offline.tar.gz(安裝包+簽好的清單+雜湊,包成一個檔)
G->>H: 推映像;上傳 agent/v1/1.6.0/(安裝包、清單、雜湊、離線檔)
G->>U: 登記版本(product=agent)+版本說明
G->>U: 發布(GitLab ID token)
U-->>G: published
M->>U: 查 Agent 線,看到 1.6.0
Note over M,A: 管理員在主系統更新中心 Agent 區按更新,或 Agent 主控台按「更新到 1.6.0」
M->>H: 下載 Agent 安裝包、驗章、核雜湊
M->>A: 推送 POST /update
A->>A: 下載包、寫升級單;Agent 主機升級、健康檢查,回報結果
Agent 演練樣本:主系統演練用 STG 的唯讀資料匯出,Agent 沒有對應來源,因此在 188 固定放 .build/rehearse/agent-<舊版號>.db。第一份由決策者從 190 現行版 Agent 匯出,之後每次 Agent 進版順手留一份。
Agent 演練:勾選 rehearse 時,在 190 清除 Agent(保留 Agent 憑證與弱點資料庫兩個 volume,免得重新註冊)、安裝 rehearse_base 指定的舊版 Agent、放入 188 上的樣本資料庫 .build/rehearse/agent-<舊版號>.db,以新包執行 guidant-agent-compose upgrade,確認整個 Agent compose 專案(含 seaweedfs)healthy、schema_version 等於新版 migration 的最後一版、upload_files 與 local_runs 筆數不變,才繼續簽章與發布。演練只准在 190 執行:腳本寫死主機,指到其他主機一律拒絕,避免清掉 STG、POC 的 Agent。Agent 的 migration 在原檔上執行、退回時不還原 agent.db(見 §9),跨版 migration 出錯的代價比主系統更高,本版有動到 Agent migration 時建議勾選。
離線檔與 R2 上的安裝包是同一份內容,只多包一層;選擇另存一份而不是需要時由更新站即時打包,是因為 R2 儲存成本低(每版約 1.3 GB),而在 Worker 上即時打包 1.3 GB 的檔有執行時間與記憶體上限的風險。客戶端的升級在發布之後由客戶自行觸發(主系統更新中心或 Agent 主控台),與主系統相同。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 D as 開發者
participant G as GitLab Agent 直接部署 pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant L as License Center(189)
participant I as 目標 Agent 主機(190、188 STG 或 189 POC 其中一台)
D->>G: 任一 branch,pipeline=deploy,target 選一台(e2e/stg/poc);可選 reuse_bundle
alt reuse_bundle
G->>G: 取 188 上同版號既有 compose 安裝包
else 一般
G->>G: 同步 188 工作樹、主控台前端、Nuitka 編譯
G->>V: 以 ID token 登入(role=test-ci),讀取 License Center 憑證
G->>L: 送防竄改清單簽署(FR-064)
L-->>G: 清單簽章
Note over G,L: 防竄改:程式啟動時逐檔比對雜湊,被改過就拒絕啟動
G->>G: Agent 映像、compose 安裝包(不掃描)
end
opt target=poc
G-->>D: 停在部署 job,等待按 ▶
D->>G: 按 ▶ 確認
end
G->>I: scp 安裝包、核 sha256
G->>I: guidant-agent-compose upgrade <包>
I-->>G: 整個 Agent compose 專案轉為 healthy
防竄改清單簽章是映像建置的必要步驟(與主系統相同,見 §6.1),所以直接部署仍要登入 Vault 取 License Center 憑證;出版簽章則不需要。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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 D as 開發者
participant G as GitLab Agent 測試版發布 pipeline(188 runner)
participant V as Vault(192.168.50.172)
participant H as R2
participant U as 更新站
participant M as 內部主系統(190/STG/POC)
participant A as 該主機的 Agent
D->>G: 任一 branch,pipeline=test-release,版號須為 X.Y.Z-beta.N(可選 fe_ref、note)
G->>G: 同步 188 工作樹、主控台前端、Nuitka、防竄改清單簽署(License Center)、Agent 映像、compose 安裝包(不掃描)
G->>V: 以 ID token 登入(role=test-ci),讀取 R2/更新站憑證
G->>H: 上傳 agent/v1/<版號>/
G->>V: transit/sign guidant-test
V-->>G: 測試金鑰簽名
G->>U: 登記版本(product=agent);內部清單自動帶入三台
Note over U: 草稿,只有內部三台的主系統看得到;測試金鑰的版本不可發布
D->>M: 到該主系統更新中心 Agent 區,選 Agent,按更新
M->>U: 查 Agent 線、取得下載網址
M->>H: 下載 Agent 安裝包
M->>M: 以本機測試公鑰驗章、核雜湊
M->>A: 推送 POST /update
A->>A: 下載包、寫升級單;Agent 主機升級、健康檢查
A-->>M: 回報每步進度與結果
Agent 測試版發布驗的是「主系統下載 Agent 包、驗章、推送、Agent 主機升級、失敗自動還原」這整段,直接部署測不到。內部清單填的是主系統的主機指紋:Agent 包由主系統向更新站取得,該主機底下任一台 Agent 升級成功即算。Agent 主控台「更新到 X」也可觸發,效果相同(請主系統替這一台做第 9 步起的動作)。驗章用的是主系統主機上的測試公鑰(/etc/guidant/release-keys/),Agent 本身不需要另裝;Agent 主控台的「離線上傳」只信 Agent 安裝包內附的正式公鑰,所以測試版不能走離線上傳。
本節回答兩個問題:CI 本身不保存任何密碼,怎麼取得簽章權限與建置期憑證;以及為什麼只有決策者觸發的 pipeline 能發版。做法是每個 job 開始時由 GitLab 簽發一張短效的身分憑證(ID token),Vault 核對憑證上的專案、ref 類型、是否受保護、觸發者帳號,符合角色條件才發給 30 分鐘的通行 token;GitLab 免費版無法限定「只有某人能打 tag」,因此「只有決策者能發版」由 Vault 核對觸發者帳號來把關。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','lineColor':'#4A5A5C','textColor':'#14201F','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#F6EBD5','labelBoxBorderColor':'#9C6B12','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
participant G as GitLab
participant J as 188 上的 CI job
participant V as Vault(192.168.50.172)
G->>J: 啟動 job,附 ID token(aud=Vault 網址,含 project_path、ref_type、ref_protected、user_id)
J->>V: POST auth/jwt/login(ID token、role)
V->>G: 取 JWKS 驗 ID token 簽章
V->>V: 核 bound_claims:專案、ref 類型、受保護、user_id
V-->>J: 短效 token(30 分鐘)
J->>V: GET kv/data/ci/*(依角色 policy)
J->>V: GET transit/keys/<鑰名>(比對 CLI 釘住的公鑰與 kid)
J->>V: POST transit/sign/<鑰名>
V-->>J: 簽名
J->>V: POST auth/token/revoke-self
| 角色 | 給誰 | bound_claims | policy |
|---|---|---|---|
release-ci |
BE、Agent 正式發版 | 兩專案、受保護 tag、決策者 user_id |
簽 guidant-release、讀 kv/ci/* 全部(含 kv/ci/update-site 的更新站 CI token 與 Cloudflare Access 服務憑證) |
test-ci |
BE、Agent 直接部署與測試版發布 | 兩專案、任何 branch、決策者 user_id |
簽 guidant-test;讀 Nexus、Harbor、License Center、R2、更新站(後兩項本版新增,測試版發布要上傳與登記;更新站一項含 Cloudflare Access 服務憑證) |
fe-ci |
FE | FE 專案任何 job | 讀 Nexus |
classifier-ci |
jedi 套件 | jedi 專案 | 讀 Harbor |
簽章 CLI 在簽署前把 Vault 回的公鑰與程式內釘住的 kid 比對,不同即拒簽;簽完以釘住的公鑰自驗。
金鑰輪換:Vault transit/keys/guidant-release/rotate 產新版本 → 新公鑰存為 scripts/installer/release-keys/<新 kid>.pub、加入 PROD_KIDS、更新 CLI 釘住的表 → 隨一版主系統裝上客戶機 → 下一版起以新版本簽 → 舊版本設 min_decryption_version 停用。
jedi(docker 群組,等同 root)執行任意指令。BE、Agent 的 pipeline 只有決策者能取得 Vault 權限;FE 每次 push 都會在 188 執行。根本解法是另開一台只收受保護 ref 的 runner,本案不做;FE 專案日後若加入非 Maintainer 成員,要先把 FE 的 verify 移出 188。guidant-ai 專案下任何映像。緩解:CI 一律推帶 commit 的 tag;日後可開 Harbor 的 tag 不可覆寫。kv/ci/update-site。V-1(簽章 CLI 接 Vault、更新站收已簽章草稿、GitLab 身分發布)、C-3/C-3b(FE CI、Dockerfile console-dist)、C-1(BE CI)、C-2(Agent CI)、C-4(jedi CI)已驗收合回 feature/FR-172。第二輪批 4(CM-3481 BE 觸發 FE、CM-3482 Agent CI 兩處、CM-3483 seaweedfs init: true)與本版不衝突,可照常進行;批 5 CM-3484(拿掉 deploy-stg/deploy-poc)由本版的 CI 改寫取代。
| repo | 檔案 | 異動 | 依據 | 卡 |
|---|---|---|---|---|
| update-center | src/releases.ts |
發布移除「內部先裝清單不能為空、每台須成功」檢查;測試金鑰(未登記 kid)的版本仍不可發布;已發布版號拒絕測試版登記(409 version_already_published);versions.ts 依 semver 排序 -beta.N |
D4、D8 | CM-3485 U-1 |
| update-center | src/routes/admin.ts register-draft |
接受版本說明三段;測試金鑰簽章的版本自動套用內部主機清單 | D7、D10 | CM-3485 U-1 |
| update-center | 內部主機清單 | 新增設定(後台維護三台主機指紋),測試版發布自動套用:資料表 internal_hosts、後台「內部主機」分頁 |
D7 | CM-3485 U-1 |
| update-center | cli/commands.ts publish |
移除「內部先裝清單不能為空」,與 Worker 一致(Mac 備用路徑) | D4、D13 | CM-3485 U-1 |
| BE | .gitlab-ci.yml |
改為分流主檔(參數 pipeline);子設定檔 .gitlab/ci/common.yml/release.yml/deploy.yml/test-release.yml/vulndb.yml |
D9 | CM-3490 B-1 |
| BE | release.yml |
正式發版:verify、fe、build、package、(勾選)rehearse、sign、upload、register、publish 一路自動;刪 deploy-stg/deploy-poc/environment/internal_test;演練移到打包之後、用同一份包 |
D4、D5 | CM-3490 B-1 |
| BE | deploy.yml |
直接部署:target 單選一台(POC 為手動 job)、skip_be、reuse_bundle、src- 標記命中不重編;不簽章 |
D6、D7 | CM-3490 B-1 |
| BE | test-release.yml |
測試版發布:build、package、測試金鑰簽章、上傳 R2、登記草稿 | D6、D8 | CM-3490 B-1 |
| BE | scripts/build/deploy_e2e.sh |
移除「內部測試包只准 e2e」檢查 | D7 | CM-3486 B-2 |
| BE | 測試版發布版號檢查 | 第一步擋不帶 -beta.N 的版號;正式發版擋帶 -beta 的版號;upgrade_common.sh 排序支援 -beta.N |
D8 | CM-3494 |
| BE | scripts/build/publish_draft.sh |
登記時帶版本說明;正式發版登記後呼叫發布;發布邏輯只留這一份(--publish、--publish-only),publish_release.sh 縮成轉呼叫,CI 改寫後可刪 |
D4、D10 | CM-3486 B-2 |
| BE | scripts/build/pull_fe_image.sh |
本機優先,沒有才拉 Harbor(2026-11-04 直接部署實跑確認取本機) | D11 | CM-3486 B-2 |
| FE | scripts/ci/build_image.sh |
推 Harbor 後不刪本機映像 | D11 | CM-3489 F-1 |
| Agent | .gitlab-ci.yml 與三支子設定檔 |
同 BE;rehearse-upgrade 改為勾選 rehearse 才跑,移到打包之後 |
D5、D9、§6.5 | CM-3491 A-1 |
| Agent | scripts/build/deploy_agent_e2e.sh 演練模式 |
新增 --base-version:清除 190 的 Agent、安裝舊版、放入帶資料的 agent.db、以新包升級、核 schema_version 與資料筆數(比照 BE deploy_rehearse.sh) |
D5 | CM-3487 A-2 |
| Agent | scripts/build/deploy_agent_e2e.sh |
目標由寫死 190 改為 target 單選三台之一 |
D7 | CM-3487 A-2 |
| Agent | 測試版發布版號檢查、版本說明選填 | 同 BE;upgrade_order.sh 排序支援 -beta.N;測試版發布加 note,版本說明改選填 |
D8、D10 | CM-3495 |
| Agent | scripts/build/publish_draft.sh |
同 BE;正式發版時離線檔一併上傳 R2 agent/v1/<版號>/;拔掉獨立的 publish 子命令,只留 register --publish |
D4、D10 | CM-3488 A-3 |
| update-center | 後台版本詳情 | Agent 版本多回 offline_download_url:沿用 presignDownload 對離線檔產 15 分鐘預簽網址;後台加「取得離線檔連結」按鈕;register-draft 回應補 upload.offline(原本沒回,Agent 有離線檔也傳不上去) |
§6.5 | CM-3492 U-2 |
卡片批次與驗收條件見 handoff/cards_r3.py。依賴:update-center 的改動要先部署,BE、Agent 的正式發版與測試版發布才能驗;BE、Agent、FE 之間互不依賴。
| 時機 | 項目 |
|---|---|
| CI 改寫前 | Vault ci-test policy 加讀 kv/ci/r2、kv/ci/update-site;測試金鑰公鑰 128cfec6fc9877b8.pub 放到 188 STG、189 POC 的 /etc/guidant/release-keys/;STG、POC 的 guidant.env 開啟 PRODUCT_UPDATE_ALLOW_SAME_VERSION=1(Agent 為 AGENT_UPDATE_ALLOW_SAME_VERSION=1);在更新站後台登記三台主機指紋為內部主機清單;Cloudflare Access 建立服務憑證 guidant-ci,Access 應用程式加「服務驗證」原則,憑證存 Vault kv/ci/update-site(cf_access_client_id、cf_access_client_secret),有到期日,到期前要換 |
| CI 改動驗完後 | 刪 188 上明文副本(/opt/guidant-ai-*/.env 的 License Center token、~/.fr167-r2.env、~/.npmrc)、BE 專案 GitLab 變數的 R2 與 Bugsink;License Center 換新 token 放進 Vault |
| 正式出 1.29.0 前 | Vault 每日快照+異地備份、防火牆;189 POC 升到 1.28.x 以上;部署 update-center Worker(含本版改動)、後台登記 05086455d42ca0d6 為 primary、Cloudflare Access 放行發布端點;內部三台重新產生 /etc/machine-id(目前三台相同,升級回報互蓋),後台內部主機清單改為三筆;印章表目前是空的,沒登記正式章前正式發版的自動發布會被 kid_not_registered 擋下 |
| # | 做什麼 | 預期結果 | 需求 |
|---|---|---|---|
| 1 | 從受保護 tag 執行正式發版,不勾 rehearse |
不跑演練;一路到發布;更新站 published、kid 05086455d42ca0d6、帶版本說明三段;客戶主機檢查更新看得到 |
R1、R2 |
| 2 | 同上,勾 rehearse、rehearse_base 填 1.27.0 |
190 清機裝 1.27.0、匯入 STG 資料、升級成功後才簽章發布 | R3 |
| 3 | 同上,演練失敗 | pipeline 停止,更新站沒有這一版 | R3 |
| 4 | 從 branch 執行正式發版 | verify 失敗;不簽章、不登記 | R4 |
| 5 | 非決策者從受保護 tag 執行 | Vault 登入被拒,印「不是由允許發版的人觸發」 | R4 |
| 6 | 直接部署,target 依序選 e2e、stg、poc(後兩次勾 reuse_bundle) |
每次只升級所選那一台;poc 停在部署 job 等按 ▶;STG、POC 升級前 api 已停、授權 kid 檢查通過;pipeline 無測試與弱點掃描 job |
R5、R7 |
| 7 | 直接部署,勾 skip_be |
不跑 Nuitka;約 10 分完成 | R5 |
| 8 | 直接部署,勾 reuse_bundle |
不建置,直接以 188 上既有包升級 | R5 |
| 9 | 測試版發布 | 更新站有一筆測試金鑰簽章的草稿,內部清單為三台;R2 有安裝包 | R6 |
| 10 | 測試版發布後,190、STG、POC 各自從更新中心升級 | 三台都看得到、驗章通過、升級成功並回報 | R6、R7 |
| 11 | 測試版發布後,非內部主機檢查更新 | 看不到該版 | R6 |
| 12 | 對測試金鑰的草稿呼叫發布 | 更新站回 kid_not_registered,維持草稿 |
R6 |
| 13 | 以不帶 -beta.N 的版號執行測試版發布;以帶 -beta 的版號執行正式發版 |
兩者都在第一步停止 | D8 |
| 14 | 內部主機先裝 1.29.0-beta.2,再正式發版 1.29.0 |
內部主機更新中心判定 1.29.0 較新,可升級 |
D8 |
| 15 | BE 與 Agent 同時執行 | 第二條在 Nuitka 段排隊,不並行 | C4 |
| 16 | Agent 正式發版,不勾 rehearse |
不跑演練;發布後客戶主系統 Agent 區看得到新版;R2 agent/v1/<版號>/ 有離線檔,更新站後台取得的下載連結可下載;在一台 Agent 主控台以「離線上傳更新包」上傳該檔,升級成功 |
R10 |
| 16a | Agent 正式發版,勾 rehearse |
190 裝回舊版 Agent、放入帶資料的 agent.db、升級後 schema_version 到最新、資料筆數不變,才簽章發布 |
R3、R10 |
| 16b | Agent 直接部署,target 依序選三台 |
每次只升級所選那一台的 Agent、整個 Agent compose 專案 healthy;poc 要按 ▶ |
R5、R10 |
| 16c | Agent 測試版發布後,在三台主系統的更新中心 Agent 區推送 | 主系統下載、驗章(測試公鑰)通過,Agent 升級成功並回報 | R6、R10 |
| 17 | 關閉 Vault,Mac 執行 publish --signer file |
發布成功 | R9 |
| 18 | 正式發版 build 段 log | FE 映像取本機,無 docker pull |
D11 |
| 19 | 188 檢查 | grep -r "R2_SECRET|_auth=|LICENSE_CENTER_API_TOKEN" ~/.npmrc /opt/guidant-ai-*/.env 為 0 |
D14 |
| 項目 | 影響 | 緩解 | 處理時機 |
|---|---|---|---|
| 正式發版自動發布,客戶看到前沒有人工確認 | 有問題的版本會直接到客戶 | 測試、弱點放行門、(勾選時)演練任一失敗不發布;正式發版前已經過直接部署、測試版發布與 STG/POC 的 Demo、UAT;發布後可撤回 | 若發生問題版本外流,評估加回「發布前等待 N 小時」的緩衝 |
| 演練改為可選,未勾選時跨版 migration 問題沒有提前驗證 | 問題出現在客戶端;線上升級失敗會自動退回,客戶看到「升級失敗」 | 本版新增 migration 時建議勾選 | 若出現跨版升級事故,改回有 migration 自動執行 |
| 測試金鑰任何 branch 都能簽,內部三台都信任它 | 決策者在別人推的 branch 上觸發,未經審查的程式碼可裝到 STG/POC | test-ci 綁決策者 user_id;觸發前確認 commit;STG/POC 是內部環境 |
— |
| 測試版與正式版排序在四處不一致 | 內部主機從 beta 升正式版被誤判為降版或同版 | 四處共用同一套 semver 排序規則(§4.8) | — |
| 188 工作區共用 | 見 §6.7 | 正式發版不信任共用快取 | 正式發版專用 runner(本案不做) |
| Vault 無快照 | 172 損毀即失去正式金鑰 | Mac 備用金鑰 | 1.29.0 前 |
Agent 升級失敗退回時不還原 agent.db(upgrade_runner.sh 的 rollback 只還原 .env 與 compose 檔) |
新版 migration 已改過 agent.db 後失敗退回,舊版 Agent 可能讀不懂新欄位或新表 |
Agent migration 只補欄位、不刪不改(add_missing_columns);跨版前勾選演練 |
另案(FR-172 升級機制):升級前備份 agent.db、退回時還原 |
design.md(2026-11-03)docs/features/FR-167-2610-online-update-channel/design.mdvault-setup-runbook.md;現況:vault-status.mdupdate-center/cli/README.md、cli/vault_signer.tsupdate-center/src/releases.tsapp/product_update/service/same_version.pydocs/system-design/release-pipeline/(一章一頁,從 01-概述.md 讀起)| v1.0 | 本版 | 為什麼改 |
|---|---|---|
| D9:正式發版尾端手動按發布,且發布前須 190 透過更新中心試裝成功 | D4:自動發布;移除內部先裝檢查 | 本案目的是自動化;選 tag 執行正式發版即代表要給客戶,再要求人工確認是同一個決定做兩次 |
D12:測試包是正式發版 pipeline 的 internal_test 參數,只裝 190 |
D6、D9:直接部署、測試版發布兩條獨立的路,各自入口 | 開發測試被正式發版的段落拖慢;一支設定檔五種情境難以維護 |
測試金鑰公鑰只放 190;deploy_e2e.sh、deploy_agent_e2e.sh 禁止裝 STG/POC |
D7:內部三台都可用、都裝測試公鑰;直接部署一次一台 | 三台都是公司機器,STG/POC 用於內部 Demo 與 UAT;把關由 Vault user_id 承擔 |
| 演練每次正式發版必跑,且在打包前另打一次包 | D5:可選;在打包之後用同一份包 | 開發期間已測過;演練與出貨同一份包也省一次封包 |
正式發版可選 environment=stg/poc,CI 直接部署 |
刪除;直接部署改走 ② | 正式發版只到更新站與客戶 |
測試版發布靠決策者 Mac 手動 publish2.sh |
③ 測試版發布由 CI 執行 | 自動化 |
| 版本說明由人到後台貼(v1.0 C-1 未完成項) | D10:CI 隨版本送出;測試版發布改選填,可用 note 一句話 |
補完 v1.0 已定但未做的事;測試版只給內部看,不必寫正式說明 |
| 測試版與正式版共用版號 | D8:測試版一律 X.Y.Z-beta.N |
實測撞號(409);格式不同就永遠不撞 |
| FE 映像經 Harbor 交接給 BE | D11:本機優先,Harbor 備查 | 同一台主機白走一趟 Harbor |
| Agent 每次正式發版演練(只以新包升級 190 現行版,未從舊版跨版) | 可選;勾選時從 rehearse_base 舊版帶 agent.db 資料跨版升級 |
與主系統一致;Agent 有 SQLite 與 migration,跨版升級要驗,但由發版人判斷是否需要 |
| Agent 離線檔只留在 188 | 一併上傳 R2,後台可取下載連結 | 客戶斷線需要離線檔時,不必再有人登 188 取檔 |