CI/CDの悪夢:わずかなフォーマットチェックに5分も待たされる
数万行規模のPythonリポジトリをチームで運用している方なら、この光景に見覚えがあるはずです。誰かがPull Requestを作成するたびに、GitHub Actionsが3〜5分も回り続けます。パイプラインが詰まる原因は、次々と順番に実行されるツール群の待機時間にありました:
- isort:import文の並び順を整理する。
- Black:PEP 8に準拠してインデント、クォート、改行をフォーマットする。
- Flake8:各種プラグイン(flake8-bugbear、flake8-docstringsなど)とともに潜在的なバグを検出する。
- pyupgrade:古いPython構文を最新構文へとアップグレードする。
その結果、CIランナーのコストは跳ね上がり、コードレビューの待機時間は無駄に長引いてしまいます。
さらに最悪なのはローカル環境です。pre-commit hookの実行に十数秒もかかるため、開発者はつい手癖で git commit --no-verify を付けてスキップしてしまいます。その結果、不要な空白などでCIが落ちてしまい、スプリント全体の開発リズムが崩れてしまうのです。
なぜFlake8、Black、isortの3点セットは遅く、衝突ばかりするのか?
プロジェクトが肥大化するにつれて、この従来の組み合わせが限界を迎える理由は、主に3つのボトルネックにあります:
- Pythonランタイムによるオーバーヘッド: 3つのツールはいずれもPythonで書かれています。実行されるたびにOSはインタープリタを起動し、多数のライブラリをRAMにロードしなければならず、この起動オーバーヘッドが大きな遅延を生みます。
- 重複する構文解析(パース): 例えば2,500行の
services/order.pyがあるとします。isortがファイルを読み込んで抽象構文木(AST)を構築し、次にBlackが同じようにASTを再パースし、さらにFlake8が3度目のパースを行います。独立して動作しメモリを共有しないため、膨大なCPUリソースが無駄になります。 - ルールの激しい衝突: Blackはデフォルトで1行88文字で折り返しますが、Flake8は79文字を超えると頑なに
E501エラーを出します。また、Blackがスライスのコロン前に空白を入れると(例:x[1: 2])、Flake8は即座にE203を警告します。結局、こうした不毛な衝突を調停するためだけに、.flake8、setup.cfg、pyproject.tomlといった設定ファイルが乱立することになります。
これまで試した「対症療法」(そしてなぜ失敗したのか)
Ruffに出会う前、私たちのチームでも様々な一時的な最適化を試みていました:
- CI上でジョブを並列実行: isort、Black、Flake8を3台の別々のランナーで並行して走らせました。待ち時間は4分から約2分に短縮されたものの、VMスロットを余計に消費するためCIの請求費用は倍増。さらにローカルのpre-commit hookの遅さは何ひとつ解決しませんでした。
- Flake8プラグインの削減: 重いルールを無効化し、最低限の構文チェックのみに絞り込みました。実行速度は多少改善したものの、代償は大きすぎました。未使用の変数や不要なimport、潜在的なロジックエラーが見逃され、レビューをすり抜けて
mainブランチに混入してしまったのです。
得られた教訓は明確でした。複数の個別ツールを無理に繋ぎ合わせるのではなく、構文解析全体をシステム層の単一エンジンへと統合することこそが根本的な解決策だということです。
根本的な解決策:すべてをRuffへ移行する
RuffはAstral創設者のCharlie Marsh氏によって開発されたオープンソースプロジェクトであり、完全にRustで記述されています。単なるLinterにとどまらず、Flake8、Black、isort、pyupgrade、autoflake、さらには40以上の主要プラグインを完全に代替できるオールインワンのツールキットです。
Ruffは純粋なPython製ツールに比べて10〜100倍高速に動作します。Rayonによるマルチスレッド処理、AST上でのダイレクトなメモリ管理、そしてネイティブなマシンコードへのコンパイルによって、この圧倒的なパフォーマンスを実現しています。
1. プロジェクトへのRuffのインストール
Ruffは pip、poetry、uv 経由で極めてシンプルに導入できます:
# pip経由でインストール
pip install ruff
# またはPoetryでdev dependenciesに追加
poetry add --group dev ruff
2. 設定をpyproject.tomlに集約
.flake8 や .isort.cfg などの個別設定ファイルはすべて削除できます。すべてのルールを pyproject.toml ひとつに集約可能です:
[tool.ruff]
# 行の最大長(Blackの標準に準拠)
line-length = 88
target-version = "py311"
# チェック対象外とするディレクトリ
exclude = [
".git",
".venv",
"__pycache__",
"build",
"dist",
]
[tool.ruff.lint]
# 有効化するルールセット:
# E, W: スタイルエラーおよび警告(pycodestyle/Flake8の代替)
# F: 未使用変数や不正なimport(Pyflakesの代替)
# I: import文のソート(isortの代替)
# B: 潜在的なロジックエラー(flake8-bugbearの代替)
# UP: 新しいPython構文への更新(pyupgradeの代替)
select = ["E", "W", "F", "I", "B", "UP"]
# 無視するルール(ノイズとなるもの、またはFormatter側で処理されるもの)
ignore = ["E501", "B008"]
[tool.ruff.lint.isort]
# 自作プロジェクトモジュールのグループ化
known-first-party = ["my_project_module"]
combine-as-imports = true
[tool.ruff.format]
# Blackと完全互換のコードフォーマット
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
3. チェックの実行と自動修正
Ruffでは2つの主要なCLIコマンドによって役割が明確に分かれています:
ruff check:Linterとして機能(未使用の変数、不要なimport、構文エラーを検出)。ruff format:Formatterとして機能(Blackを100%代替)。
静的解析を行い、安全に修正可能なエラーを自動修正するには以下を実行します:
# 安全に修正可能なエラーをスキャンして自動修正
ruff check . --fix
# コードベース全体を一瞬でフォーマット
ruff format .
4. 超軽量なPre-commit Hookの統合
.pre-commit-config.yaml を更新し、コミット時に即座にエラーを検出できるように設定します:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
# Linterを実行して自動修正
- id: ruff
args: [--fix]
# コードをフォーマット
- id: ruff-format
この構成にすると、pre-commitの実行時間はわずか0.1〜0.2秒程度で完了します。フックが走ったことすら気づかないほどの速さであり、--no-verify でスキップする悪習慣を完全に根絶できます。
5. GitHub ActionsでのCI/CDパイプライン最適化
CI環境でも、これまでの複雑な設定を一掃し、シンプルで無駄のないワークフローへと置き換えられます:
name: Code Quality Check
on: [push, pull_request]
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruff
run: pip install ruff
- name: Run Ruff Linter
run: ruff check --output-format=github .
- name: Run Ruff Formatter
run: ruff format --check .
実際の検証結果として、私たちのチームの約65,000行におよぶバックエンドリポジトリにおいて、GitHub Actions上のLint&Format工程の所要時間は2分20秒からわずか1.4秒へと劇的に短縮されました。LinterとFormatterの衝突もなくなり、コードレビューのたびにシングルクォートかダブルクォートかで不毛な議論を交わす必要も一切なくなったのです。

