FR-167 · 操作手冊 · 2026-11-01
從定版號到三台主機升完:188 出包、開包驗、蓋章、上傳 R2 與 D1、驗更新站、依序升級、失敗處理與 build 機清理
給原廠內部發版的人照著做。每一步寫明在哪台機器、跑什麼指令、成功長什麼樣、失敗看哪裡。指令都對過 repo 內的腳本與旗標;188 上的暫存腳本與 Mac 上的測試期發布腳本不在 repo,本頁寫出它們的內容與位置。本頁不含任何密碼、金鑰或帳號編號,需要時只寫存放位置。
| 角色 | 做什麼 | 在哪台 |
|---|---|---|
| 決策者 | 定版號、蓋正式章(guidant-release publish)、放行 STG/POC 升級、母卡 Done |
自己的 Mac(發布印章私鑰只在這台) |
| 發版執行者(首腦或被指派的人) | 版號與 release note、188 出包、開包驗、測試期上傳 R2/D1、升 190 測試機 | Mac+188 |
| 客戶主機管理員 | 在更新中心按「更新到 X」 | 客戶主機 |
機器座標
| 機器 | 角色 | 存取 |
|---|---|---|
| 192.168.50.188 | build 機(/opt/guidant-ai-be、/opt/guidant-ai-fe、/opt/guidant-ai-agent、/opt/jedi-python-package)+STG stack(/srv/guidant-ai) |
ssh jedi@192.168.50.188,docker 免 sudo;沒有 node |
| 192.168.50.190 | 測試機,升級先在這台跑 | ssh jedi@192.168.50.190 |
| 192.168.50.189 | POC stack(等同正式環境) | ssh jedi@192.168.50.189;升級要決策者當次放行 |
| Mac | FE 主控台前端 build、測試章簽章、wrangler 寫 D1 | 本機 |
要準備的環境變數與憑證(只列名稱與位置)
| 名稱 | 誰用 | 放在哪 |
|---|---|---|
GUIDANT_BUGSINK_ADMIN_PASSWORD |
build_all.sh 前提檢查、build_bundle.sh 烙進安裝包 |
正式:GitLab CI 遮罩變數;188 手跑:188 /home/jedi/fr167-test-bugsink-admin.txt(≥12 字元、不可含 $) |
LICENSE_CENTER_API_TOKEN、LICENSE_ACTIVATION_SERVER_URL |
Agent build 第 3 步送簽 | 188 /opt/guidant-ai-agent/.env |
FE_CONSOLE_DIST |
Agent build 收主控台前端 | 指向 188 /tmp/fr167-fe-console/dist-console |
| R2 讀寫金鑰、R2 endpoint | 上傳 R2 | 測試期:188 /home/jedi/.fr167-r2.env(600);CI:R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT 遮罩變數;本機:update-center .env |
UPDATE_SITE_URL、UPDATE_SITE_CI_TOKEN |
CI 的 publish_draft.sh 登記草稿 |
GitLab CI 遮罩變數 |
| Cloudflare API 權杖與帳號 | wrangler 寫 D1、部署 Worker | update-center .env;Mac 上 wrangler 已登入 |
| 發布印章(主力+備用) | guidant-release publish |
決策者 Mac ~/.guidant-release(passphrase 只從終端機讀) |
| 測試章 | 測試期簽章 | Mac ~/Projects/Billows/Audit-Manager/.fr167-dev/publish/sign/(不入版控) |
RELEASE_HAS_SECURITY_FIX |
release_json.sh 標「含安全修正」 |
出包時 export true;不設=false |
本機工具:Mac 要有 node 22、npx wrangler、ssh 免密到三台;188 要有 rclone、BE .venv(含 pip-audit)。
%%{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 TD
S1["步驟 1 版號與 release note<br/>BE/FE/Agent 各自 bump、commit、push"] --> S2a
subgraph S2["步驟 2 188 出包"]
S2a["pull 三個 clone 到目標 commit<br/>pgrep 查有沒有別的 build"] --> S2b["Mac:npm run build:CONSOLE<br/>scp 到 188"]
S2b --> S2c["Agent:build_all.sh --with-compose"]
S2c --> S2d["主系統:build_all.sh --all<br/>→ build_bundle.sh"]
end
S2d --> S3["步驟 3 開包驗<br/>grep 本版新字串、storage-seed、ops 小檔、Agent 包"]
S3 --> S4{"步驟 4 蓋哪一種章?"}
S4 -- 測試期 --> S4t["Mac 測試章簽<br/>(含 ops 兩欄)"]
S4 -- 給客戶 --> S4p["決策者 guidant-release publish<br/>(正式章)"]
S4t --> S5["步驟 5 上傳 R2 五檔+D1 寫一列<br/>(含三段說明)"]
S4p --> S5
S5 --> S6["步驟 6 驗更新站<br/>D1 列、R2 物件、planPath、190 立即檢查"]
S6 --> S7a["步驟 7 190 升級 → 驗"]
S7a --> S7b["188 STG 升級 → 驗<br/>(決策者放行)"]
S7b --> S7c["189 POC 升級 → 驗<br/>(決策者放行)"]
S7c --> S7d["各台 Agent 區推送 Agent"]
S7a -. 失敗 .-> S8["步驟 8 失敗處理<br/>進度檔、journal、diag、recover"]
S7b -. 失敗 .-> S8
S7c -. 失敗 .-> S8
正式出貨走 GitLab CI(.gitlab-ci.yml:verify → build → rehearse → package → deploy),出包、排練升級、推 Harbor、登記草稿上 R2 一條龍;決策者再用 guidant-release publish 蓋章。本頁步驟 2~5 寫的是 188 手跑這條路(CI 不可用、或要出內部測試包時),並標出哪些事只在 CI 做。
主系統(BE):照 version-bump skill(.claude/skills/version-bump/SKILL.md)做兩件必做的事:
docs/release_notes/v<X.Y.Z>.md,檔尾固定一段「更新中心顯示文字」(3~5 條白話+影響範圍+建議動作;停機不寫分鐘數,統一寫「升級期間將看到維護頁」)。這段是更新站後台三欄的來源,見步驟 5。pyproject.toml 的 version 改成新版號。build_bundle.sh、release_json.sh、gen_stamp_sql.sh 都從這裡讀版號。前端(FE):package.json 的 version 對齊同一個 X.Y.Z。
Agent:程式有改就一定要升版號(同號重出,更新中心會判「已是最新」而推不出去)。
evidence-agent/pyproject.toml 的 version。evidence-agent/RELEASE_NOTES.md 加一段 ## <版號>,條列白話(build 時取最多 5 條寫進 Agent 的 notes.json,更新中心 Agent 區顯示它)。跟版號一起確認的檔
| 檔 | 什麼時候要動 |
|---|---|
scripts/build/min_agent_version |
這一版主系統不再接受某版以下的 Agent 時,寫最低版號(一行)。檔不存在=等於本版 |
scripts/sql/manifest.tsv 第五欄 min_base |
這一版有 migration 要求客戶「必須先經過某版」時標記;release_json.sh 取小於本版的最大標記當 min_base_version |
scripts/init/99-stamp.sql、02-schema.sql |
這一版有主線 phase=active/envs=* 的新 migration 時要重產基線,否則 build_init_image.sh 的 gate (a) 會擋(見步驟 8)。重產步驟見 sql-migration skill「出貨基線同步」段 |
三個 repo 各自 commit、push 到當下的 feature branch。188 要 pull 得到才算數。
# ① 三個 clone 拉到目標 commit,親眼看 HEAD 與版號
ssh jedi@192.168.50.188 'for r in /opt/guidant-ai-be /opt/guidant-ai-fe /opt/guidant-ai-agent; do git -C $r pull -q --ff-only && echo "$r $(git -C $r log -1 --format="%h %s" | cut -c1-70)"; done; grep -m1 ^version /opt/guidant-ai-be/pyproject.toml /opt/guidant-ai-agent/pyproject.toml'
# ② 查有沒有別的 build 在跑(兩個 Nuitka 同時跑會 linker crash)。[b] 寫法避免抓到自己這行
ssh jedi@192.168.50.188 'pgrep -af "[n]uitka|[b]uild_all|[b]uild_bundle" || echo 沒有在跑的 build'
# ③ 磁碟:一包主系統 5.3G+解開的目錄+image 層,至少留 40G
ssh jedi@192.168.50.188 'df -h / | tail -1'BE 有合回的工作坊分支時,先合回 feature branch 並 push 再出包;188 拉的是 feature branch,沒合回就出包,包裡不會有修正。
Agent 映像要收 Agent 主控台的前端產物,188 沒有 node,所以在 Mac build:
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git log -1 --format='%h %s' # 確認是要出的那個 commit
npm run build:CONSOLE # 產出 dist-console/
ssh jedi@192.168.50.188 'rm -rf /tmp/fr167-fe-console/dist-console && mkdir -p /tmp/fr167-fe-console'
scp -rq dist-console jedi@192.168.50.188:/tmp/fr167-fe-console/每次出 Agent 都要重 build 一次。沿用 188 上舊的 dist-console 會讓新版 Agent 帶舊主控台,而且沒有任何錯誤訊息。
build_bundle.sh 會到 /opt/guidant-ai-agent/.build/compose/ 取最新一支 guidant-agent-compose-*.tar.gz 收進主系統包,所以 Agent 一定先出。
# 188 上
cd /opt/guidant-ai-agent
export FE_CONSOLE_DIST=/tmp/fr167-fe-console/dist-console
scripts/build/build_all.sh --check-only # 秒級:LC token、簽發站、工具鏈、磁碟、主控台產物
scripts/build/build_all.sh --with-compose # fetch 工具 → Nuitka(約 11 分)→ 簽章打包 → smoke → compose 包
ls -la .build/compose/ | grep <Agent 版號> # 要有 .tar.gz、.tar.gz.sha256、.notes.json要指定別支 Agent 包(例如沿用上一版 Agent),出主系統前 export AGENT_COMPOSE_BUNDLE=<檔>。
scripts/build/build_all.sh 開跑前自己擋三道前提(以前只寫在 .gitlab-ci.yml,手跑永遠過不了):
| 前提 | 腳本怎麼處理 | 手跑要做什麼 |
|---|---|---|
① pip-audit 找得到 |
自己 export PATH=<repo>/.venv/bin:$PATH;還找不到就紅 |
188 .venv 已裝;不用做事 |
| ② DB 前置斷言回 3(STG 落後本版 migration) | ⓪ 放行;② smoke 走 smoke_auto.sh → smoke_tempdb.sh:STG 唯讀 pg_dump → 188:25432 臨時庫 → 套本版 migration → smoke → 刪庫 |
不用做事。絕不為了出包去升 STG |
③ GUIDANT_BUGSINK_ADMIN_PASSWORD ≥ 12 字元 |
開跑第一步 gen_bugsink_admin_bake.sh --check |
要自己 export(值從檔案讀,不上命令列) |
188 上用一支暫存腳本串起來(放 /home/jedi/fr167-build-<版>.sh,不入版控),內容照這個寫:
#!/usr/bin/env bash
set -o pipefail
cd /opt/guidant-ai-be || exit 1
export GUIDANT_BUGSINK_ADMIN_PASSWORD="$(tr -d '[:space:]' < /home/jedi/fr167-test-bugsink-admin.txt)"
echo "[$(date +%T)] HEAD $(git rev-parse --short HEAD) $(grep -m1 ^version pyproject.toml)"
scripts/build/build_all.sh --all || { echo "[$(date +%T)] build_all FAILED rc=$?"; exit 2; }
scripts/build/build_bundle.sh --skip-prod-key-check || { echo "[$(date +%T)] build_bundle FAILED rc=$?"; exit 3; }
echo "[$(date +%T)] ALL DONE"--skip-prod-key-check:只准內部測試包用。它略過「BE image 要含 PROD 授權公鑰」與「要有 Agent 包」兩道守門,包尾會印「僅供內部測試,不可交付客戶」。給客戶的包不加這個旗標,缺 PROD 鑰就該紅。ls 確認 compose 包、再主系統),一個背景行程跑完。ssh jedi@192.168.50.188 'nohup setsid bash /home/jedi/fr167-build-<版>.sh > /home/jedi/fr167-build-<版>.log 2>&1 < /dev/null &'等它結束時,判斷條件要錨定腳本自己印的那幾行。smoke 輸出含 FAIL=0 字樣,只 grep FAIL 會提前誤判:
ssh jedi@192.168.50.188 'grep -E "^\[[0-9:]+\] (ALL DONE|.*FAILED)" /home/jedi/fr167-build-<版>.log; pgrep -af "[f]r167-build" || echo 已結束'程式碼與上一版完全相同、只要換版號重發(例如只為了驗證升級流程本身)時:
scripts/build/build_all.sh --reuse-images <舊版> --version <新版> --skip-prod-key-check
scripts/build/build_all.sh --reuse-images <舊版> --version <新版> --dry-run # 只印步驟be 疊一層只換 /app/VERSION、fe/init 重打 tag,跳過編譯、smoke、image build 與 OS 層 SBOM,直接 build_bundle.sh。舊 be image 沒有 /app/VERSION 會直接擋。成功後自動 docker builder prune -f --keep-storage 20G;連出兩包時第一包加 --no-prune。有程式碼變動就要完整跑 2.4。
| 事 | CI 哪一段 | 188 手跑時 |
|---|---|---|
| 排練升級:190 先裝舊版+灌 STG 唯讀 dump,再用新包升級 | rehearse-upgrade(deploy_e2e.sh) |
不做;改由步驟 7 在 190 線上升級驗 |
internal_test input:排練段用 --skip-prod-key-check |
rehearse-upgrade |
等同 2.4 手跑加旗標 |
正式包(不准 --skip-prod-key-check)、推 Harbor |
package(push_harbor.sh) |
不做 |
| 登記更新站草稿+傳 R2 三檔 | package(publish_draft.sh) |
改用步驟 5 的測試期腳本 |
| 部署 STG/POC | deploy-stg、deploy-poc(POC 為 manual) |
不做;改走步驟 7 |
| 段落 | 時間 |
|---|---|
Agent build_all.sh --with-compose |
約 20 分(Nuitka 約 11 分) |
主系統 build_all.sh --all+build_bundle.sh |
約 33 分(build_release 12~20 分、smoke 臨時庫數分、FE image 約 20 秒、封包數分) |
| Agent+主系統同一支腳本 | 約 55 分 |
--reuse-images 快速出包 |
約 11 分 |
/opt/guidant-ai-be/.build/bundle/ 下:
| 檔 | 用途 |
|---|---|
guidant-ai-<版>.tar.gz+.sha256 |
安裝包本體(約 5.3G) |
guidant-ai-<版>.ops.tar.gz+.sha256 |
維運指令小檔(約 100K),升級第 0 步憑它秒級換指令 |
guidant-ai-<版>.release.json |
未簽發布清單(版號、主版號、min_base_version、整包與小檔的雜湊與大小、min_agent_version、has_security_fix;issued_at 留空) |
guidant-ai-<版>/ |
解開的 staging 目錄,開包驗用;驗完可刪 |
build log 說成功不代表修正有進包。挑這一版 commit 新增的獨特字串去 grep 產物,回 0 就是沒進去。下表是每次都要過的項目,<版> 換成版號:
cd /opt/guidant-ai-be/.build/bundle
V=<版>
# 1. 版號與 Agent 包
cat guidant-ai-$V/VERSION
ls guidant-ai-$V/agent/ # guidant-agent-compose-<Agent 版>.tar.gz、.sha256、.notes.json
# 2. 系統檔(框架 PDF+檢測基準檔)筆數,對 storage-seed.map 宣告的 key 數
find guidant-ai-$V/storage-seed -type f | wc -l
# 3. 維運指令小檔內容(VERSION、guidantai、guidantai_bugsink.sh、lib/、release-keys/、maintenance/)
tar -tzf guidant-ai-$V.ops.tar.gz | wc -l
tar -tzf guidant-ai-$V.ops.tar.gz | grep -E 'lib/|maintenance/' | head
# 4. 清單欄位:ops 兩欄要在、min_base_version 合理
python3 -m json.tool guidant-ai-$V.release.json
sha256sum guidant-ai-$V.ops.tar.gz; stat -c %s guidant-ai-$V.ops.tar.gz # 要等於清單的 ops_sha256/ops_size
# 5. 版號區間與內容清單
cat guidant-ai-$V/images/bundle-info.txt
# 6. 主系統執行檔含本版新字串(grep dist,不要 grep 整顆 image,太慢)
grep -a -c '<本版新增的字串>' /opt/guidant-ai-be/.build/dist/guidant-ai-$V/guidant-ai
# 7. Agent 映像環境變數(Agent 有改設定時)
docker image inspect guidant-ai-agent:<Agent 版> --format '{{json .Config.Env}}' | tr ',' '\n' | grep -E 'LIBREOFFICE|<相關變數>'rolled_back,cancelled,skipped),在 FE image 的 html 目錄或 /opt/guidant-ai-fe/dist 找。storage_seed_guard.sh 每次從套件 assets 重建並對 map 核對,筆數對不上 build_bundle.sh 會擋;這裡再看一眼筆數是為了發現 map 本身漏宣告。.BAD-<原因> 保留;前一顆可用只是少東西就升版號。build 機與 CI 都沒有發布私鑰,release.json 出包時是未簽的。簽章只在 Mac 做。
| 測試章 | 正式章 | |
|---|---|---|
| 給誰 | 190/188/189 這些已裝測試章公鑰的機器 | 客戶 |
| 誰簽 | 發版執行者 | 決策者本人 |
| 工具 | Mac .fr167-dev/publish/sign/sign-release.ts(import update-center cli/envelope.ts,與正式 CLI 同一套 v2 信封) |
update-center guidant-release publish <版號> |
| 公鑰在哪 | 測試機 /etc/guidant/release-keys/ |
scripts/installer/release-keys/<kid>.pub,隨包裝到客戶主機 /etc/guidant/release-keys/ |
| 清單來源 | 188 的 release.json(含 ops 兩欄) |
更新站 D1 的草稿列(CI register-draft 登記) |
客戶主機只信自己機器上的公鑰。用哪把章簽,就只有裝了那把公鑰的機器收得下。
guidant-release publish詳細見 update-center cli/README.md。流程是:CI package 段的 publish_draft.sh 先向更新站 register-draft 登記草稿並把三個檔傳到 R2 → 決策者在後台指定「內部先裝」機器、填三段說明 → 內部機器升到這版並回報成功 → 決策者在 Mac 跑:
cd ~/Projects/Billows/Audit-Manager/update-center
cloudflared access login <更新站網址> # 第一次或憑證過期時
npm run cli -- publish <版號> --site <更新站網址>publish 會核「內部先裝機器全部回報升到這版且成功」、要你輸入版號確認、從終端機讀 passphrase、蓋章後先用 .pub 自己驗一次再送回更新站。內部機器沒回報、失敗、或草稿沒指定任何內部機器,它不問 passphrase、什麼都不送。
現況限制:
| 限制 | 影響 | 目前做法 |
|---|---|---|
更新站草稿(register-draft、D1 product_releases)沒有 ops_sha256/ops_size 欄,publish_draft.sh 也只傳三個檔、不傳 ops 小檔 |
正式章簽出的清單不含 ops 兩欄,客戶升級第 0 步要從整包抽維運指令(時限依包大小,每 GB 60 秒、下限 300 秒),不能秒級完成 | 測試期改用 4.2 的腳本從 188 的 release.json 直接簽(含 ops 兩欄);正式路徑補欄位另開卡 |
CLI publish 成功時仍印「清單 90 天後到期」 |
客戶端已不檢查清單有效期,這句是舊提醒 | 忽略即可,不用每 90 天重蓋章 |
Mac ~/Projects/Billows/Audit-Manager/.fr167-dev/publish/publish-<版>.sh(不入版控;以最新一支複製改名)一次做完步驟 4、5:
scp 未簽 release.json 回 Mac,核:版號=參數、sha256 64 字、size 大於 1G、ops_sha256/ops_size 等於實際值。sign-release.ts 用測試章簽成 v2 信封,解開 payload 確認 ops_sha256/ops_size 有進去。scp 回 188 暫存,接步驟 5。腳本頂端的 S= 是工作目錄(放 Mac 自己的 scratchpad),每次換 session 要改。
bucket guidant-ai-release,key 一律 v<主版號>/<完整版號>/<檔名>,主版號資料夾是授權邊界:
| 物件 | 測試期 key | 誰讀 |
|---|---|---|
| 安裝包 | v1/<版>/guidant-ai-<版>.tar.gz |
客戶主機(經 download-url 拿 15 分鐘預簽網址) |
| 雜湊 | v1/<版>/guidant-ai-<版>.tar.gz.sha256 |
人工核對 |
| 維運指令小檔 | v1/<版>/guidant-ai-<版>.ops.tar.gz |
客戶主機升級第 0 步(download-url 帶 artifact: "ops") |
| 小檔雜湊 | v1/<版>/guidant-ai-<版>.ops.tar.gz.sha256 |
人工核對 |
| 清單 | v1/<版>/guidant-ai-<版>.release.json(測試期放簽好的版) |
人工核對;客戶從 check 回應拿 D1 的 signed_manifest,不讀這個檔 |
正式路徑由 Worker releaseObjectKeys 回 key(未簽清單在 v1/<版>/release.json),CI 照回的 key 上傳,不自己拼。
測試期在 188 上傳,憑證用環境變數組臨時遠端、不寫 rclone.conf:
ssh jedi@192.168.50.188 bash -s <<'SH'
set -euo pipefail
set -a; . /home/jedi/.fr167-r2.env; set +a # 內含 R2 金鑰與 endpoint,600
export RCLONE_CONFIG_R2_TYPE=s3 RCLONE_CONFIG_R2_PROVIDER=Cloudflare \
RCLONE_CONFIG_R2_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" RCLONE_CONFIG_R2_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" \
RCLONE_CONFIG_R2_ENDPOINT="$R2_ENDPOINT"
B=/opt/guidant-ai-be/.build/bundle; V=<版>; D=r2:guidant-ai-release/v1/$V
for f in guidant-ai-$V.tar.gz guidant-ai-$V.tar.gz.sha256 guidant-ai-$V.ops.tar.gz guidant-ai-$V.ops.tar.gz.sha256; do
rclone copyto --s3-no-check-bucket "$B/$f" "$D/$f"
done
rclone copyto --s3-no-check-bucket /tmp/guidant-ai-$V.release.signed.json "$D/guidant-ai-$V.release.json"
rm -f /tmp/guidant-ai-$V.release.signed.json
rclone ls "$D/"
SH5.3G 上傳約 5 分鐘。大包先、清單後:中途失敗時沒有清單,不會被當成完整的一版。
更新站 D1 guidant-update-center 的 product_releases 一版一列。客戶在更新中心「更新內容」看到的三段來自這一列的 release_notes(字串陣列)、impact、action,不在簽章範圍內,發布前都能改。
三段的來源是步驟 1 release note 檔尾「更新中心顯示文字」。測試期寫成 Mac .fr167-dev/publish/notes-<版>.json:
{"notes": ["<白話一句>", "<白話一句>", "<白話一句>"],
"impact": "升級期間將看到維護頁,所有使用者暫時無法操作",
"action": "建議排在非上班時段;升級前請確認沒有進行中的證據分類工作"}隨包有新版 Agent 時,notes 最後一條寫「主系統更新完成後,請在更新中心的 Agent 區推送 <Agent 版>」。
測試期腳本把未簽清單的欄位、簽好的信封、三段說明組成一句 INSERT INTO product_releases (...)(status 預設 published;環境變數 STATUS=draft 只寫草稿),用 wrangler 寫進正式 D1,並把 SQL 留一份 rel<版>-insert.sql:
cd ~/Projects/Billows/Audit-Manager/update-center
npx wrangler d1 execute guidant-update-center --remote --file <insert.sql>正式路徑不寫 SQL:三段在更新站後台貼,草稿列由 CI register-draft 建,publish 把 status 改成 published 並存信封。
cd ~/Projects/Billows/Audit-Manager/update-center
# 1. D1:這一版在、published、kid 是預期那把章、大小與 r2_key 對
npx wrangler d1 execute guidant-update-center --remote --json \
--command "SELECT version, status, kid, size, r2_key, min_base_version, min_agent_version FROM product_releases ORDER BY created_at"
# 2. R2:五個檔都在、大小對得上 188 的檔(5.1 上傳腳本最後一行 rclone ls 已印出,對照 188 的 ls -l)
ssh jedi@192.168.50.188 'ls -l /opt/guidant-ai-be/.build/bundle/ | grep <版>'
# 3. Worker 活著
curl -s https://<更新站網址>/healthz # {"ok":true}跨版路徑(planPath):客戶從現行版到最新版每一跳挑「現在能直升的最高版」(src/releases.ts planPath)。這一版的 min_base_version 高於某些客戶現行版時,要確認那些客戶走得到:在 update-center test/releases.spec.ts 的 planPath 區塊照 D1 的實際列加一個案例(現行版=三台機器的版號),npx vitest run test/releases.spec.ts --exclude '.worktrees/**' 跑過就刪掉,不 commit。min_base_version 都是 1.0.0 時每台都是一跳直升,可略過。
端到端:/v1/check 要帶真的身分票與機器指紋,不適合手打 curl。實際驗法是到 190 更新中心按「立即檢查」,看到新版號與「更新內容」三段說明。
| 順序 | 機器 | 放行 | 為什麼這個順序 |
|---|---|---|---|
| 1 | 190 測試機 | 發版執行者可直接做 | 第一個裝新包的地方;失敗在這裡就停,不往下 |
| 2 | 188 STG | 決策者當次放行 | STG 資料較真實;同時也是 build 機,升級要避開正在跑的 build |
| 3 | 189 POC | 決策者當次放行 | 等同正式環境、對外 demo |
升級前每台都查:
for h in 190 188 189; do
echo "== $h"; ssh jedi@192.168.50.$h 'curl -sk https://127.0.0.1/api/1.0/version; echo; df -h / | tail -1; ls /var/run/reboot-required 2>/dev/null; uname -r'
done/var/run/reboot-required、或執行中核心比已安裝的舊:先 sudo reboot。預檢也會擋,但先處理省一輪。管理員登入該台 → 系統設定 → 更新中心 → 「立即檢查」 → 「更新到 X」:
升級中畫面與維護頁顯示八步:換成新版維運指令 → 預檢 → 停止接新工作,等進行中的做完 → 停服務、複製資料庫 → 在複本上升級 → 啟動新版、健康檢查 → 開放使用 → 清理舊版本。實測整次約 8 分鐘(第 0 步有 ops 小檔時約 1 秒)。
升完驗:
ssh jedi@192.168.50.<台> 'curl -sk https://127.0.0.1/api/1.0/version; echo; sudo guidantai status'
ssh jedi@192.168.50.<台> "docker exec guidant-db psql -U cmmgr -d guidant_ai -At -c \"SELECT event_code, message FROM public.system_logs WHERE event_code BETWEEN 6239 AND 6248 ORDER BY id DESC LIMIT 10\""升完的主機每一步會補寫一筆系統紀錄(事件碼 6239~6247 對應第 0~8 步,6248 是整次結果)。更新站 D1 instance_version_reports 會出現這台的新版號(主系統每分鐘同步主機結果並回報更新站)。
install.sh --upgrade 這條路的限制手動把包解開、sudo ./install.sh --upgrade 也能升,但:
<資料目錄>/upgrade/bundles/ 不會有這一版,更新中心 Agent 區顯示「這一版的 Agent 安裝包不在升級目錄」、推不了 Agent(PRODUCT_UPDATE_412008)。sudo guidantai upgrade <包.tar.gz>(包、.sha256、.release.json 三個檔放同一個資料夾),或走線上升級。只有「這台還沒有更新中心」(第一次從沒有 guidantai upgrade 的舊版升上來)才需要走這條路。升級前先停 api(docker stop guidant-api guidant-worker guidant-socketio),不然 Agent 心跳會和 migration 互卡。
主系統升完後,更新中心「Agent」區列出每台版本、在線、落後、必須更新:
GET /update/progress),Agent 區展開看得到下載、解開、預檢、換版、健康檢查各段。Agent 跟進的前提:Agent 是 docker compose 形態、版本 1.4.4 以上(更早的要先手動升一次)、主機有 systemd。Agent 升完會回報主系統與更新站(D1 agent_version_reports)。
| 症狀(log 最後幾行) | 原因 | 怎麼做 |
|---|---|---|
前提不足:缺 GUIDANT_BUGSINK_ADMIN_PASSWORD |
第三道前提沒 export | 照 2.4 腳本從檔案讀進環境變數再跑 |
前提不足:PATH 加了 … 仍找不到 pip-audit |
BE .venv 沒建或沒裝 dev 依賴 |
188 /opt/guidant-ai-be 重建 venv |
⓪b audit_deps 紅、列出漏洞 ID |
相依有未放行的漏洞 | 升套件版本;確定不受影響才寫進 scripts/build/audit_ignore.txt(每行要有理由) |
DB 前置斷言失敗(exit 1/2) |
連錯庫或斷言壞掉(exit 3 是常態,會自動走臨時庫) | 看 scripts/build/README.md「build 機的 smoke 連哪個資料庫」 |
FAIL (a):99-stamp.sql 補登清單與 manifest.tsv 不一致 |
本版有主線新 migration、基線沒重產 | 照 sql-migration skill「出貨基線同步」重產 02-schema.sql/99-stamp.sql,commit、pull 再出 |
| storage-seed 筆數與 map 不符 | 套件 assets 沒攤平或 map 漏宣告 | 看 scripts/build/README.md「套件 assets 攤平 → storage-seed」 |
Agent compose 交付包…不存在(正式包) |
沒先出 Agent | 先做 2.3,或 AGENT_COMPOSE_BUNDLE=<檔> |
Agent build LICENSE_CENTER_API_TOKEN … 皆未設定 |
188 Agent repo .env 缺 token |
補 .env;--check-only 先驗 |
Agent build 缺主控台前端產物 |
沒做 2.2 或路徑錯 | 重做 2.2,FE_CONSOLE_DIST 指到它 |
no space left on device、linker crash |
磁碟滿、或兩個 build 同時跑 | 照「清理」段清空間;pgrep 確認只有一個 build |
| 看什麼 | 在哪 |
|---|---|
| 更新中心失敗提示 | 「已自動退回」「這次不更新」「退回失敗」三種,附第幾步、原因;可按「下載診斷包」 |
| 進度檔 | <資料目錄>/upgrade/upgrade-progress.json(STG/POC 為 /srv/guidant-ai/upgrade/upgrade-progress.json) |
| 主機撿單與執行 | sudo journalctl -u guidant-upgrade-order.service --since "1 hour ago" |
install.sh --upgrade 的全程輸出 |
<資料目錄>/log/upgrade-<日期時間>.log |
| 診斷包 | sudo guidantai diag(含進度檔、更新單、最近三支升級與安裝紀錄,密碼金鑰已遮) |
退回失敗(結束代碼 4、維護頁紅字):
sudo guidantai recover # 走與自動退回同一支程式,重跑安全;換接已完成就不會拉回舊版
sudo guidantai statusrecover 也失敗就產診斷包,不要手動動資料夾。換接完成後才發現新版有問題:不退回,發修正版再升。
常見原因:核心裝了沒重開(預檢擋「請先 sudo reboot」,errno 524 也是它);磁碟不足(預檢列差多少);主機 lib 的修正(第 0 步、維護頁這類升級途中才執行的東西)要裝了那版之後的下一次升級才生效,回報「這次會看到某修正」前先分清它是升級前就在跑、還是升級中才用到。
| 看什麼 | 在哪 |
|---|---|
| 更新中心 Agent 區 | 該台「失敗」與原因 |
| 進度檔 | Agent 主機 <安裝目錄>/upgrade/upgrade-progress.json |
| 主機撿單 | sudo journalctl -u guidant-agent-upgrade-order.service --since "1 hour ago" |
| 升級輸出 | <安裝目錄>/upgrade-<日期時間>.log |
df -h、sudo journalctl -u docker --since "1 hour ago" | grep -i "no space";升級同時要放下載包、解開的包、載入的映像與快照,約需 12G 以上。清掉舊版 Agent 映像與舊包後,在 Agent 區勾那台重推,包已在主機上不必重下。docker logs 只看得到新容器;判讀升級中狀態看主機層檔案(進度檔、journal)。188 一天出三包會吃掉 50G 以上。清之前先看誰還在用:
ssh jedi@192.168.50.188 'df -h / | tail -1; docker system df'
# 每個容器的 compose 工作目錄與設定檔:有命中的目錄就不是垃圾
ssh jedi@192.168.50.188 'docker inspect $(docker ps -aq) --format "{{.Name}} {{index .Config.Labels \"com.docker.compose.project.working_dir\"}} {{index .Config.Labels \"com.docker.compose.project.config_files\"}}"'| 可以清 | 指令或做法 |
|---|---|
| build cache(留 20G 給下次命中) | docker builder prune -f --keep-storage 20G |
自家舊版 image tag(guidant-ai-be/fe/init、guidant-ai-agent 舊版號),沒有任何容器引用 |
先 docker ps -a --filter ancestor=<image:tag> 確認空,再 docker rmi <image:tag> |
| 已在 R2 的舊版本機包 | rm /opt/guidant-ai-be/.build/bundle/guidant-ai-<舊版>.{tar.gz,tar.gz.sha256,ops.tar.gz,ops.tar.gz.sha256,release.json} |
| 解開的 staging 目錄 | rm -r /opt/guidant-ai-be/.build/bundle/guidant-ai-<版>(開包驗完就可刪) |
| Agent 舊 compose 包 | /opt/guidant-ai-agent/.build/compose/guidant-agent-compose-<舊版>.* |
| 🔴 不可清 | 為什麼 |
|---|---|
STG 的 compose 工作目錄(/home/jedi/guidant-ai-<版>/,上面 docker inspect 有命中的那個) |
線上升級不重建 redis/seaweedfs/bugsink,它們的 compose 標籤仍指這個目錄;刪了下次 compose 操作就找不到設定 |
| 第三方映像(node、nginx、postgres、分類器、Harbor 拉的) | 重下載成本高,封閉網路可能拉不到 |
docker system prune、docker image prune |
會一起刪第三方映像與現役 image,只准 builder prune |
/opt/guidant-ai-be/.build/dist/ |
開包驗要 grep 它;--reuse-images 的 SBOM 也沿用 |
/opt/guidant-ai-be/.build/models/prompt-guard-2/ |
AI 閘道注入偵測模型,重轉要 HF 權杖與時間 |
/home/jedi/.fr167-r2.env、/home/jedi/fr167-test-bugsink-admin.txt |
測試期上傳與出包要用 |
| # | 項目 | 完成 |
|---|---|---|
| 1 | BE release note(含「更新中心顯示文字」)、pyproject.toml、FE package.json 同一版號 |
☐ |
| 2 | Agent 有改:pyproject.toml 升版、RELEASE_NOTES.md 加一段 |
☐ |
| 3 | min_agent_version、manifest.tsv min_base、基線重產是否需要 |
☐ |
| 4 | 三 repo 已 push;工作坊分支已合回 | ☐ |
| 5 | 188 三個 clone pull 到目標 commit、pgrep 無其他 build、磁碟夠 |
☐ |
| 6 | Mac npm run build:CONSOLE、scp 到 188 |
☐ |
| 7 | Agent build_all.sh --with-compose 成功、compose 包三檔在 |
☐ |
| 8 | 主系統 build_all.sh --all+build_bundle.sh 成功(log 有 ALL DONE) |
☐ |
| 9 | 開包驗 7 項全過(版號、Agent 包、storage-seed、ops、清單欄位、執行檔新字串、Agent env) | ☐ |
| 10 | 簽章:測試章或正式章;信封 payload 含 ops 兩欄(測試章) | ☐ |
| 11 | R2 五檔在、D1 一列 published、三段說明對 |
☐ |
| 12 | 190「立即檢查」看到新版與說明;min_base 非 1.0.0 時 planPath 跑過 |
☐ |
| 13 | 190 升級成功、版本 API、system_logs 6239~6248 |
☐ |
| 14 | 188、189 決策者放行後升級、驗 | ☐ |
| 15 | 各台 Agent 推送成功;失敗的照 8.3 處理 | ☐ |
| 16 | 188 清理(只清可清表內的項目) | ☐ |
guidantai upgrade 八步、時限、進度檔、退回與 recover。scripts/build/README.md、build_all.sh --help、build_bundle.sh --help;update-center cli/README.md;version-bump skill。