uvをGitHub Actionsで使う方法|uv.lockを使ったテスト環境の再現

uvとGitHub Actionsでテスト環境をそろえる

uvとGitHub Actionsでテスト環境をそろえる

uvで管理しているPythonプロジェクトをGitHub Actionsでテストするなら、pyproject.tomluv.lockをGitで管理し、CIではuv sync --lockedからuv run --locked pytestへ進む構成を出発点にできます。

CI(変更のたびにテストなどを自動実行する仕組み)では、手元にしかない依存関係を使えません。環境の作り方をファイルへ残し、設定とlockfileがずれたときに止めることで、ローカルで通った理由をCIでも確認しやすくします。

この記事は、uvのプロジェクト管理を使い、tests/にpytestのテストがある単一のPythonプロジェクトを対象とします。設定例とローカルでの正常・失敗両方の検証を示します。GitHub Actions上の実行とキャッシュ速度の測定は行っていません。

CIへ渡すのは.venvではなく、環境を作るためのファイル

Gitで管理した設定とlockfileを使い、CIで環境を作ってテストする

*Gitで管理した設定とlockfileを使い、CIで環境を作ってテストする。独自に整理した概念図です。*

uvのプロジェクトには、役割の異なるファイルがあります。

ファイル・ディレクトリ 役割 Gitで管理するか
pyproject.toml プロジェクト情報と依存関係の条件 管理する
uv.lock 解決済みの依存関係を記録 管理する
.python-version ローカルで使うPythonの指定 プロジェクト方針として管理する
tests/ 実行するテスト 管理する
.venv/ その環境にインストールされた実体 管理しない

lockfile(解決したパッケージのバージョンなどを記録したファイル)があると、CI側で毎回すべてを新しい条件から選び直すことを避けられます。.venvはOSやPythonの配置に依存するため、Gitへ含めず、CI上で作り直します。.gitignore.venv/が登録されていることも確認してください。uvのプロジェクト構成

既存のuvプロジェクトで、pytestを開発用依存にまだ登録していない場合は、プロジェクトのルートで次のように追加します。ここでは検証例に合わせてpytest 8系を指定しています。既にテスト環境があるプロジェクトでは、その対応範囲を維持してください。

# テストツールを個人の環境だけでなく、プロジェクトの依存に記録する
uv add --dev "pytest>=8,<9"

# ローカルのPython選択も記録する
uv python pin 3.12

# CIと同じ条件で同期とテストを先に確認する
uv sync --locked
uv run --locked pytest -q

uv add --devpyproject.tomldevグループへ依存関係を追加します。uvでは通常このグループが同期対象になるため、pytestをここへ登録しておけば、後述の基本例で導入できます。uvの依存関係管理

新規プロジェクトでtests/がまだなければ、先に実際の処理を確認するテストを用意します。pytestの導入だけでテストケースが生成されるわけではありません。また、requires-pythonが3.12を許可することも確認してください。uvへの移行自体がまだの場合は、uvとpip・venvの違いと移行手順から準備できます。

GitHub Actionsの設定例

プロジェクトがリポジトリのルートにある場合、次を.github/workflows/test.ymlとして保存します。GitHub Actionsのworkflowは、自動処理の開始条件と手順をYAMLで記述するファイルです。

name: Python tests

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      # 取得するActionの実装を固定し、意図しない差し替えを避ける
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Set up uv and Python selection
        uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
        with:
          # ローカル検証と同じuv本体を使う
          version: "0.11.19"
          python-version: "3.12"
          # 初回はキャッシュなしで成立することを確認する
          enable-cache: false

      - name: Install Python
        run: uv python install 3.12

      - name: Sync dependencies
        run: uv sync --locked

      - name: Run tests
        run: uv run --locked pytest -q

mainが既定ブランチでない場合は、branchesを実際の名前へ変更します。permissions: contents: readはリポジトリ内容の読み取り権限です。この例はテスト用で、成果物の配布やリポジトリへの書き込みを行いません。構文と実行条件はGitHub Actionsのworkflow構文を参照してください。

setup-uvはuvを導入し、後続ステップで呼び出せる状態にします。その後、Pythonを用意し、依存関係を同期してからテストを実行します。公式にもこの順序の導入方法が示されています。uvのGitHub Actions連携

Action、uv、Pythonは別々に固定する

setup-uvのバージョンと、インストールされるuv本体のバージョンは別です。長い英数字のSHA(Gitのコミットを識別する値)だけを固定しても、uv本体まで同じになるとは限りません。

指定 固定・選択する対象 残る変動
uses: ...@完全なSHA Actionの実装 そのActionが取得するツールは別設定
version: "0.11.19" uv本体 OSやPythonまでは固定しない
python-version: "3.12" Python 3.12系の選択 パッチバージョンは変わり得る
runs-on: ubuntu-24.04 Ubuntu 24.04系の実行環境 イメージ内部の更新は続く
uv.lock 解決済みの依存関係 OSやPython条件によって導入対象が変わり得る

完全なSHAでのAction固定はGitHubの安全な利用に関する案内に沿っています。例のSHAは2026年9月11日に公式リポジトリのタグと照合しました。uv 0.11.19は本記事の検証版であり、最新版を意味しません。setup-uvの入力設定actions/checkout

.python-versionに3.12を書いていても、CI側で別のPythonを明示すれば条件が変わります。基本例は両方を3.12へそろえています。パッチ版もそろえる必要があるプロジェクトでは、ローカルとCIに同じ完全なバージョンを指定してください。uvのPythonバージョン選択

–lockedと–frozenは、どちらも「同じ固定」ではない

依存追加後にlockを更新しない場合、lockedは停止しfrozenは整合性確認を省略する

*依存追加後にlockを更新しない場合、lockedは停止しfrozenは整合性確認を省略する。独自に整理した概念図です。*

pyproject.tomlへ依存を追加したのにuv.lockを更新し忘れた場合、CIはどう振る舞うべきでしょうか。通常のテストでは、この不整合を検出できる--lockedが使いやすい選択です。

指定 lockfileの扱い 向く場面 注意点
指定なし 必要なら自動で更新する ローカルで依存関係を整える作業 CI内で更新が起きると、コミット済みファイルの検証にならない場合がある
--locked 現在の設定と整合するか確認し、更新が必要ならエラー lock更新漏れを止めたいCI エラー時は手元でlockを更新して変更を確認する
--frozen 既存lockfileを使い、最新性の確認を省く 事前の整合性検証を別工程で保証できる場合 設定に追加した依存が反映されていなくても見逃す可能性がある

ここでいう整合性は、pyproject.tomlの文字列が一文字でも変われば失敗するという意味ではありません。uvが現在の条件を満たすlockfileとして使えるかを判定します。新しいパッケージ版が公開されたことだけで、lockfileが古いと判定されるわけでもありません。uvのlockと同期の仕様

uv runも既定では必要に応じてlockと環境を更新するため、基本例ではテスト実行側にも--lockedを付けています。uv lockで更新する工程と、--lockedで検証する工程を分ける方が、依存変更をレビューしやすくなります。

ローカルで更新漏れを再現した結果

本記事では、pytestをdevに登録した小さな検証用プロジェクトで、依存パッケージのバージョン比較を確認する3ケースのテストを実行しました。uv 0.11.19、Python 3.12.3、Linux上での結果です。

検証した操作 実際の結果
uv lock後にuv sync --lockedとpytestを実行 テスト3件成功
検証用の.venvを削除して再同期・再テスト テスト3件成功。lockfileは変更されない
pyproject.tomlidna==3.10を追加し、lockは更新せずuv sync --locked 更新が必要というエラーで終了。lockfileは変更されない
同じ不整合状態でuv sync --frozen 同期は成功したが、追加したidnaは導入されなかった
uv lockで更新して再同期・再テスト テスト3件成功

この例では、--frozenの同期成功は「現在の設定どおりの依存がすべて入った」ことを示しませんでした。CIで検出したいのがlockの更新漏れなら、成功・失敗の意味が目的に合う指定を選ぶ必要があります。

キャッシュは、依存関係を決めるファイルの代わりにならない

基本例が成立してから、パッケージ取得やビルドの繰り返しを減らす目的でキャッシュを追加できます。キャッシュは、既に取得・作成した成果物を再利用する仕組みです。uvのキャッシュ

setup-uvにはキャッシュ機能があるため、先ほどのwith内を次のように変更します。

with:
  version: "0.11.19"
  python-version: "3.12"
  enable-cache: true
  # ルートにある単一プロジェクトの依存ファイルをキーへ反映する
  cache-dependency-glob: |
    pyproject.toml
    uv.lock

これはsetup-uvステップのwithを置き換える断片で、単独のworkflowではありません。複数プロジェクトを含む場合は、キャッシュキーに含めるファイルの範囲を広げる必要があります。setup-uvのキャッシュ設定

キャッシュが見つからない初回でも、必要なパッケージを取得してテストできる構成にします。また、キャッシュが見つかった場合もuv sync --lockedは実行してください。キャッシュ内にファイルがあることと、今回のプロジェクト環境へ必要な依存が入っていることは別だからです。

本記事ではキャッシュ復元や高速化率を測定していません。効果を見る場合は、キャッシュの有無だけでなく、依存の同期とジョブ全体の所要時間を分けて比較します。

CIだけ失敗するときの確認箇所

失敗したステップを基に調べると、キャッシュ削除や再実行を繰り返す前に確認する場所を選べます。

症状 確認するもの 対処の方向
pyproject.tomlが見つからない コマンドの実行ディレクトリ プロジェクトのルートで実行する
lockfileの更新が必要と表示される pyprojectとuv.lockの差分 手元でlock更新・テスト後、両方をcommitする
pytestが見つからない dev依存、--no-devなどの指定 テスト用グループを同期し、uv runで実行する
Pythonの条件を満たせない requires-pythonとCIの指定 サポート範囲と実行版をそろえる
テストが収集されない tests/の配置、テスト名、実行場所 テストの検出条件を確認する
インストール中にビルドが失敗する OS、外部ライブラリ、対応パッケージ lockfile以外に必要なOS側の依存を確認する

プロジェクトがサブディレクトリにある場合

たとえばbackend/pyproject.tomlを使うなら、runステップの実行場所もbackendにします。次はjobs.test内に追加する断片です。

# uvが対象プロジェクトを正しく見つけられる場所で実行する
defaults:
  run:
    working-directory: backend

この設定はrunステップに適用され、usesで呼ぶActionの入力までは自動で変えません。キャッシュを有効にする場合は、cache-dependency-globbackend/pyproject.tomlbackend/uv.lockへ変更します。GitHub Actionsの実行ディレクトリ設定

複数Python版を試す場合

Python 3.12と3.13の両方をサポートするなら、matrix(設定の組み合わせごとにジョブを作る機能)で分けられます。ただし、requires-pythonと依存パッケージが両方に対応していることが前提です。 本記事のローカル検証用プロジェクトは3.12系に限定しており、このmatrix例は別の対応範囲を持つプロジェクト向けです。

jobs.testへ次を追加します。

strategy:
  matrix:
    python-version: ["3.12", "3.13"]

そのうえで、setup-uvpython-version\({{ matrix.python-version }}へ、Python導入コマンドをuv python install \){{ matrix.python-version }}へ変更します。固定の3.12を片方に残すと、試したいPython版との対応が崩れます。uvの複数Python版でのCI

依存更新とCIでの検証を分けて運用する

依存を変える作業では、ローカルで設定とlockfileを更新し、テストしてから差分をレビューします。CIではコミットされた内容を--lockedで検証する、という役割分担です。

lock更新漏れを直す場合は、プロジェクトのルートで次を実行します。

# 更新はレビューできる手元で行い、CIでの自動更新に頼らない
uv lock
uv sync --locked
uv run --locked pytest -q
git diff -- pyproject.toml uv.lock

uv lockは既存の解決済みバージョンを可能な範囲で維持します。全依存を最新版へ更新する作業とは分けて扱ってください。uvの依存バージョン更新

本記事で確認したのは、ローカルでの環境再構成、テスト実行、lock不整合時の動作、ActionのSHAと公式タグの対応です。GitHub上のrunner(ジョブを実行するマシン)、イベント発火、Action動作、キャッシュ復元は未検証です。設定を導入した後は、実際のActionsログで使用したPython・uvと各ステップの成否を確認してください。

GitHub Actionsを有効にして実行する前には、対象リポジトリとrunnerの利用条件・費用も確認します。この記事の作成では既存workflowを変更せず、GitHub側の実行も行っていません。GitHub Actionsの課金に関する公式案内

コメント

タイトルとURLをコピーしました