PythonのCI/CD入門|GitHub Actionsでpytestを自動実行する設定とPyPI自動公開

PythonのCI(継続的インテグレーション)とは、GitHubにpushやプルリクエストをするたびに、pytestのテストやコードチェックを自動で実行する仕組みです。GitHub Actionsなら、リポジトリに.github/workflows/ci.ymlを1つ置くだけで始められ、公開リポジトリでは無料で使えます。

この記事では、小さなPythonプロジェクトを例に、手元でテストを通すところから、GitHub ActionsのCI設定、複数のPythonバージョンでのテスト、PyPIへの自動公開(CD)までを順に説明します。バージョン番号は2026年10月15日時点の公式情報で確認したものです。Pythonのコードとテストは手元(Python 3.12.15、pytest 9.1.1、ruff 0.17.0)で実際に実行し、ワークフローのYAMLは構文を検証しています。

CI/CDとは?CIとCDの違い

用語正式名称自動化することPythonでの例
CIContinuous Integration(継続的インテグレーション)変更のたびにテストやチェックを走らせ、壊れていないか確かめるpytest、ruff(コードチェック)
CDContinuous Delivery / Deployment(継続的デリバリー/デプロイ)テストに通ったものを、リリースできる状態にする、または本番へ自動で反映するPyPIへのパッケージ公開、サーバーやコンテナへのデプロイ

CDには2つの意味があり、「いつでもリリースできる状態まで自動化し、最後は人がボタンを押す」のが継続的デリバリー、「本番反映まで全自動」なのが継続的デプロイです。まずはCIから始め、テストが安定してからCDを足すのが一般的な順番です。

手元でテストが通る状態を作る

CIは「手元で実行しているコマンドを、GitHubのサーバーで代わりに実行する」ものです。最初に、手元で同じコマンドが通る状態にしておきます。例として、税込価格を計算する小さなパッケージを用意します。

myapp/
├── pyproject.toml
├── src/
│   └── myapp/
│       ├── __init__.py
│       └── price.py
├── tests/
│   └── test_price.py
└── .github/
    └── workflows/
        └── ci.yml
# src/myapp/price.py
def tax_included(price: int, rate: float = 0.10) -> int:
    """税抜価格から税込価格(1円未満切り捨て)を返す"""
    if price < 0:
        raise ValueError("price must be >= 0")
    return int(price * (1 + rate))
# tests/test_price.py
import pytest

from myapp.price import tax_included


def test_standard_rate():
    assert tax_included(1000) == 1100


def test_reduced_rate():
    assert tax_included(1000, rate=0.08) == 1080


def test_negative_price():
    with pytest.raises(ValueError):
        tax_included(-1)
# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"

[project.optional-dependencies]
dev = ["pytest", "pytest-cov", "ruff"]

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[tool.pytest.ini_options]
testpaths = ["tests"]

開発用のツールをまとめてインストールし、コードチェックとテストを実行します。

pip install -e ".[dev]"
ruff check .
pytest
$ ruff check .
All checks passed!
$ pytest
(中略)
collected 3 items

tests/test_price.py ...                                                  [100%]

============================== 3 passed in 0.03s ===============================

pytestの書き方そのものはpytestの使い方入門で詳しく説明しています。

つまずきやすい点:ModuleNotFoundError

このようにsrcフォルダの下にパッケージを置く構成では、pip install -e .をせずにpytestだけを入れて実行すると、テストがパッケージを見つけられずに止まります。実際に試すと次のようになりました。

E   ModuleNotFoundError: No module named 'myapp'
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!

手元では動くのにCIだけ失敗する原因の多くは、このように「手元の環境にだけ入っているもの」に頼っていることです。CIの設定にも、手元と同じインストール手順を書いておくことが大切です。

GitHub ActionsでCIを設定する

GitHub Actionsでは、リポジトリの.github/workflows/フォルダに置いたYAMLファイルが、決められたタイミングで自動実行されます。次のci.ymlは、mainブランチへのpushとプルリクエストのたびに、ruffとpytestを5つのPythonバージョンで実行します。

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - name: Install
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"
      - name: Lint
        run: ruff check .
      - name: Test
        run: pytest --cov=myapp --cov-report=term-missing

各行の意味は次のとおりです。

設定意味
on実行するタイミング。ここではmainへのpushと、すべてのプルリクエスト
runs-on: ubuntu-latestGitHubが用意するLinuxの仮想マシンで実行する
strategy.matrixpython-versionの値ごとにジョブを並行して実行する
actions/checkoutリポジトリのコードを仮想マシンに取り出す
actions/setup-python指定したバージョンのPythonを用意する。cache: pipでpipのダウンロードをキャッシュする
run手元と同じコマンドを実行する。どれか1つでも失敗するとジョブが失敗(赤い×)になる

アクションのバージョンは、それぞれの公式リポジトリの最新リリースで確認しました(2026年10月15日確認)。actions/checkoutはv7.0.1、actions/setup-pythonはv7.0.0が最新です。GitHub DocsのPythonのビルドとテストのページはcheckout@v6、setup-python@v5の例のままですが、書き方は同じです。setup-pythonのREADMEでは、Pythonのバージョンはpython-versionで明示的に指定することが推奨されています。cacheの対象にできるのはpip、pipenv、poetryで、初期設定では無効です。

YAMLの書き方に慣れていない場合はYAMLとは?基本の書き方もあわせてどうぞ。

テストするPythonのバージョンの選び方

Python公式のバージョン一覧によると、2026年10月15日時点でサポート中なのは3.11〜3.15です。3.15は2026年10月9日に正式版が出たばかりで、3.10は2026年10月1日に、3.9は2025年10月31日にサポートが終わっています。

バージョン状態サポート終了予定
3.15バグ修正(2026年10月9日リリース)2031年10月
3.14バグ修正2030年10月
3.13セキュリティ修正のみ2029年10月
3.12セキュリティ修正のみ2028年10月
3.11セキュリティ修正のみ2027年10月
3.10サポート終了2026年10月1日

GitHub Actionsで使うPythonのビルドを配布しているactions/python-versionsにも、3.15.0が2026年10月10日に追加されています。ライブラリとして公開するならサポート中の全バージョン、社内のアプリなら本番と同じバージョン1つ、というように目的で選びます。マトリックスを増やすほど実行時間(無料枠の消費)も増えます。

カバレッジ(テストが通った行の割合)を出す

上のワークフローの最後の行のように、pytest-covを使うとテストで実行された行の割合が分かります。手元で実行した結果です。

pytest --cov=myapp --cov-report=term-missing
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
src/myapp/__init__.py       0      0   100%
src/myapp/price.py          4      0   100%
-----------------------------------------------------
TOTAL                       4      0   100%
============================== 3 passed in 0.13s ===============================

Missingの列に、テストで一度も通らなかった行番号が表示されます。カバレッジは100%を目指すものというより、「この分岐はテストしていなかった」と気づくための道具として使うと効果的です。

CD:タグを付けたらPyPIへ自動公開する

ライブラリを作っている場合のCDの代表例が、PyPIへの自動公開です。PyPIは現在、APIトークンをGitHubに保存する代わりに、GitHub ActionsとPyPIを直接ひも付ける「Trusted Publishing」を案内しています。PyPI側で公開元のリポジトリとワークフローを登録したうえで、次のように書きます。

# .github/workflows/release.yml
name: Release

on:
  push:
    tags: ["v*"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.14"
      - run: |
          python -m pip install build
          python -m build
      - uses: actions/upload-artifact@v7
        with:
          name: dist
          path: dist/

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

v1.0.0のようなタグをpushすると、buildジョブでパッケージを作り、publishジョブでPyPIへアップロードします。PyPIのドキュメントでは、id-token: writeの権限はTrusted Publishingに必須で、ワークフロー全体ではなく公開するジョブだけに付けることが強く推奨されています。pypa/gh-action-pypi-publishのREADMEでも、ビルドと公開のジョブを分けることが勧められています。upload-artifactはv7.0.2、download-artifactはv8.0.2が最新です(2026年10月15日確認)。

Webアプリの場合のCDは、テストに通ったあとにコンテナイメージを作ってクラウドへデプロイする形になります。デプロイ先ごとに手順が大きく違うため、使うサービスの公式ドキュメントにあるGitHub Actionsの手順に従うのが確実です。パスワードやAPIキーは、ワークフローのファイルに直接書かず、リポジトリの「Secrets」に登録して${{ secrets.名前 }}で参照します。

GitHub Actionsの料金(無料枠)

GitHubのGitHub Actionsの課金のドキュメントによると、公開リポジトリで標準のGitHubホストランナーを使う場合は無料です。非公開リポジトリでは、プランごとに毎月の無料枠があります(2026年10月15日確認)。

プラン実行時間(毎月)成果物の保存容量
GitHub Free2,000分500MB
GitHub Pro3,000分1GB
GitHub Team3,000分2GB
GitHub Enterprise Cloud50,000分50GB

マトリックスで5バージョンを試すと、1回のpushで5ジョブ分の時間を使います。非公開リポジトリで無料枠に収めたい場合は、プルリクエストでは全バージョン、普段のpushでは1バージョンだけ、のように分けると節約できます。

CIを続けるためのコツ

  • 手元と同じコマンドを使う:CIに書くのは、手元で毎日実行しているコマンドと同じものにします。CIでしか動かない手順は、失敗したときに原因を追いにくくなります。
  • 速く終わる順に並べる:数秒で終わるruffを先に、時間のかかるテストを後にすると、単純なミスで早く失敗して待ち時間が減ります。
  • 依存パッケージのバージョンを固定する:バージョンを固定しないと、ある日突然、新しいライブラリのせいでCIが落ちることがあります。requirements.txtの書き方も参考にしてください。
  • 失敗を放置しない:赤い×が出たままの状態が続くと、誰もCIの結果を見なくなります。失敗したら最優先で直す、を習慣にします。

よくある質問

Q. PythonのCIは何から始めればいいですか?

まず手元でpytestのテストが通る状態を作り、そのコマンドをGitHub Actionsのワークフロー(.github/workflows/ci.yml)に書くところから始めます。コードチェック、複数バージョンでのテスト、デプロイの自動化は、テストが安定してから順に足していけば十分です。

Q. GitHub Actionsは無料で使えますか?

公開リポジトリで標準のGitHubホストランナーを使う場合は無料です。非公開リポジトリでは、GitHub Freeなら毎月2,000分などプランごとの無料枠があり、超えた分は課金されます(2026年10月15日確認)。

Q. 手元では通るテストがCIでだけ失敗するのはなぜですか?

手元の環境にだけ入っているパッケージや設定に頼っていることが多い原因です。たとえばsrcフォルダ構成でpip install -e .をしていないと、CIではModuleNotFoundErrorになります。CIの設定に手元と同じインストール手順を書き、依存パッケージのバージョンを固定すると防げます。

Q. どのPythonバージョンでテストすればいいですか?

2026年10月15日時点でサポート中なのは3.11〜3.15です。ライブラリとして公開するならサポート中の全バージョン、自社のアプリなら本番環境と同じバージョンでテストするのが基本です。

あわせて読みたい

学習の順序はPython学習のロードマップ、標準で使える機能の一覧はPython標準ライブラリのモジュール一覧にまとめています。

開発環境まわりの全体像はGitと開発環境の基礎で、環境構築・エディタ・Gitを3つに分けて整理しています。

インフラまわりの全体像はインフラ・ネットワークの基礎で、クラウド・コンテナ・ネットワークを3つの軸で整理しています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら