.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。