iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
Rust

Rust 後端入門系列 第 28

Day 28 Axum 整合 GitHub Actions 完成 CI

  • 分享至 

  • xImage
  •  

什麼是 GitHub Actions

  • GitHub Actions 是 GitHub 提供的 CI/CD 與自動化工作流程平台。你可以在 repo 裡用 YAML 定義工作(workflow),在指定事件(例如 push、pull_request)發生時,自動執行測試、建置、部署、發佈等任務。
  • 優點:直接整合在 GitHub 裡、支援自訂 runner、豐富的社群 action、容易用 secret 管理敏感資料。

主要概念

  • workflow:放在 .github/workflows/*.yml 的檔案,定義何時觸發與要執行的工作。
  • event:觸發條件,例如 push、pull_request、schedule(排程)、workflow_dispatch(手動觸發)。
  • job:workflow 下的單一工作單位,在 runner(執行環境)上執行,可設定並行或相依。
  • step:job 裡的單步驟,可以執行 shell 指令或呼叫 action。
  • action:可重複使用的單一功能單元(由 GitHub 或社群提供),可用在 step 裡。
  • runner:實際執行 job 的機器,可使用 GitHub 提供的 hosted runner(ubuntu-latest、windows-latest、macos-latest)或自建 runner。
  • secret:用來存放敏感資訊(API key、私密 token),在 workflow 內透過 secrets.NAME 使用,不會暴露在日誌。

常見進階功能

  • matrix(多組合測試):同一 job 可在多個組合(例如不同 Rust 版本、不同作業系統)上平行執行。
  • artifact:把建置輸出或測試報告上傳為 artifact 供後續下載或保留。
  • cache:加速相依套件載入(npm、cargo 等),減少重複下載時間。
  • secrets:使用 repo 或 org 的 secrets 儲存敏感資料,避免寫在原始碼裡。
  • 自建 runner:若需要特殊硬體或內網存取,可以自架 runner。

常見用途

  • 程式碼品質:lint、format 檢查(clang-format、rustfmt、eslint)
  • CI:build、unit/integration tests、coverage
  • CD:自動部署到伺服器、Docker Hub、雲端(GCP、AWS、Azure)
  • 自動化維護:依賴更新(dependabot)、自動發 release、發佈 GitHub Release

使用GitHub Actions的好處

  • 持續整合(CI):每次 push / PR 自動執行測試,及早發現錯誤,避免不穩定程式被合入主分支。
  • 可重現性:CI 的環境固定,能穩定重現錯誤,減少「在我機器可以但 CI 失敗」的問題。
  • 自動化品質門檻:使測試成為合併條件(守門),提升程式品質與團隊信心。
  • 加速開發:PR 時即時回饋測試結果,降低人工測試成本。

修復先前程式的錯誤

handlers.rs 的 delete_user 部分:

let res = sqlx::query(
		r#"
		DELETE FROM users
		WHERE id = $1 AND id = $2
		"#,
	)
	.bind(id)
	.bind(caller_id)
	.execute(&pool)
	.await
	.map_err(|e| internal_err(e))?;

在 integration_tests.rs,將登入測試移到 PUT 前方添加並取得 jwt ,在測試 PUT 和 DELETE 時使用 jwt

#[tokio::test]
async fn integration_create_get_update_delete_user_flow() {
    // ...
    
	// POST /users/login -> 成功
    let res = client
        .post(format!("{}/users/login", base_url))
        .json(&serde_json::json!({
            "username_or_email": "user1",
            "password": "password"
        }))
        .send()
        .await
        .unwrap();
    assert_eq!(res.status(), StatusCode::OK);
	
	let login_body: Value = res.json().await.unwrap();
	let jwt = login_body
		.get("access_token")
		.and_then(|v| v.as_str())
		.expect("login response should contain string field `access_token`");

    // PUT /users/{id} - 成功
    let res = client
        .put(format!("{}/users/{}", base_url, id))
		.bearer_auth(jwt)
        .json(&serde_json::json!({"username": "new_user1"}))
        .send()
        .await
        .unwrap();
    assert_eq!(res.status(), StatusCode::OK);

    // DELETE /users/{id} -> 成功
    let res = client
        .delete(format!("{}/users/{}", base_url, id))
		.bearer_auth(jwt)
        .send()
        .await
        .unwrap();
    assert_eq!(res.status(), StatusCode::NO_CONTENT);

    // ...
}

範例

建立一個針對 Rust 專案的 CI 工作流程,在 push 與 pull_request 時自動跑測試,包含與外部服務(Postgres、Redis)的整合測試場景。

把以下內容放到 .github/workflows/ci.yml:

name: CI

# 在 push 與 pull_request 時觸發
on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

env:
  # 測試用 Postgres 帳號(與 services.db.env 保持一致)
  POSTGRES_USER: myapp
  POSTGRES_PASSWORD: myapp_pass
  POSTGRES_DB: myapp_db

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    services:
      # Postgres service 用於測試(對應你的 tests spawn_app 使用)
      postgres:
        image: postgres:18
        env:
          POSTGRES_USER: ${{ env.POSTGRES_USER }}
          POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
          POSTGRES_DB: ${{ env.POSTGRES_DB }}
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10

      # Redis service
      redis:
        image: redis:8
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Rust
        uses: dtolnay/gh-actions-rust@stable
        with:
          toolchain: stable
          profile: minimal

      - name: Cache cargo registry and index
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Wait for Postgres to be ready
        # Sleep a bit to allow DB initialization; pg_isready run in service but give extra buffer
        run: sleep 5

      - name: Export test env vars
        run: |
          echo "TEST_DATABASE_URL_BASE=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432" >> $GITHUB_ENV
          echo "TEST_REDIS_URL=redis://127.0.0.1:6379/" >> $GITHUB_ENV

      - name: Install system deps for tests (libssl for sqlx)
        run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config

      - name: Run cargo test
        env:
          # cargo test 期間可用到 DB 與 REDIS env(spawn_app 會用 TEST_DATABASE_URL_BASE)
          TEST_DATABASE_URL_BASE: ${{ env.TEST_DATABASE_URL_BASE }}
          TEST_REDIS_URL: ${{ env.TEST_REDIS_URL }}
          RUST_LOG: info
        run: cargo test --workspace --verbose --all-features

觸發條件

  • on: push 與 pull_request,且 branches 設為 ["**"],表示所有分支的 push/PR 都會觸發 CI,方便在不同分支上驗證變更。

環境變數管理

  • 在 workflow 頂端用 env 宣告 Postgres 的帳號密碼與資料庫名稱,讓 services 與後續步驟可以共用同一組設定,維持一致性。

services(Postgres、Redis)

  • 使用 Docker 服務容器在 runner 上啟動 Postgres 與 Redis,模擬真實依賴環境,適合整合測試。
  • Postgres 指定 image: postgres:18,並傳入環境變數(由 workflow-level env 引用)。
  • 透過 options 設定 health checks(pg_isready、redis-cli ping)與重試參數,確保服務啟動且可用再跑測試,降低因 DB 尚未就緒造成的 CI 偶發失敗。
  • 提供 ports 映射(5432、6379),在 hosted runner 通常不必要,但在本地測試或 debug 時有幫助;注意在 GitHub-hosted runner 裡 port 映射不會對外暴露 runner 的網路。

步驟重點

  • Checkout:使用 actions/checkout@v4 取出 repo 程式碼。
  • Set up Rust:使用 dtolnay/gh-actions-rust 設定 stable toolchain,profile=minimal 加快安裝速度。
  • Cache cargo:用 actions/cache 快取 ~/.cargo 與 target,透過 Cargo.lock 的 hash 作為 key,加速重複執行的依賴安裝與建置。
  • Wait for Postgres:簡單 sleep 5 作為額外緩衝(service 的 health check 已有,但 extra sleep 可減少 race condition)。
  • Export test env vars:將 TEST_DATABASE_URL_BASE 與 TEST_REDIS_URL 寫入 GITHUB_ENV,讓後續步驟(cargo test)能取得正確的測試連線字串。此處把 DB host 指向 localhost,配合 services 使用。
  • Install system deps:安裝 libssl-dev 與 pkg-config(常見於 sqlx / openssl 需求),避免測試在缺系統函式庫下失敗。
  • Run cargo test:在設定好的環境變數下執行 cargo test --workspace --all-features,包含整個 workspace 與所有 feature,確保整體相依與條件都被驗證。

可靠性與性能考量

  • health-cmd 與重試次數能減少偶發的「服務尚未就緒」錯誤,但仍要注意資料庫初始化時間(大型 migration 會更久)。
  • Cache 設計以 Cargo.lock 的 hash 作為 key,可在依賴沒變動時大幅減少安裝時間。restore-keys 提供 fallback。
  • 使用 profile: minimal 與 apt-get 只安裝必要套件,可縮短 workflow 時間與降低 runner 負擔。
  • checkout code、還原 cargo cache、啟動 Postgres 與 Redis(GitHub Actions services)、設定 env(TEST_DATABASE_URL_BASE / TEST_REDIS_URL),執行 cargo test。
  • 為加速 CI,使用 cargo cache 與 docker build cache。

將程式碼 push 到 GitHub 後,一開始會是黃燈,經過一些時間後,黃燈將轉變成跟下面一樣的綠色勾勾。

實務建議與注意事項

  1. Secrets 與安全性
    • 實際專案時,不要把敏感資訊直接寫在 workflow 裡,使用 GitHub Secrets(Settings → Secrets)管理 JWT_SECRET、 密碼等。
    • 測試 job 不需要 production secrets;盡量使用測試專用的 env(例如 TEST_DATABASE_URL_BASE)。
  2. 快取與效能
    • 使用 actions/cache 快取 cargo registry、git 與 target 可以加速重複執行。
    • cache key 用 Cargo.lock hash 可以在依賴改變時自動失效。
  3. 測試在容器內執行
    • services 的容器在 runner network 中,使用 localhost 與指定 ports 即可連接。確保 tests 將 env 指向 localhost 而不是 service name(actions services port會映射到 localhost)。
  4. 分支策略
    • 建議把 build(推映像)限制在 main 分支或 release tag,避免每次 PR 都推 image。可以在 build job 加上 if: github.ref == 'refs/heads/main' 或 if: startsWith(github.ref, 'refs/tags/')。
  5. 快速失敗與回報
    • 把 tests 設為必須通過的 branch protection(GitHub Branch Protection Rules),確保 PR 在 merge 前測試通過。

上一篇
Day 27 Axum 專案使用 Docker 打包
下一篇
Day 29 使用 k6 壓力測試專案
系列文
Rust 後端入門30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言