發版流程:腳本在哪、流程怎麼走、什麼時候簽章
本頁範圍:說明一個新版本從程式碼到客戶「更新中心」看得到為止,經過哪些階段、各階段由哪支腳本在哪裡執行、簽章在哪一刻發生。分兩段:現行做法(1.27.x 實際在用)與改版後做法(FR-172 Agent 獨立出版、FR-173 出版管線定稿,設計已定、實作中)。依程式碼、CI 設定與三案設計文件整理,2026-11-02。
設計原則:私鑰不進 CI、更新站沒有私鑰。CI 能做的只到「草稿」,草稿客戶看不到;要讓客戶看得到,一定要經過簽章,簽章的鑰匙放在 CI 拿不出來的地方。客戶主機用安裝時就放好的公鑰驗章,驗不過就拒裝。
一、現行做法(1.27.x)
發版分三個階段,腳本放在兩個 repo,在三個地方執行:
| 階段 | 做什麼 | 腳本 | 在哪裡跑 | 拿得到的祕密 |
| ① 出包 | 編譯(Nuitka)、弱點掃描、打包成約 5 GB 的 tar.gz、算雜湊、產未簽章的發布清單 release.json | 主專案 scripts/build/:build_all.sh、sbom.sh、build_bundle.sh、release_json.sh | GitLab CI,實際跑在 188 build 機 | 只有錯誤追蹤站管理員密碼(CI 變數,烙進包) |
| ② 上傳登記 | 向更新站登記一筆草稿,再把包、雜湊檔、清單傳到 Cloudflare R2 | 主專案 scripts/build/publish_draft.sh | 同一條 CI 的 package 段最後一步,接在 ① 後自動跑 | R2 上傳金鑰、更新站 CI 專用 token,只放 GitLab CI 變數,不落檔 |
| ③ 蓋章發布 | 用私鑰對清單簽章、送回更新站,狀態從草稿改成「已發布」,客戶才看得到 | 更新站 repo update-center/cli/guidant-release.ts 的 publish | 決策者自己的電腦 | Ed25519 私鑰在 ~/.guidant-release/,有密碼保護,密碼只在終端機輸入(見 Q9) |
①② 在 .gitlab-ci.yml 的 package 段連續跑:sbom.sh(弱點放行門,白名單外 Critical 就停)→ build_bundle.sh(不准加跳過正式公鑰檢查的參數)→ push_harbor.sh → publish_draft.sh。③ 必須等人執行。
%%{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':'13px'}}}%%
sequenceDiagram
autonumber
participant D as 開發者
participant G as GitLab CI(188 build 機)
participant R as Cloudflare R2
participant U as 更新站(Worker+D1)
participant K as 決策者電腦
participant C as 客戶主機
rect rgb(246,235,213)
Note over D,C: ① 出包(CI,無私鑰)
end
D->>G: 推 commit/手動觸發
G->>G: verify:守衛測試、pip-audit
G->>G: build:Nuitka 編譯、BE/FE/init 映像
G->>G: rehearse:用同一支 build_bundle 打的包在 190 做升級排練
G->>G: package:sbom 弱點放行門 → build_bundle 打包 → release_json 產未簽清單
rect rgb(246,235,213)
Note over D,C: ② 上傳登記(CI,用 R2 金鑰與更新站 CI token)
end
G->>U: register-draft(版號、雜湊、大小)
U-->>G: 回 R2 物件路徑 v1/版號/…
G->>R: 上傳 tar.gz、.sha256、未簽 release.json
Note over U: 狀態 draft:只有指定的內部測試機看得到
rect rgb(226,240,241)
Note over D,C: ③ 蓋章發布(決策者電腦,唯一有私鑰的地方)
end
K->>U: guidant-release publish 版號:取草稿
K->>K: 輸入私鑰密碼 → 解開私鑰 → 對清單簽 Ed25519
K->>U: 送回簽好的清單
Note over U: 狀態 published:同主版號客戶都看得到
rect rgb(236,236,236)
Note over D,C: 客戶端(見 Q11、Q12 與執行流程)
end
C->>U: 每日檢查有沒有新版
U-->>C: 有 1.27.x、附簽好的清單
C->>R: 用 15 分鐘預簽網址下載
C->>C: 用本機 /etc/guidant/release-keys 公鑰驗章、比對雜湊
圖 1 現行發版循序圖(從程式碼到客戶)。①② 在 CI 自動連續跑,CI 沒有私鑰,只能產出草稿;③ 由決策者在自己電腦執行,是唯一能讓客戶看得到新版的動作。
現行 CI 流程(BE .gitlab-ci.yml)
觸發方式:只有人主動發才跑,push、MR、打 tag 一律不觸發。兩種發法:GitLab 網頁按「Run pipeline」,或指令 glab ci run。另有一條每日排程,只負責發布弱點資料庫,與出版互斥。定版 tag 由決策者手打,CI 不碰 tag。
在哪跑:全部 job 跑在 188 build 機(runner tag guidantai-build,shell 執行),不另外 clone,直接在 188 的 /opt/guidant-ai-be 工作樹裡跑。開跑前檢查兩件事:188 上有人在手跑 build 就拒跑(兩條 Nuitka 同跑會 crash);工作樹不乾淨就拒跑,不悄悄 reset 掉別人的東西。這個檔只寫「什麼時候、在哪台、呼叫哪支」,邏輯全在 scripts/build/*.sh,所以 188 手跑與 CI 跑是同一條路。
按 Run pipeline 時可選的參數:
| 參數 | 意思 | 預設 |
environment | 部署目標:e2e(190)/stg(188)/poc(189,等同正式,部署要再按一次) | e2e |
skip_be | 跳過後端 Nuitka 編譯,沿用 188 上同版號那顆後端映像,只重打其他映像 | 否 |
rehearse_base | 升級排練從哪個舊版升上來(不可低於 1.27.0) | 1.27.0 |
internal_test | 內部測試包:排練段跳過正式章守門,只為把 190 升上去測;package 段照樣守門照樣紅,產物不可出貨 | 否 |
deploy_only | 只部署:跳過前面全部,拿 188 上同版號既有的包升目標機 | 否 |
五個 stage:
| stage | job | 做什麼(呼叫的腳本) | 紅了會怎樣 |
| verify | verify | 把 188 工作樹同步到這次 pipeline 的 commit;FE、jedi 套件 clone 一起 pull;sbom.sh --source 原始碼層 SBOM、audit_deps.sh 相依套件弱點掃描 | 後面全不跑 |
| build | build-be | build_release.sh Nuitka 編譯 → build_prompt_guard_onnx.sh → smoke_auto.sh 冒煙測試(資料庫落後時自動改用臨時庫)→ build_image.sh 打後端映像。上限 2 小時 | 後面全不跑 |
build-fe | build_fe_image.sh 前端映像 |
build-init | build_init_image.sh 資料庫初始化與 migration 映像(等 build-be 做完,要用它攤平的 migration) |
build-classifier | build_classifier_image.sh 證據分類服務映像 |
| rehearse | rehearse-upgrade | 先用 build_bundle.sh 打一次包,再 deploy_e2e.sh --base-version:清掉 190、裝舊版、灌 STG 唯讀資料庫、用新包升級、比筆數。上限 2 小時 | 不出包 |
| package | package | sbom.sh --images 映像弱點放行門(白名單外 Critical 就停)→ build_bundle.sh(不准跳過正式章檢查)→ push_harbor.sh 推內部映像庫 → publish_draft.sh 登記更新站草稿、上傳 R2。弱點報告存成 CI artifacts | 不出包、不登記 |
| deploy | deploy-e2e/-stg/-poc | deploy_e2e.sh:把包 scp 到目標機、驗雜湊、sudo ./install.sh --upgrade --force。stg/poc 多兩道門:先停 api 再升、授權鑰要在這版公鑰表內。poc 要再按一次才跑 | — |
%%{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':'13px'}}}%%
flowchart LR
T["人按 Run pipeline
或 glab ci run"] --> V
subgraph S1["verify"]
V["同步 188 工作樹
sbom 原始碼層
audit_deps 套件弱掃"]
end
subgraph S2["build(平行)"]
BE["build-be
Nuitka+smoke+映像"]
FE["build-fe"]
CL["build-classifier"]
IN["build-init"]
end
subgraph S3["rehearse"]
RH["打一次包
190 舊版→新版排練"]
end
subgraph S4["package"]
PK["映像弱點放行門
build_bundle
推 Harbor
publish_draft 草稿+R2"]
end
subgraph S5["deploy"]
DE["e2e 190"]
DS["stg 188"]
DP["poc 189
再按一次"]
end
V --> BE & FE & CL
BE --> IN
BE & FE & IN & CL --> RH --> PK
PK --> DE & DS & DP
PK -. 草稿 .-> SIGN["決策者電腦
guidant-release publish
(不在 CI 內)"]
圖 1-1 現行 CI 流程。任一段紅,後面全部不跑;簽章不在 CI 內。
實況要說清楚:上面這條 CI 是設計好的正式出版路,但 1.27.x 這幾版實際上不是走它出的。測試期是首腦在 188 手跑 /home/jedi/fr167-build-<版>.sh(內容是 build_all.sh --all 加 build_bundle.sh --skip-prod-key-check,跳過正式章檢查),再用本機 publish-<版>.sh 直接上傳 R2 並用測試章簽。所以 CI 的 package 段(含 publish_draft.sh 登記草稿)還沒跑過一次正式出版;正式章產出後,第一次正式出版要走 CI 並驗證這段。手跑時要自己補 CI 才有的三件事:PATH 加 venv、資料庫落後時改用臨時庫做冒煙測試、手動 export 錯誤追蹤站管理員密碼(FR-167 STATE 有記)。
Agent 怎麼出:現行 Agent 沒有自己的發版流程,跟著主系統包一起出。build_bundle.sh 把 evidence-agent repo 的 build_compose_bundle.sh 產的 Agent 交付包放進主系統包的 agent/ 資料夾(build_bundle.sh:465)。客戶主系統升級完,第 7 步清理只保留 bundles/<版>/agent/,之後由更新中心推給各台 Agent。所以修一個 Agent 的 bug,要重出整個主系統、客戶主系統也要停機升一次。
測試期的實況:正式私鑰還沒產(決策者尚未執行 init-keys),188、189、190 裝的都是 10/30 產的測試章 b91819f86eb4b4f8。測試期由首腦用 ~/Projects/Billows/Audit-Manager/.fr167-dev/publish/publish-<版>.sh 直接簽,沒走正式 guidant-release publish。正式出貨前要換正式章;build_bundle.sh:187 沒有正式公鑰就拒絕出包,CI package 段不准加跳過參數。
二、改版後做法(FR-172+FR-173)
兩個改變:主系統與 Agent 分開出版(FR-172),簽章從決策者電腦搬到自架金庫(FR-173)。
| 主系統 | Agent | 前端 |
| CI | BE repo 自己的 pipeline,188 shell runner | evidence-agent repo 新建自己的 pipeline,188 shell runner,與 BE 串行(兩條 Nuitka 同跑會 crash) | FE repo 自己的 pipeline,docker runner;BE 發版時由 BE pipeline 觸發 |
| 觸發 | 推保護 tag(例如 v1.28.0) | 推保護 tag(例如 v1.5.0),不被 BE 觸發 | push 跑檢查;BE 觸發時打映像 |
| 包 | guidant-ai-<版>.tar.gz,不再含 agent/ | guidant-agent-compose-<版>.tar.gz,自帶 release.json、release-keys/、版本說明 | 映像進 Harbor,不單獨出包 |
| R2 路徑 | v1/<版>/ | agent/v1/<版>/ | — |
| 更新站版本線 | product=host,帶 min_agent_version | product=agent,帶 min_host_version | — |
| 章的種類 | release_manifest | agent_release_manifest(同一把鑰匙,不同型別,拿 Agent 清單餵主系統會被拒) | — |
| 簽章 | CI 最後一個 手動按鈕「簽章並發布」:CI 用 GitLab 短效身分(OIDC)向自架 Vault 要簽名,Vault 用存在裡面、拿不出來的 Ed25519 鑰匙簽好回傳。決策者電腦的私鑰保留當備用章 | 無 |
| 版本說明、最低相容版 | docs/release_notes/v<版>.md 檔尾、scripts/build/min_agent_version | RELEASE_NOTES.md 檔尾、scripts/build/min_host_version | — |
改版後的 CI 流程(FR-173 設計)
跟現行最大的三個差別:觸發改成推保護 tag(現行是人按 Run pipeline);前端與 Agent 各有自己的 pipeline(現行全塞在 BE 一支);簽章進 CI,變成最後一個手動按鈕(現行在決策者電腦)。建置期祕密(R2、Harbor、更新站 token、Nexus)從 188 的 .env 與 GitLab 變數搬進 Vault,job 執行時才拿、結束即失效。
%%{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':'13px'}}}%%
flowchart LR
subgraph trig["觸發(人做的第一件事)"]
T1["推保護 tag
v1.28.0(BE)"]
T2["推保護 tag
v1.5.0(Agent)"]
T3["FE/jedi 任意 push"]
end
subgraph be["BE pipeline(188,與 Agent 串行)"]
B1["verify
守衛 pytest 弱掃"]
B2["觸發 FE 下游
等它綠"]
B4["build
Nuitka BE/init 映像"]
B5["rehearse
190 升級排練"]
B6["package
弱點放行門 打包
Harbor 草稿 R2 v1/"]
B7["簽章並發布
(手動按鈕)"]
B1 --> B2 --> B4 --> B5 --> B6 --> B7
end
subgraph fe["FE pipeline(docker runner)"]
F1["verify
lint vitest npm 弱掃"]
F2["打 FE 映像
主控台前端產物"]
F1 --> F2
end
subgraph ag["Agent pipeline(188,與 BE 串行)"]
G1["verify"]
G2["build
Nuitka 取主控台前端"]
G3["rehearse
舊包 --upgrade"]
G4["package
打包 Harbor
草稿 R2 agent/v1/"]
G5["簽章並發布
(手動按鈕)"]
G1 --> G2 --> G3 --> G4 --> G5
end
VA["自架 Vault
Ed25519 鑰匙不可匯出
建置期祕密"]
T1 --> B1
T2 --> G1
T3 --> F1
B2 -.觸發.-> F1
F2 -.前端產物.-> G2
B6 -.取祕密.-> VA
G4 -.取祕密.-> VA
B7 -.要簽名.-> VA
G5 -.要簽名.-> VA
圖 2-1 改版後三條 CI。BE 與 Agent 都跑在 188(Nuitka 需要 188 環境),排同一個佇列不併跑;FE 跑 docker runner。人要做的只有兩件:推保護 tag、按「簽章並發布」。
| BE | Agent | FE | jedi 套件 |
| runner | 188 shell,與 Agent 同佇列串行 | 188 shell,同左 | docker | docker |
| 觸發 | 保護 tag 或手按 | 保護 tag 或手按,不被 BE 觸發 | push 跑檢查;BE 觸發時打映像 | push 跑測試;BE 觸發時打分類服務映像 |
| 弱點掃描 | 套件、lockfile、映像三層,加放行門 | 同左 | npm 套件;映像層由 BE package 段照掃 | 套件 |
| 簽章 | 手動按鈕,向 Vault 要簽名 | 同左,--product agent | 無 | 無 |
| 要人按的 | 推 tag、按簽章 | 推 tag、按簽章 | 無 | 無 |
部署到 190/188/189 那段在 FR-173 設計裡沒有改,沿用現行 deploy_e2e.sh;STG/POC 改成走線上更新還是維持 install.sh --upgrade,設計文件沒寫,要另外確認。
%%{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':'13px'}}}%%
sequenceDiagram
autonumber
participant M as 維護者
participant B as BE CI
participant F as FE CI
participant A as Agent CI
participant V as 自架 Vault(192.168.50.172)
participant R as R2
participant U as 更新站
rect rgb(246,235,213)
Note over M,U: 主系統發版
end
M->>B: 推保護 tag v1.28.0
B->>B: verify:守衛、pytest、弱掃
B->>F: 觸發 FE 下游,等它綠
F-->>B: FE 映像 tag
B->>B: build → rehearse 排練 → package 弱掃放行門與打包
B->>V: 用 GitLab OIDC 取 R2、更新站 token 等建置期祕密
B->>U: register-draft product=host
B->>R: 上傳到 v1/1.28.0/
M->>B: 按「簽章並發布」(手動按鈕,只在保護 tag)
B->>V: OIDC 換短效身分 → transit/sign 要簽名
V-->>B: 簽名(鑰匙不離開 Vault,每次簽章記稽核 log)
B->>U: 送簽好的清單 → published
rect rgb(226,240,241)
Note over M,U: Agent 發版(獨立,不需重出主系統)
end
M->>A: 推保護 tag v1.5.0
A->>A: verify → build(主控台前端拿 FE artifact)→ rehearse → package
A->>U: register-draft product=agent
A->>R: 上傳到 agent/v1/1.5.0/
M->>A: 按「簽章並發布」
A->>V: transit/sign
V-->>A: 簽名
A->>U: 送簽好的清單 → published
圖 2 改版後發版循序圖(從程式碼到更新站)。主系統與 Agent 各一條 pipeline、各自觸發;簽章改由 CI 手動按鈕向 Vault 要簽名,鑰匙拿不出 Vault。仍要人按的只有兩個:推保護 tag、按「簽章並發布」。
Agent 包怎麼到客戶的 Agent 主機(FR-172 已裁):Agent 不連外網、不連更新站。主系統代為檢查與下載 Agent 新版,驗過章存到 upgrade/agent-bundles/<版>/,再推給各台;Agent 主控台也能請主系統更新這一台,或在離線時直接上傳包,用現行版帶的公鑰驗章。所以修 Agent 的 bug,主系統不必出版、不必停機。
為什麼簽章要從決策者電腦搬走(FR-173 D1):決策者出差就出不了版,與無人值守自動化方向相反。但不能把私鑰放 GitLab CI 變數:CI 變數讀得到就等於私鑰外流。改用 Vault 的「Transit」:鑰匙建在 Vault 裡設成不可匯出,CI 只能拿資料去請 Vault 簽,拿不到鑰匙本身;而且 Vault 只認「保護 tag 觸發的 BE、Agent 兩個專案」的 GitLab 身分。客戶端驗章程式不用改:還是 Ed25519,只是多一把公鑰。
三、進度(2026-11-02)
| 項目 | 狀態 |
| 主系統包不再帶 Agent(FR-172 B-0) | ✅ 已做(BE f3018bdfa),在工作坊分支 ws-CM-3401,還沒合回 feature/FR-172,還沒出過包 |
更新站 Agent 版本線(U-1)、發布 CLI 加 --product agent(U-2) | ✅ 已做(update-center d1ec95a、5c66bda),同上未合回、未部署 |
| Agent 包自帶清單與公鑰(A-0) | ✅ 已做(evidence-agent 84a7c23),同上 |
| 主系統 Agent 區查新版、下載(H-1、H-2) | ✅ 已做,同上 |
| FR-172 其他 7 張(進度即時、主控台更新頁、收尾) | 待派工 |
| 自架 Vault | ✅ 192.168.50.172 已架、Ed25519 鑰匙已建且不可匯出、試簽驗過;每日快照、防火牆、GitLab 串接未做 |
| 三條 CI、Vault 簽章(FR-173 六張卡) | 未開卡,排在 FR-172 之後;若 1.28.0 來不及就照現行決策者電腦手簽出 |
正式章 init-keys | 未做;改版後正式章改在 Vault,決策者電腦那把轉為備用 |
最低相容版本怎麼決定
可直升起始版(min_base)不是人填的數字,是從 migration 清單推算:scripts/sql/manifest.tsv 每支 migration 有一欄 min_base,寫 migration 的人只在「這支依賴某版才建的東西,從更舊的版直跳會壞」時填那個版號。出包時取全部的最大值寫進清單。目前沒有任何一支填,所以每版都是 1.0.0,任何版都能一跳升上來。
主系統與 Agent 互相要求的最低版本(FR-172 起)是人填的一行檔:主系統 scripts/build/min_agent_version、Agent scripts/build/min_host_version。只擋一個方向:Agent 新版要求的主系統版本比客戶現行版新,就不讓推(灰鈕加原因);主系統新版要求的 Agent 版本比某台新,只在那台標「必須更新」,不擋主系統升級。