第 3 章:CI 第一步 — 用 GitHub Actions 自動跑測試與 build

第 3 章:CI 第一步 — 用 GitHub Actions 自動跑測試與 build

這一章在做什麼? 我們要把 code/ 推上 GitHub,然後設定「每次 push,GitHub 就自動幫你跑測試、靜態檢查、並 build 一次 Docker image」。做完這章,壞掉的程式碼會在進主線前就被機器擋下來——你不用再靠「人工記得跑測試」。

為什麼從 CI 開始? 因為 CI(持續整合)是整條自動化管線的第一節,也是最有感的一節:它把「每次改完都要手動驗證」這件最常做、最容易忘的事,變成自動發生。

前置:第 2 章的 image 優化、以及 code/ 能跑。本章新增 code/internal/handler/handler_test.gocode/.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 才抓得到)。

  1. code/ 初始化 git 並提交(若已是 repo 可跳過 init):
cd code
git init
git add .
git commit -m "chore: 初始化 no-logic-trade 後端"
  1. 在 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.Tgo 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: vs run: 是兩種步驟:
    • uses: actions/checkout@v4使用別人寫好的 action(可重用的小工具)。@v4 是版本。checkout 負責把你的 repo 拉到機器上——幾乎每個 workflow 第一步都是它。
    • uses: actions/setup-go@v5 + with::安裝指定版本的 Go。with: 是傳給 action 的參數(go-versioncache)。
    • run: go vet ./...直接在機器上執行一行 shell 指令
  • 後面三個 run 步驟,就是把你本機會做的事(go vetgo testdocker 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.sumruns-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 打上版本標籤。