文件版號 v2026100501(討論與引用請註明版號)。英文版為正本(English · SSOT),本文為中文版,內容與同版號的英文版一致。給各部門工程師與 SRE。上版約定與分支範本講「做什麼」,這份講「怎麼做」。標 約定 的章節說明 SRE 怎麼處理交付,所有人適用;標 範本 的章節是建議的 app repo pipeline,可以採用或自行替換。
交付就是每個 repo 一個 tag 與它的完整 commit hash。SRE 核對兩者一致,就部署那一組。
artifact 由交付的 tag build,以 commit 識別。灰度與 production 跑同一個 artifact。
build 時 CI 沒有任何環境的設定,image 裡不能有 domain、API 位址、開關值。
SRE 在 GitLab 上核准上版申請;之後 K8s 由 GitOps 同步、VM 由部署 job 執行,兩者對申請人都不可見。
git rev-parse <tag>^{commit})。對不上就不上,直到 Release Manager 確認更正後的組合。環境 repo是記錄「每個環境現在跑哪個版本、用什麼設定」的 git repo(公司目前是 fleet-delivery)。GitOps 工具(Argo CD)自動把環境同步成 repo 上寫的樣子。目前涵蓋 K8s 上的服務,VM 服務之後納入。上版申請就是對它開的 MR:SRE 核准就是上版的決定,合併就是部署。
| 環境 repo 路徑 | 內容 | 誰核 | 核准後 |
|---|---|---|---|
workloads/<svc>/<自驗環境>/ | 渲染後的 K8s manifests | 自動合併 | GitOps 同步 |
workloads/<svc>/gray/ | 同上,版本 = 交付的 tag(灰度,真實流量) | SRE(CODEOWNERS) | GitOps 同步 |
workloads/<svc>/production-<cluster>/ | 同上,版本 = 通過灰度的 tag | SRE(CODEOWNERS) | GitOps 同步 |
workloads/<svc>/production-vm/ | version.yaml(tag)與設定檔 | SRE | 部署 job 在 on-prem runner 對 VM 做 git checkout <tag> 與重啟 |
| 同路徑,只改設定值 | 功能開關、環境變數;artifact 不變,標 config-only | SRE,依 Release Manager 的申請 | 同上;讀 env 的服務 rollout restart |
| 退版 | SRE 依 release plan 反向,對每個 repo 部署上一版。K8s 服務:把 production 版本改回上一版(部署 MR 頁的 Revert 按鈕,或改回上一版 tag 的新 MR);VM 服務:目前用既有部署腳本,之後納入環境 repo。可做成手動 job rollback-release:輸入 release plan,依反向順序逐一退版 | SRE 宣告並執行 | 逐一回到上一版 |
Argo CD 是自動同步、沒有核准概念;多數部門也不在 K8s 上。把核准放在 GitLab 的 MR,K8s 與 VM 就走同一道門。
SRE 看到的都是「某 service 的 production 版本從 A 改成 B」。服務從 VM 搬到 K8s,是同一個目錄換內容,流程不變。
環境 repo 的 production 目錄尚未接到 production 叢集;ApplicationSet 的目標叢集要依目錄決定;main 要加 CODEOWNERS 與 code owner approval;tag ↔ hash 核對與 rollback-release job 尚未實作。以上由 SRE 完成。
配合分支範本的共用 CI/CD component,各 repo 只提供測試指令、Dockerfile 和 service 名。三條路徑:一般 MR / push、rc tag、合進 master 之後的正式 tag。
各 stack 的測試指令
部到自驗環境
內容 == production 上的 rc
rc 在 release/* 或 hotfix/* 上
build 一次,記錄 digest
開上版申請:tag、hash、digest
== master tip,內容 == production 上的 rc
同一個 image 加別名 X.Y.Z
Release 補 digest、MR 清單
刪 release 與已合併 feat
| job | 何時跑 | 做什麼 |
|---|---|---|
test | MR、branch push | 各 stack 的測試指令(第 5 節)。 |
deploy-dev | 手動 | build 當下 commit 的 image,部到自驗環境,由開發人員選環境。 |
check-merge | MR → master | 守門,見第 4 節。純 git,不呼叫 API。 |
tag-verify | tag pipeline 第一步 | 守門,見第 4 節。不成立後面都不跑。 |
build | rc tag | build image,registry tag = commit,輸出 digest。唯一會 build 給灰度 / production 的時機。 |
request-deploy | rc tag | 對環境 repo 開(或更新)上版申請,帶 tag、完整 commit hash 與 digest;Release Manager 補上 release plan 與退版方式。之後由 SRE 接手(第 2 節)。 |
retag | 正式 tag | crane tag / skopeo copy 把 production 上那個 rc 的 image 加別名 X.Y.Z;不重新部署。 |
release-notes | 正式 tag | 更新 GitLab Release:digest、對應的 rc、Milestone 的 MR 清單。 |
cleanup | 正式 tag | 刪除 release/X.Y.Z(或 hotfix/*)與已合併的 feat/*。 |
兩個 job 讓範本的承諾不靠人記:「production 跑的就是交付的 tag」「master 只收驗證過的東西」。它們跑在團隊自己的 repo,保護的是團隊;SRE 自己的檢查是第 2 節的 tag ↔ hash 核對。
| 守門 | 條件 | 不成立時 |
|---|---|---|
check-mergeMR → master | ① 來源是 release/* 或 hotfix/*;② 來源的最新 commit 正好是一個 rc tag(production 上的那個,之後不能再有 commit);③ master 是這個 commit 的祖先。 | ①:不該進 master 的分支。②:production 之後又推了東西,要交付新的 rc。③:期間有別的東西進了 master,先把 master 合進來、打新的 rc。 |
tag-verifytag pipeline | rc tag:commit 在 release/* 或 hotfix/* 上。正式 tag:commit 是 master 最新,檔案樹與某個 rc 完全相同(git diff --quiet rc final),且該 rc 是它的祖先。 | tag 建錯位置。交付前刪掉重建即可,沒有任何東西被部署。 |
不論用哪種 pipeline,各 stack 差的是四件事:怎麼測、怎麼 build、artifact 是什麼、設定怎麼進去。「設定在部署時注入」屬於約定(build 一次);其餘是建議。
| Next.js | Java Spring Boot | Laravel | |
|---|---|---|---|
test | npm ci && npm test、npm run lint、tsc --noEmit | ./mvnw -B verify 或 ./gradlew test | composer install、vendor/bin/phpunit |
build | multi-stage:npm ci → next build(output: 'standalone')→ runtime image 只帶 .next/standalone | multi-stage:mvnw package → JRE image 帶 jar;或 buildpacks / jib | multi-stage:composer install --no-dev、config:cache → php-fpm + nginx image |
| artifact | container image;共用元件發 npm package | container image;共用元件發 jar | container image;共用元件發 composer package |
| 設定注入 | server side 執行期讀 env。client side 不能用 NEXT_PUBLIC_* 放環境值(build 時烙進 bundle):改用同源相對路徑、server 提供 /api/config、或容器啟動時產生 env.js | 執行期 env → application.yaml placeholder,或 ConfigMap 掛成 application-prod.yaml | 執行期 env → config/*.php(config:cache 後只認 config) |
| 功能開關 | server side 讀 env,或 client 從 /api/config 拉;OpenFeature JS SDK 可統一 | 執行期用 Environment 讀,或 Togglz / OpenFeature;@ConditionalOnProperty 只在啟動時判斷 | config('features.x') 從 env 來;用套件時 driver 指到 env,不是 DB |
| 改設定要不要重啟 | 讀 env:要;讀掛載檔:不用 | 要(預設);Spring Cloud Kubernetes reload:不用 | 要(config:cache) |
| health | 自訂 /api/health | Actuator /actuator/health/readiness | Laravel 11 /up |
| DB migration | 通常沒有;Prisma / Drizzle 同右 | Flyway / Liquibase,expand / contract | artisan migrate 在 Job / init container;不退 migration |
| 版本可觀測 | image label org.opencontainers.image.revision=<commit>;app 露出 /version 回 commit,SRE 可在灰度與 production 確認跑的就是交付的 hash。 | ||
NEXT_PUBLIC_*),視為程式碼而不是環境設定:它跟著 commit 走,不會因環境而異。| 設定 | 值 |
|---|---|
| Protected branches | master、release/*:push = No one,merge = Maintainers。 |
| Merge method | merge commit。分支之間用 MR 合併整條分支,不用 cherry-pick。 |
| Squash | project 設定「允許」、預設不勾(GitLab 只能 project 層級設定,不能依目標分支)。只用在 feat/*、hotfix/* → release/*;release/* → master 勾了會被 check-merge / tag-verify 擋下。feat squash 後不會被 git 認成已合併,清分支改靠「merge 後刪 source branch」。 |
| Protected tags | v*:只有 Maintainer(Release Manager)可建。 |
| Push rules | 分支名 ^(feat|hotfix|release)/;commit message 含票號 #\d+;author email @fmt.com.tw(既有)。 |
| MR | merge 後自動刪 source branch;pipeline 必須成功;目標為 master 與 release/* 需要 approval。 |
| Milestone / Release | 每版一個 vX.Y.Z;MR 掛 Milestone;Release 由 Release Manager 建立,notes 自動產生。 |
| Registry | immutable tags;保留策略只清 commit tag,不清版號 tag。 |
| Runner | 要呼叫 GitLab API 的 job 用公司的 group runner(tags:);純 git 檢查用 GIT_FETCH_EXTRA_FLAGS。 |
| 設定 | 值 |
|---|---|
| Protected branch | main:push = No one;merge = Maintainers + 各課的 bot;Code owner approval required。 |
| CODEOWNERS | /workloads/*/production*/、/workloads/*/gray/ → sre group。只用 CODEOWNERS,不另加全 main 的 approval rule。 |
| Merge method | merge commit(Revert 按鈕退整個部署)。 |
| Bot | 每課一個 service account(Developer + 可 merge main),token 放課的 group CI variable;以 git commit + push 提交,committer 用 <svc>-gitops-bot@fmt.com.tw。 |
| Deploy freeze | GitLab deploy freeze;凍結期間申請照開、不核准。 |
只需要交付:每個 repo 前一工作日交一個 tag + 完整 commit hash、artifact 由那個 commit build 且設定在部署時注入、加上上版申請(第 2 節)。怎麼產生由你們決定。
頂層 group 有 IP allowlist,gitlab.com 的 SaaS runner 不在名單內。把 job 指到公司的 group runner(tags:);純 git 的檢查不需要 API,用 GIT_FETCH_EXTRA_FLAGS 讓 runner 自己帶 refs。
push rule 要求 author email 是 @fmt.com.tw,bot 帳號的 email 不是。改用 git commit + push,committer 設成 <svc>-gitops-bot@fmt.com.tw。
新 project 預設 ci_pipeline_variables_minimum_override_role = no_one_allowed。需要時設成 maintainer,或改用 pipeline inputs。
GitLab 自動保護 default branch 時把 push 給了 Maintainers。建 repo 後要手動改成 No one。
擋得住 git push --delete;Maintainer 從 UI / API 仍可刪。「tag 不可變」靠流程,不靠這個設定。
commits /revert API 不接受 merge commit。用 MR 頁的 Revert 按鈕,或 git revert -m 1。
code owner 自己開的 MR 視為已滿足 code owner 規則,退版因此很快。要求兩人才能動 production 時,另加一條 approval rule。