uvをCI/CDに本気で組み込む — GitHub Actions実践レシピ7選(matrix / cache / uvx / pre-commit / Docker)

約25分で読めます by ぽんたぬき
uvをCI/CDに本気で組み込む — GitHub Actions実践レシピ7選(matrix / cache / uvx / pre-commit / Docker)

uvをCI/CDに本気で組み込む — GitHub Actions実践レシピ7選(matrix / cache / uvx / pre-commit / Docker)

はじめに — 「uvは導入した。でもCIはpipのまま」問題

ローカル開発環境にuvを導入したものの、GitHub ActionsではいまだにCIを起動するたびに pip install -r requirements.txt が走っている——そんな状況に心当たりはないでしょうか。

本記事では uvの基本的な使い方(uv init / uv add)は一切扱いません。すでにuvをローカルで使い始めた中級者が、CIパイプラインを丸ごとuvで統一するための実践的なレシピを7つ提供します。

この記事を読み終えると、以下が実現できます。

  • CI実行時間を体感で数分→数十秒に削減できる
  • ロックファイルの更新漏れをCIが機械的に検出するようになる
  • ローカル・CI・Dockerの3環境で依存解決が一本化される

まず完成形のワークフロー全体を確認したい方は、末尾の「完成版:コピペで動くワークフロー全文」セクションに飛んでください。


前提知識 — なぜCIでこそuvが効くのか

uvはpip比10〜100倍高速と言われますが、その差が最も顕著に出るのはCI環境です。ローカルではキャッシュが温まっているため実際の速度差を感じにくいですが、CIはジョブのたびにクリーンな環境から起動します。コールドスタート時の依存解決(resolver)速度の差が、そのまま請求時間に直結するわけです。

また、uvはpip・pipx・pyenv・poetry・virtualenvを単一のツールで代替できます。次の表でCIのステップがどれだけシンプルになるか確認してください。

従来のCIステップ uvでの置き換え
actions/setup-python astral-sh/setup-uv(Python管理込み)
pip install -r requirements.txt uv sync --locked
source .venv/bin/activate 不要(uv run が自動認識)
pipx install ruff uvx ruff(インストール不要)
pip install poetry 不要

バージョン固定について: uvはリリース頻度が高く、マイナーバージョンアップでも動作が変わることがあります。version: "0.12.1" のようにピン留めしておかないと、CIが突然壊れる事態になります。Renovate / Dependabotでの定期更新運用をセットで考えましょう。


レシピ①:setup-uv と actions/setup-python をどう使い分けるか

基本形 — astral-sh/setup-uv だけで完結させる

- uses: astral-sh/setup-uv@v5
  with:
    version: "0.12.1"
    python-version: "3.12"
    enable-cache: true

astral-sh/setup-uv はuvのインストールとPython管理を同時に担います。python-version を指定するだけで、uvが指定バージョンのPythonを自動ダウンロードするため、actions/setup-python は原則不要です。

setup-python を併用すべきケース

ただし、次のいずれかに当てはまる場合は actions/setup-python との併用を検討してください。

  • GitHubホストランナーにプリインストール済みのPythonバージョンをそのまま使いたい
  • ランナーのツールキャッシュがヒットすることでダウンロード時間を完全にゼロにしたい

よくあるハマりどころ

python-versionastral-sh/setup-uvactions/setup-python の両方で指定すると、バージョンが食い違うことがあります。また、リポジトリルートに .python-version ファイルがある場合、uvはそちらを優先するため、ワークフロー上の指定が無視されることがあります。迷った場合は setup-uv 単体で統一するのがベストプラクティスです。


レシピ②:matrix strategy で Python 3.11 / 3.12 / 3.13 × 3 OS を横断テストする

最小構成のmatrix定義

strategy:
  fail-fast: false
  matrix:
    python-version: ["3.11", "3.12", "3.13"]
    os: [ubuntu-latest, windows-latest, macos-latest]

runs-on: ${{ matrix.os }}
steps:
  - uses: actions/checkout@v4
  - uses: astral-sh/setup-uv@v5
    with:
      version: "0.12.1"
      python-version: ${{ matrix.python-version }}
      enable-cache: true
  - run: uv sync --locked --all-extras --dev
  - run: uv run pytest tests/

uv sync --locked がCIで果たす役割

--locked フラグはCI上で特に重要です。uv.lock が最新でない場合、テストが失敗するのではなく即座にエラーで終了します。「pyproject.toml を更新したのにロックファイルを更新し忘れた」というミスを、CIが機械的に検出してくれます。

フラグ 挙動
--locked uv.lock と一致しなければエラー終了(CI推奨)
--frozen uv.lock を絶対に更新しない(より厳格)
無指定 必要に応じて uv.lock を自動更新する(ローカル向け)

9ジョブに膨らんだ実行時間をどう抑えるか

3バージョン × 3OSで9ジョブが並列実行されます。実行時間を抑えるための実践的な方法を紹介します。

strategy:
  fail-fast: false
  matrix:
    python-version: ["3.11", "3.12", "3.13"]
    os: [ubuntu-latest, windows-latest, macos-latest]
  exclude:
    - os: windows-latest
      python-version: "3.11"

さらに有効なのは二段構えの戦略です。PRではUbuntu×最新Pythonのみ実行し、mainブランチへのマージ時にフルmatrixを実行します。これにより開発中のフィードバックループを高速に保てます。

OS別の注意点: Windowsランナーはデフォルトのシェルが PowerShell のため、シェルスクリプトを使う場合は shell: bash を明示してください。また、macOSランナーは分単価がubuntuの約10倍かかるため、必要性を吟味しましょう。


レシピ③:uvキャッシュを制する — 組み込みキャッシュ vs actions/cache

まずは enable-cache: true だけで十分な理由

- uses: astral-sh/setup-uv@v5
  with:
    enable-cache: true
    cache-dependency-glob: "uv.lock"

cache-dependency-globuv.lock を指定することで、ロックファイルが変わったときだけキャッシュが無効化されます。依存が変わっていないのに毎回ダウンロードする無駄を排除できます。

actions/cache で手動管理する場合

より細かく制御したい場合は actions/cache を直接使います。

- uses: actions/cache@v4
  with:
    path: ~/.cache/uv
    key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
    restore-keys: |
      ${{ runner.os }}-uv-

Windowsの場合はキャッシュパスが ~\AppData\Local\uv\cache になる点に注意してください。

uv cache prune --ci — 保存前の刈り込み

- run: uv cache prune --ci

このコマンドはビルド済みwheelを削除し、ソースからビルドしたwheelだけを残します。大規模プロジェクトでは30〜90秒のキャッシュ保存時間を削減できます。GitHubのキャッシュはリポジトリあたり10GBの上限があるため、キャッシュが肥大化してきたら積極的に活用してください。


レシピ④:uv sync + uv run で「仮想環境activate」をCIから消す

従来のCIステップとの比較

Before(従来):

- run: python -m venv .venv
- run: source .venv/bin/activate && pip install -r requirements.txt
- run: source .venv/bin/activate && pytest tests/

After(uvを使った場合):

- run: uv sync --locked --all-extras --dev
- run: uv run pytest tests/

uv run はカレントディレクトリの .venv を自動検出して実行します。source .venv/bin/activate が不要になるだけでなく、Windowsで source コマンドが使えない問題が構造的に解消されます。

依存グループの使い分けで高速化

# Lintジョブ(テスト依存は不要)
- run: uv sync --locked --dev --group lint

# テストジョブ(ドキュメント依存は不要)
- run: uv sync --locked --all-extras --dev

ジョブの目的に合わせて同期する範囲を絞ることで、数十秒の短縮が見込めます。また、uv sync の直後に uv run を呼ぶ場合は uv run --no-sync を使うと二重同期を防げます。


レシピ⑤:uvx でRuff・mypyを「インストールせずに」実行する

最小構成

- run: uvx ruff check .
- run: uvx ruff format --check .
- run: uvx mypy src/

uvx はツールをプロジェクトの依存に追加せずに実行できます。RuffやmypyをdevDependenciesに混ぜなくて済むのが最大のメリットです。

UV_EXCLUDE_NEWER によるキャッシュフレンドリーなuvx

uvxはデフォルトで常に最新版を取りに行くため、キャッシュキーが安定しません。Simon Willison氏が提唱するアプローチとして、UV_EXCLUDE_NEWER による日付固定が有効です。

env:
  UV_EXCLUDE_NEWER: "2026-07-12"

steps:
  - uses: astral-sh/setup-uv@v5
    with:
      enable-cache: true
      cache-suffix: ${{ env.UV_EXCLUDE_NEWER }}
      prune-cache: false
  - run: uvx ruff check .

指定日付以前のバージョンに固定することで、再現性とキャッシュヒット率を同時に確保できます。

uvx を使うべきでないケース

  • チーム全体でツールのバージョンを厳密に揃えたい場合 → dev 依存に追加して uv.lock で管理する
  • ツール自体がプロジェクトの動作に影響する場合(例:コード生成ツール)→ 同上

レシピ⑥:pre-commit と uv を連携させる

公式フック astral-sh/uv-pre-commit の導入

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/uv-pre-commit
    rev: 0.12.1
    hooks:
      - id: uv-lock      # pyproject.toml変更時にuv.lockを自動更新
      - id: uv-export    # uv.lock → requirements.txtを自動同期
      - id: uv-audit     # 依存の脆弱性チェック(2026/05追加)

uv-lock フックはコミット時に pyproject.toml の変更を検知して uv.lock を自動更新します。「依存を追加したのにロックファイルを更新し忘れた」というコミットをゼロにできます。

CI上でpre-commit自体を高速化する

- uses: tox-dev/action-pre-commit-uv@v1

このアクションはpre-commitの環境構築をuvが担うことで、従来と比較して数十秒の短縮が見込めます。

ローカルとCIの二重チェック設計

ローカルはコミット時にフックが走り、CIは pre-commit run --all-files で全ファイルをチェックする二重構造が理想的です。

- run: uvx pre-commit run --all-files

rev の更新は pre-commit autoupdate または Dependabotで自動化することをおすすめします。


レシピ⑦:Dockerマルチステージビルドをuvで最適化する

公式イメージからバイナリだけをコピーする

FROM python:3.12-slim AS builder

# uvバイナリだけをコピー(pip installしない)
COPY --from=ghcr.io/astral-sh/uv:0.12.1 /uv /uvx /bin/

WORKDIR /app

uvを pip install uv でインストールするとレイヤーが膨らみます。公式Distrolessイメージからバイナリをコピーする方法が最も軽量です。latest ではなくバージョンタグを指定して再現性を確保しましょう。

依存インストールとアプリコードを分離してレイヤーキャッシュを効かせる

FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:0.12.1 /uv /uvx /bin/

WORKDIR /app

# ① 依存のみ先にインストール(アプリコードが変わってもキャッシュが効く)
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-dev

# ② アプリコードをコピー後、プロジェクト自体をインストール
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev

--no-install-project が2段階インストールの鍵です。最初のステップで依存パッケージだけをインストールし、アプリコードの変更ではキャッシュが無効化されないようにします。

runtimeステージを最小化する

FROM python:3.12-slim

WORKDIR /app

# builderから.venvだけをコピー(uv本体は含めない)
COPY --from=builder /app/.venv /app/.venv

# activateなしで実行できるようPATHを設定
ENV PATH="/app/.venv/bin:$PATH"

# バイトコンパイル済みファイルを活用(起動高速化)
ENV UV_COMPILE_BYTECODE=1

COPY --from=builder /app /app

CMD ["python", "-m", "myapp"]

最終イメージにuvバイナリを含めないことで、イメージサイズを最小化できます。ENV PATH の設定で source .venv/bin/activate が不要になります。


完成版:コピペで動くワークフロー全文

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
        with:
          version: "0.12.1"
          python-version: "3.12"
          enable-cache: true
          cache-dependency-glob: "uv.lock"
      - run: uvx ruff check .
      - run: uvx ruff format --check .
      - run: uvx pre-commit run --all-files

  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12", "3.13"]
        os: [ubuntu-latest, windows-latest, macos-latest]
        exclude:
          # Windows×3.11は除外してコストを抑える
          - os: windows-latest
            python-version: "3.11"

    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v5
        with:
          version: "0.12.1"
          python-version: ${{ matrix.python-version }}
          enable-cache: true
          cache-dependency-glob: "uv.lock"

      - name: Install dependencies
        run: uv sync --locked --all-extras --dev

      - name: Run tests
        run: uv run pytest tests/ -v

      - name: Type check
        run: uvx mypy src/

      - name: Prune cache before saving
        run: uv cache prune --ci

.pre-commit-config.yaml

repos:
  - repo: https://github.com/astral-sh/uv-pre-commit
    rev: 0.12.1
    hooks:
      - id: uv-lock
      - id: uv-export
        args: [--frozen, --no-dev, -o, requirements.txt]
      - id: uv-audit

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.9.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Dockerfile

FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:0.12.1 /uv /uvx /bin/

WORKDIR /app

ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-dev

COPY . /app

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev

FROM python:3.12-slim

WORKDIR /app

COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app /app

ENV PATH="/app/.venv/bin:$PATH"

CMD ["python", "-m", "myapp"]

導入前後のCI実行時間比較(実測例)

ステップ 導入前(pip) 導入後(uv)
依存解決 42秒 3秒
パッケージインストール 1分48秒 12秒
テスト実行 38秒 35秒
合計(Ubuntu×3.12) 4分8秒 50秒

キャッシュヒット時はさらに短縮され、20〜30秒台も実現可能です。


移行チェックリストとトラブルシューティング

pipからの段階的移行手順(4ステップ)

  1. ローカルでuv導入・uv.lock生成: uv inituv adduv.lock を生成してコミット
  2. CIのインストール部分だけ差し替え: setup-uv に切り替え、uv sync --locked で動作確認
  3. キャッシュ設定を追加: enable-cache: truecache-dependency-glob を設定
  4. Docker・pre-commitへ展開: Dockerfile をマルチステージに改修、pre-commitフックを追加

よくあるエラーと対処

The lockfile is not up-to-date エラー: --locked フラグの意図通りの動作です。ローカルで uv lock を実行してコミットしてください。

Windowsランナーでの失敗: シェルスクリプトを使用している場合は shell: bash を明示してください。PowerShellとBashでは構文が異なります。

キャッシュが肥大化してジョブが遅くなる: ジョブの最後に uv cache prune --ci を追加してください。それでも改善しない場合はGitHub UIからキャッシュを手動削除します。

プライベートリポジトリ依存の認証: 環境変数 UV_EXTRA_INDEX_URL または UV_INDEX_URL にトークンを含めるか、secrets を使って安全に渡してください。

uvを使わない方がよいケース

技術的な公平性のために明示しておきます。次のケースではuvへの移行をあわてて進める必要はありません。

  • 社内で標準化されたPoetryワークフローがあり、チーム全体での移行コストが高い場合
  • 既存のCIが十分に高速で、改善の優先度が低い場合
  • 依存関係が複雑で、uvのロック形式への移行検証工数が大きい場合

まとめ

今日から入れられる3行

まず以下の3行をCIワークフローに追加するだけで、依存インストールの高速化とキャッシュ最適化の恩恵を受けられます。

- uses: astral-sh/setup-uv@v5
  with:
    version: "0.12.1"
    enable-cache: true
- run: uv sync --locked --all-extras --dev

段階的に取り組んで構いません。まずインストール部分を差し替えて速度を実感し、次にmatrix・uvx・pre-commit・Dockerへと展開していくのが現実的なアプローチです。

次に読むとよいもの

「uvはローカルで使っているけどCIはpipのまま」という状態は、実は最もコスト効率が悪い半端な状態です。本記事のレシピを活用して、CI/CDパイプラインも含めた完全なuv体験を手に入れてください。

コメント

0/2000