第 3 章:CI 第一步 — 用 GitHub Actions 自動跑測試與 build
這一章在做什麼? 我們要把 code/ 推上 GitHub,然後設定「每次 push,GitHub 就自動幫你跑測試、靜態檢查、並 build 一次 Docker image」。做完這章,壞掉的程式碼會在進主線前就被機器擋下來——你不用再靠「人工記得跑測試」。
為什麼從 CI 開始? 因為 CI(持續整合)是整條自動化管線的第一節,也是最有感的一節:它把「每次改完都要手動驗證」這件最常做、最容易忘的事,變成自動發生。
前置:第 2 章的 image 優化、以及
code/能跑。本章新增code/internal/handler/handler_test.go與code/.github/workflows/ci.yml。
步驟 1:先搞懂三個名詞
CI(Continuous Integration,持續整合)
每次有人 push 程式碼,就自動跑測試與 build,及早抓出問題。核心精神:別讓壞掉的 code 累積,每次小改動都立刻驗證。
GitHub Actions
GitHub 內建的自動化平台。你在 repo 裡放一個設定檔(YAML),描述「當某事件發生時(例如 push),要在一台雲端機器上依序做哪些事」。GitHub 會免費提供這台機器(公開 repo 額度很大)跑你的流程。
YAML
一種用縮排表示層級的設定檔格式,副檔名
.yml/.yaml,DevOps 到處都是它。三個規則先記住:
key: value表示一個設定。- 縮排(只能用空白、不能用 Tab)表示「屬於上一層」。
- 以
-開頭表示「清單的一項」。#後面是註解。
🔁 前端類比:CI 就像你在前端設定的「push 就自動跑 ESLint + Jest + build」。GitHub Actions 是執行這套流程的機器人,YAML 是你給機器人的工作說明書。
步驟 2:把 code/ 推上 GitHub
我們把 code/ 當成「你的專案 repo 根目錄」(.github/workflows/ 一定要在 repo 根目錄,Actions 才抓得到)。
- 在
code/初始化 git 並提交(若已是 repo 可跳過 init):
cd code
git init
git add .
git commit -m "chore: 初始化 no-logic-trade 後端"
- 在 GitHub 建立一個新的 repo 並推上去。最快是用 GitHub CLI(
gh):
# 若沒裝 gh:https://cli.github.com/ ;裝完先 gh auth login
gh repo create no-logic-trade --public --source=. --remote=origin --push
或手動(在 GitHub 網頁先建一個空 repo,再):
git branch -M main
git remote add origin https://github.com/<你的帳號>/no-logic-trade.git
git push -u origin main
💡 為什麼建議公開(public)repo? 公開 repo 的 GitHub Actions 完全免費、額度大;第 4 章把 image 推到 GHCR 也最省事(公開 image 免設定拉取憑證)。先用公開練習最順;正式專案再評估私有。 ⚠️ 推之前再確認一次:
code/.gitignore有排除.env(含密碼)。千萬別把.env推上去。
步驟 3:替 app 補一個最小單元測試
CI 要「跑測試」,那得先有測試。我們替第 4 章金額格式化函式 formatCents 寫一個單元測試。建立 code/internal/handler/handler_test.go:
// code/internal/handler/handler_test.go
package handler
import "testing"
// 表格驅動測試:把多組「輸入/期望」放進一張表,逐筆驗證
func TestFormatCents(t *testing.T) {
cases := []struct {
name string
cents int64
want string
}{
{"零", 0, "0.00"},
{"整數元", 100000, "1000.00"},
{"含分", 150050, "1500.50"},
{"個位分補零", 5, "0.05"},
{"負數", -2399, "-23.99"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := formatCents(c.cents)
if got != c.want {
t.Errorf("formatCents(%d) = %q,期望 %q", c.cents, got, c.want)
}
})
}
}
Go 測試的規則(第一次寫,重點解釋):
- 檔名以
_test.go結尾:Go 把這種檔案視為測試,只有go test時才編譯,不會進正式執行檔。 - 函式以
Test開頭、收一個*testing.T:go test會自動找到並執行它。 - 同一個
package handler:所以能存取小寫開頭的formatCents(同 package 內可見)。 - 表格驅動測試:把測資放進一個 slice,用迴圈跑——加一個案例只要加一列,是 Go 社群最常見的測試風格。
t.Run(名稱, 函式):每個案例變成一個有名字的子測試,失敗時看得出是哪一筆。t.Errorf:標記失敗並印訊息(但會繼續跑其他案例)。
本機先確認測試會過:
cd code
go test ./...
# 預期:ok no-logic-trade/internal/handler ...,其餘 [no test files]
步驟 4:寫第一個 workflow(CI)
建立 code/.github/workflows/ci.yml:
# code/.github/workflows/ci.yml
name: CI
# 何時觸發:push 到 main、或對 main 發 PR
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: 測試與建置
runs-on: ubuntu-latest # 在一台全新的 Ubuntu 雲端機器上跑
steps:
- name: 取出原始碼
uses: actions/checkout@v4 # 把你的 repo 內容 checkout 到這台機器
- name: 安裝 Go
uses: actions/setup-go@v5
with:
go-version: '1.22' # 對齊 go.mod 的版本
cache: true # 快取 Go 模組,下次更快
- name: 靜態檢查
run: go vet ./...
- name: 跑測試
run: go test -v ./...
- name: 建置 Docker image(驗證能 build)
run: docker build -t nlt-api:ci .
逐段(逐行)解釋——這是你的第一份 YAML:
name: CI:這個 workflow 的顯示名稱(會出現在 GitHub 的 Actions 頁)。on::觸發條件。底下:push: branches: [ main ]:有人 push 到main分支時觸發。pull_request: branches: [ main ]:有人對main開 PR 時也觸發(讓 PR 在合併前先被驗證)。[ main ]是 YAML 的「行內清單」寫法,等同於底下用-列一項。
jobs::一個 workflow 由一或多個 job 組成。test::我們替這個 job 取的 id(叫test)。name: 測試與建置:job 的顯示名稱。runs-on: ubuntu-latest:在哪種機器上跑。ubuntu-latest是 GitHub 提供的全新 Ubuntu(amd64——所以這裡 build 出來的 image 架構正好是 Cloud Run/GKE 要的,呼應第 2 章踩雷點)。steps::這個 job 要依序執行的步驟清單(每個-是一步)。uses:vsrun:是兩種步驟:uses: actions/checkout@v4:使用別人寫好的 action(可重用的小工具)。@v4是版本。checkout負責把你的 repo 拉到機器上——幾乎每個 workflow 第一步都是它。uses: actions/setup-go@v5+with::安裝指定版本的 Go。with:是傳給 action 的參數(go-version、cache)。run: go vet ./...:直接在機器上執行一行 shell 指令。
- 後面三個
run步驟,就是把你本機會做的事(go vet、go test、docker build)搬到雲端自動跑。任何一步失敗(回傳非 0),整個 job 就會標紅 ❌。
步驟 5:推上去,看它自動跑
把新檔案提交並 push:
cd code
git add internal/handler/handler_test.go .github/workflows/ci.yml
git commit -m "ci: 加上單元測試與 GitHub Actions CI"
git push
push 完,到 GitHub 你的 repo 頁面 → 上方 Actions 分頁。你會看到一個剛觸發的 CI 執行:
- 黃色轉圈=執行中;點進去可即時看每個步驟的 log。
- 綠色勾 ✅=全部通過。
- 紅色叉 ❌=某步失敗,點開該步看錯誤訊息。
✅ 如何驗證這一章成功
驗證 1:本機先綠
cd code
go vet ./... # 無輸出 = 通過
go test ./... # internal/handler ... ok
驗證 2:GitHub Actions 綠勾
- push 後到 repo 的 Actions 分頁。
- 看到最新一筆 CI 為綠色 ✅。
- 點進去,四個步驟(checkout/setup-go/vet/test/build)都打勾。
驗證 3:CI 真的會擋壞 code(可選,很有感)
故意把測試弄壞來看 CI 變紅:把測試裡 {"零", 0, "0.00"} 暫時改成 {"零", 0, "9.99"},push,Actions 會 ❌、log 顯示 formatCents(0) = "0.00",期望 "9.99"。確認後改回來再 push 就會恢復綠勾。
- [ ] 本機
go test ./...通過。 - [ ] repo 的 Actions 分頁出現綠色 ✅ 的 CI。
- [ ] (可選)弄壞測試能讓 CI 變紅、修好又變綠。
🧯 常見錯誤 / 踩雷點
| 症狀 | 原因 | 解法 |
|---|---|---|
| push 後 Actions 完全沒反應 | workflow 沒放在 .github/workflows/、或檔案不是 .yml、或 branch 不是 main |
確認路徑與副檔名;git branch 看你在不在 main(或把 on.push.branches 改成你的分支) |
YAML 報錯 did not find expected key 之類 |
用了 Tab 縮排,或縮排對不齊 | YAML 只能用空白;同層級縮排要一致(建議 2 空白) |
| CI 紅但本機綠 | Go 版本不一致、或相依沒進版控 | go-version 對齊 go.mod;確認 go.mod/go.sum 有 commit |
go test 在 CI 找不到套件 |
go.sum 沒 push、或 module 路徑錯 |
commit go.sum;runs-on 預設工作目錄就是 repo 根(即 code/) |
| Docker build 步驟很慢 | 每次都重抓相依 | 已用層快取;之後第 4 章會加 buildx 快取進一步加速 |
| 來自 fork 的 PR 拿不到 secrets | GitHub 安全限制(防止外人偷 secret) | CI(只測試/build)不需 secret,沒影響;需要 secret 的部署放在 push 觸發(第 6 章) |
不小心把 .env 推上去了 |
.gitignore 沒生效或用了 git add -f |
立刻撤換外洩的密碼;用 git rm --cached .env 移除追蹤並重 push |
下一步 → 第 4 章:CD — 自動 build Docker image 並推送到 GHCR
CI 通過後,我們要把 build 好的 image 推到 GitHub Container Registry(GHCR),讓它成為「可被部署的產物」。你會學到 registry 是什麼、如何用 GITHUB_TOKEN 安全登入、以及怎麼替 image 打上版本標籤。