.github/
└─ workflows/
   ├─ ci.yml         # 每個 push / PR 都會跑
   └─ release.yml    # 打 tag 時建置並發佈
astral-sh/setup-uv@v3 裝好 uv,uv sync --locked 依鎖檔還原依賴,並啟用快取加速。coverage.xml,並設 -cov-fail-under 門檻。name: CI
on:
  push:
    branches: [ main ]
  pull_request:
jobs:
  build-test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Set up uv with cache
        uses: astral-sh/setup-uv@v3
        with:
          enable-cache: true  # 啟用 uv 快取,加速 CI 安裝
      - name: Sync dependencies from lock
        run: uv sync --locked
      # Lint(Ruff + Black --check)
      - name: Lint
        run: |
          uv run ruff check .
          uv run black --check .
      # Type check(Pyright 或 mypy,依你的專案)
      - name: Type check
        run: |
          if uv run -q pyright --version; then uv run pyright .; fi
          uv run mypy src/
      # Test with coverage(門檻請對齊 Day 11)
      - name: Test
        run: uv run pytest -q --cov=your_package --cov-report=xml:coverage.xml --cov-fail-under=80
      - name: Upload coverage xml
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.python-version }}
          path: coverage.xml
      # Build wheel/sdist 只是驗證能打包成功,產物上傳 artifact 供下載
      - name: Build
        run: uv run hatch build
      - name: Upload dist
        uses: actions/upload-artifact@v4
        with:
          name: dist-${{ matrix.python-version }}
          path: dist/
為什麼用 uv 而不是 pip install -r?因為我們在已經把「完整依賴樹」鎖進 uv.lock,CI 用 uv sync --locked 可確保版本 100% 一致,還能吃到快取。
a|b|rc)推 TestPyPI,否則推正式 PyPI。hatch publish 與平台 Secrets:TEST_PYPI_TOKEN、PYPI_TOKEN。name: Release
on:
  push:
    tags:
      - "v*.*.*"     # e.g. v0.2.0, v1.0.0, v1.2.0rc1
jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - uses: astral-sh/setup-uv@v3
        with:
          enable-cache: true
      - name: Sync deps from lock (no dev)
        run: uv sync --locked --no-dev
      - name: Build package
        run: uv run hatch build -t wheel -t sdist
      - name: Upload dist as artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-dist
          path: dist/
      # Decide target repo based on tag: pre-release -> TestPyPI, else PyPI
      - name: Publish to TestPyPI (pre-release)
        if: contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc')
        env:
          TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
        run: |
          uv run hatch publish --repo testpypi --user __token__ --auth "$TEST_PYPI_TOKEN"
      - name: Publish to PyPI (stable)
        if: !(contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc'))
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          uv run hatch publish --user __token__ --auth "$PYPI_TOKEN"
穩定版走 PyPI,預發佈走 TestPyPI;一律用 token,絕不把憑證寫進 repo。
ruff/black/mypy/pytest 兜在 Hatch scripts(hatch run ci),可在 CI 直接呼叫同一入口,減少兩邊漂移。pyright/mypy,並保留 Pydantic v2 的契約測試區,讓「邊界資料」在 PR 階段就爆錯。-cov-fail-under 鎖住最低標;必要時再加上 -junitxml 輸出供平台收集。.env committed,對應 Day 12 的祕密管理原則。| 症狀 | 可能原因 | 快速修正 | 
|---|---|---|
| CI 安裝依賴忽快忽慢 | 沒吃到快取 | setup-uv@v3打開enable-cache: true,並使用uv sync --locked。 | 
| 本地過、CI 不過 | 本地與 CI 執行入口不同 | 把 Lint/Typecheck/Test 收斂到 Hatch scripts,CI 直接呼叫同一組腳本。 | 
| 釋出上 PyPI 失敗 | token 或 repo 判斷錯誤 | 檢查 tag 規則與條件區塊,預發佈分流到 TestPyPI;token 放 Secrets。 | 
| 覆蓋率門檻不一致 | 本地/CI pytest 參數不同 | 固定在 pyproject.toml的 pytest options,減少環境差異。 | 
uv.lock 帶進去,uv sync --frozen 即可重現。postCreateCommand 跑 Hatch 一鍵化腳本,讓本地、容器與 CI 的入口一致。這組範本的關鍵不是 Actions 的 YAML 花樣,而是把前面 24 天打好的地基搬進 CI/CD:
用鎖檔確保重現性、用一鍵化縮減心智負擔、用覆蓋率與型別檢查把品質前移、用 TestPyPI/ PyPI 雙軌避免一次到地獄。接下來,你只需要專心寫功能,剩下的交給 pipeline。