設計

流程、觸發、產物落點、為什麼這樣選

設計

本頁描述 GitLab CI 發版流程現在的規則與理由。實際行為以 .gitlab-ci.yml 與 scripts/build/*.sh 的檔頭註解為準;本頁與程式碼不一致時,相信程式碼。

§1

流程

一次 pipeline(GitLab 上一次完整的自動化執行)分四段,前一段全部成功才會進下一段:

verify ──► build(四個 job 平行)──► package ──► deploy
 ~1 分        ≈ 17 分(由 BE 決定)      ~9 分        ~3.5 分
段 做什麼 時間
verify 確認 188 上沒人在手跑 build、工作樹乾淨 → 把 BE 與 FE 工作樹拉到這次 pipeline 的 commit → 從 lock 檔產應用層 SBOM(sbom.sh --source)→ 用 pip-audit 掃 Python 相依漏洞(audit_deps.sh),有未列白名單的漏洞就停 約 1 分
build-be Nuitka 編譯 BE → 產 Prompt Guard 模型 → smoke 驗證 → 打 BE image 約 17 分
build-fe 打 FE image 約 3 分
build-init 打 init image(建 schema、套 migration 用) 1 分內
build-classifier 打證據分類服務 image 1 分內
package 掃四顆 image 產 OS 層 SBOM(sbom.sh --images)→ 封安裝包(build_bundle.sh)→ 推 Harbor(push_harbor.sh) 約 9 分
deploy 把安裝包 scp 到 190 → 核對 sha256 → install.sh --upgrade --force → 確認所有服務 healthy、health 端點回應正確 約 3.5 分

build 段的四個 job 同時跑(188 的 runner 設定 concurrent = 3,加上 BE 那條最長,實際總時間約等於 BE 那條)。

skip_be 勾起來時,build-be 不編譯,直接沿用 188 上同版號的 BE image(guidant-ai-be:<版號>);188 上沒有這顆就失敗。適合只改 FE、init 或分類服務時,省掉 Nuitka 那 17 分鐘。

package 封的是測試包(帶 --skip-prod-key-check);正式出貨要另跑一次不帶這個旗標。

§2

觸發

只有人主動發才會跑:

  • GitLab 網頁 Build → Pipelines → Run pipeline
  • 命令列 glab ci run(GitLab 的官方 CLI,一樣是人主動發)

push、MR、打 tag 一律不觸發(workflow.rules 只放行 web 與 api 兩種來源)。理由:發版會動到環境,該是人的決定,不該因為推了一個 commit 就自動發生。

按下去就是部署指令,中間沒有第二道確認。保護機制是「任何一段失敗 pipeline 就停在那段」——漏洞、smoke 紅、封包失敗都到不了部署。

執行時可選兩個參數(inputs):

參數 意義 預設
environment 部署目標,目前只有 e2e e2e
skip_be 跳過 BE 的 Nuitka 編譯,沿用 188 上同版號的 BE image 不勾
§3

tag 與版號

  • 定版 tag(v<X.Y.Z>)由決策者手打,CI 完全不碰 tag。 CI 只負責「把指定 commit 做成一包、升到 e2e」,哪一包算定版是人的判斷。
  • image tag 是 <pyproject 版號>-<commit 短 hash>,例如 1.23.1-a1b2c3d。定版前同一個版號會反覆出測試包,帶上 hash 就不會互相覆蓋,也能一眼看出哪個 commit 最後被打了 tag。
  • 188 本機上的 image 只標版號(guidant-ai-be:1.23.1),推上 Harbor 時才加 hash 後綴。
  • 證據分類服務的 image 例外:tag 跟它自己的套件版號走,不跟產品版號、也不帶 hash——同一個套件版號內容就是同一份。
§4

SBOM 兩份

SBOM(Software Bill of Materials,軟體物料清單)是出貨附給客戶的「成分表」,列出產品裡用了哪些套件與版本,格式是 CycloneDX JSON。分兩份,因為能產出的時間點不同:

層 來源 在哪段產 會不會擋
應用層 BE poetry.lock、FE package-lock.json verify 會擋:產不出來就停;漏洞由同段的 audit_deps.sh 擋
OS 層 四顆 image 裡用 apt 裝的東西(LibreOffice、OpenSSL、字型等) package 不擋:osv-scanner 出一份漏洞報告僅供參考

為什麼應用層不能改成掃 image:BE 是 Nuitka 編成 standalone(編譯後的獨立執行檔),Python 套件的 dist-info 中繼資料在編譯時全被拿掉,掃描工具 syft 掃 image 看不到任何 Python 套件。lock 檔是應用層唯一的真相來源。

目前的缺口:FE 的 package-lock.json 沒有進版控,188 上的 FE clone 裡沒有這支檔,sbom.sh 會印警告並略過 FE 應用層 SBOM,實際只產出 BE 那份。補 FE lock 檔另案處理。

為什麼 OS 層只報告不擋:基底 image 的漏洞幾乎天天有新的,拿它當閘門就永遠出不了貨。

兩份 SBOM 都會收進安裝包的 sbom/ 目錄,也會上傳成 GitLab artifacts(CI 執行完留下、可在網頁下載的檔案)。

§5

部署路徑

所有環境都走安裝包升級,和客戶升級是同一條路:

  1. 把 188 上的安裝包(guidant-ai-<版號>.tar.gz 與 .sha256)scp 到 190 的 /home/jedi/
  2. 核對 sha256,不符就停(傳輸壞掉或被動過)
  3. 解開後執行 sudo ./install.sh --upgrade --force,install.sh 會依序:完整備份資料庫 → 載入新 image 並比對 digest → 套增量 migration → 換版重啟並等 healthcheck 全綠
  4. 再用 docker 逐一確認服務都是 healthy、打 health 端點確認回應正確
  5. 刪掉解壓目錄,只留 tar.gz 與 .sha256

e2e 這樣升有額外好處:順便驗證安裝包本身升不升得上去。如果 e2e 改成從 Harbor pull image,就驗不到客戶實際拿到的那一包。

Harbor 照推,但它是出版的一環(留一份可追溯的 image),不是部署來源。

為什麼固定帶 --force:install.sh 預設把「新舊版號相同」當成誤操作擋下。但定版前同一版號會反覆出測試包(commit 不同、版號相同),不帶 --force 第二次就升不上去。--force 不會略過任何步驟——備份、digest 比對、migration 照做——只是不因版號相同而中止。

為什麼升完要刪解壓目錄:維運指令 guidantai 與 compose 檔,install.sh 已經複製到 /usr/local/bin 與資料目錄。解壓目錄留著,docker compose ls 會指向一個日後被刪掉的路徑。

§6

為什麼在 188 的 /opt/guidant-ai-be 直接跑

CI 設定 GIT_STRATEGY: none(不讓 runner 自己 clone),verify 自己 git fetch 後 reset --hard 到 pipeline 的 commit。

理由:建置需要的東西都在 /opt/guidant-ai-be——.venv(Python 虛擬環境)、Prompt Guard 模型、smoke 用的 .env,FE 也有 clone 在 /opt/guidant-ai-fe。另外 clone 要全部重建;而且部署機更新本來就是走 git(禁止 scp/rsync 改程式)。

代價是 CI 和人手跑共用同一棵工作樹,對策:

  • 有人在手跑就拒跑:verify 開頭用 pgrep 找 nuitka/build_all.sh/build_release.sh/build_image.sh,找到就停。兩條 build 同時跑會互撞,linker 直接 crash。
  • 工作樹不乾淨就拒跑:git status 有任何未提交的改動就停並列出來,不會悄悄 reset 掉別人的東西。
  • build/package 開頭再確認一次工作樹確實停在 pipeline 的 commit,不是的話代表 verify 沒跑到,直接停。
§7

產物落點

產物 在哪 理由
SBOM(*.cdx.json、osv-*.txt) GitLab artifacts,保留 30 天 小檔,方便網頁下載
build-be 的 log GitLab artifacts(out/logs/),保留 14 天 失敗時要能在網頁看
安裝包(約 2.5GB) 188 的 /opt/guidant-ai-be/.build/bundle/ gitlab.com 的 artifacts 上限 1GB,上傳會被拒(HTTP 413)讓整個 job 紅;package 段 log 最後一行印出路徑
image 188 本機 docker + Harbor harbor.jedicogy.com.tw:8081/guidant-ai/ 本機給下次 skip_be 沿用,Harbor 留出版紀錄

artifacts 路徑只能寫相對於 runner 自己工作目錄(CI_PROJECT_DIR)的路徑,不能直接指到 /opt,所以每個 job 收尾時把 .build/sbom 與 .build/logs 用 rsync 搬到 $CI_PROJECT_DIR/out/ 再上傳;失敗時也會搬,log 才看得到。

§8

擴環境怎麼加

要加 stg 或 poc 時:

  1. .gitlab-ci.yml 的 spec.inputs.environment.options 加上環境名
  2. 照 deploy-e2e 複製一個 deploy job,rules 改成比對新環境名、environment.name/url 改掉
  3. 寫對應的部署腳本(或讓 deploy_e2e.sh 吃目標 host,它已經讀 E2E_HOST、E2E_DROP 環境變數)

限制誰能按:用 GitLab 的 Protected environments(受保護環境),設定哪些人可以部署到該環境。stg/poc 等同上版動作,加之前要決策者放行。

§9

踩過的坑(換機器會再撞的規則)

  • runner 使用者的 ~/.bash_logout 要移除:shell executor(runner 直接用本機 shell 跑指令的模式)會以登入 shell 執行 job。Ubuntu 預設的 .bash_logout 會呼叫 clear_console,在沒有終端機的 CI 環境裡回傳失敗,job 在「準備環境」階段就紅(GitLab 官方文件記載的已知問題)。188 的 runner 以 jedi 跑,所以要移除的是 /home/jedi/.bash_logout。
  • 收集 artifacts 前先清 out/:shell executor 的工作目錄跨 job 重用,前一個 job 留下的檔案會被下一個 job 一起打包。.collect 片段第一步就是 rm -rf out/,拿掉它就可能把 2.5GB 的安裝包誤包進 artifacts,觸發 413。
  • 證據分類 image 的來源 clone 要在 verify 先 git pull:evidence-classifier 不是從 Nexus 裝,是拿 188 上 /opt/jedi-python-package 這個 git clone 直接 build。不 pull 的話,套件改了內容沒 bump 版號時 CI 會 build 出舊的,而且沒有任何錯誤訊息。verify 固定跟 main。那個 clone 的 remote 目前靠嵌在 URL 裡的 token 讀取(188 的 ssh key 在 commonservice3 group 沒權限),token 到期前要改成把 key 加進該 group 的 deploy keys。
  • .venv/bin 要進 CI 的 PATH:人手跑時會先 source .venv/bin/activate,CI 的 shell 不會。pip-audit 等裝在 venv 裡的工具,要靠 .gitlab-ci.yml 頂層的 PATH 變數把 /opt/guidant-ai-be/.venv/bin 放最前面才找得到。
  • pgrep 要用 -x 整串比對:單用 pgrep -f 是在整條指令列裡找片段,連 ssh 帶過來的指令字串、CI 自己的 job 指令都會被抓到,變成誤判「有人在手跑」而拒跑。現在用 pgrep -x -f 搭配完整程序名的正規式。
  • Nuitka 當機會留下 nuitka-crash-report.xml:這支檔案會讓工作樹變髒、被 verify 擋下。已加進 .gitignore;換機器或換 repo 時要確認這條還在。