流程、觸發、產物落點、為什麼這樣選
本頁描述 GitLab CI 發版流程現在的規則與理由。實際行為以 .gitlab-ci.yml 與 scripts/build/*.sh 的檔頭註解為準;本頁與程式碼不一致時,相信程式碼。
一次 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);正式出貨要另跑一次不帶這個旗標。
只有人主動發才會跑:
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 | 不勾 |
v<X.Y.Z>)由決策者手打,CI 完全不碰 tag。 CI 只負責「把指定 commit 做成一包、升到 e2e」,哪一包算定版是人的判斷。<pyproject 版號>-<commit 短 hash>,例如 1.23.1-a1b2c3d。定版前同一個版號會反覆出測試包,帶上 hash 就不會互相覆蓋,也能一眼看出哪個 commit 最後被打了 tag。guidant-ai-be:1.23.1),推上 Harbor 時才加 hash 後綴。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 執行完留下、可在網頁下載的檔案)。
所有環境都走安裝包升級,和客戶升級是同一條路:
guidant-ai-<版號>.tar.gz 與 .sha256)scp 到 190 的 /home/jedi/sudo ./install.sh --upgrade --force,install.sh 會依序:完整備份資料庫 → 載入新 image 並比對 digest → 套增量 migration → 換版重啟並等 healthcheck 全綠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 會指向一個日後被刪掉的路徑。
/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 和人手跑共用同一棵工作樹,對策:
pgrep 找 nuitka/build_all.sh/build_release.sh/build_image.sh,找到就停。兩條 build 同時跑會互撞,linker 直接 crash。git status 有任何未提交的改動就停並列出來,不會悄悄 reset 掉別人的東西。| 產物 | 在哪 | 理由 |
|---|---|---|
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 才看得到。
要加 stg 或 poc 時:
.gitlab-ci.yml 的 spec.inputs.environment.options 加上環境名deploy-e2e 複製一個 deploy job,rules 改成比對新環境名、environment.name/url 改掉deploy_e2e.sh 吃目標 host,它已經讀 E2E_HOST、E2E_DROP 環境變數)限制誰能按:用 GitLab 的 Protected environments(受保護環境),設定哪些人可以部署到該環境。stg/poc 等同上版動作,加之前要決策者放行。
~/.bash_logout 要移除:shell executor(runner 直接用本機 shell 跑指令的模式)會以登入 shell 執行 job。Ubuntu 預設的 .bash_logout 會呼叫 clear_console,在沒有終端機的 CI 環境裡回傳失敗,job 在「準備環境」階段就紅(GitLab 官方文件記載的已知問題)。188 的 runner 以 jedi 跑,所以要移除的是 /home/jedi/.bash_logout。out/:shell executor 的工作目錄跨 job 重用,前一個 job 留下的檔案會被下一個 job 一起打包。.collect 片段第一步就是 rm -rf out/,拿掉它就可能把 2.5GB 的安裝包誤包進 artifacts,觸發 413。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-crash-report.xml:這支檔案會讓工作樹變髒、被 verify 擋下。已加進 .gitignore;換機器或換 repo 時要確認這條還在。