1. はじめに
会社の開発では当たり前に CI を使っていて、レビュー前に機械で落とせる赤は落とす文化が根づいています。一方、個人開発だと「まず動くもの」を優先しがちで、重いCIを最初から積むのは腰が重いと感じています。
そこで今回は、“個人でも回せる最小限のCI” を考えるために、静的チェックを中心に
- 旧来スタック:Flake8 + Black + isort + mypy
- モダン統合:Ruff + mypy
を同条件で実測比較できるワークフローを用意し、速度・シンプルさ・設定のしやすさの観点で検証しました。記事の後半には、実際に使える YAML と設定 をそのまま載せます。最小の労力で、品質ゲートと再現性を手に入れるのがゴールです。
2. CIとは?(目的と役割)
CI(Continuous Integration)は単に「テストを自動で回す仕組み」ではなく、変更を“出荷可能”に近づけるための自動検品ラインです。良いCIは次の原則を満たします。
2.1 目的(ゴール)
- 常に動く main:main(またはtrunk)はいつでもデプロイ可能。
- 早いフィードバック:プルリクを出して数分以内に合否が返る。
- 再現性:誰がどこで実行しても同じ結果(依存・手順・環境が固定化)。
- トレーサビリティ:コミット → ジョブ → 成果物が追跡できる(SHAタグ等)。
2.2 CI設計のベストプラクティス
業務で利用するためのCI設計のベストプラクティスは以下のように考えられます。
- Shift-left:重いE2Eより前に静的チェック→ユニットテスト→ビルドの順で早く失敗させる。
- 所要時間の予算:PR CI は 3〜7分 を目安に収める。遅いとCIは使われない。
- ゲートの線引き:
- Gating(必須):format / lint / import / type /(必要なら)依存監査
- Non-gating(参考):ベンチ、長時間の静的解析、実験的チェック(レポートのみ)
- 再現ビルド:同じコミットから**同じ成果物(wheel/イメージ)**を作って保存(アーティファクト化)。
- キャッシュ:actions/setup-python の cache: pip と cache-dependency-path を設定。
- 差分実行:パスフィルタ(例:backend/** 変更時だけPythonジョブ)で無駄を削る。
- フレーク対策:同じコミットで結果が揺れるジョブはゲートにしない or 再試行。
- 可視化:Step SummaryやArtifactに**計測値・レポート(CSV/HTML)**を出して、あとでグラフ化できるように。
逆に、長くて不安定なCIは“誰も待てない”→バイパスされる→形骸化します。スピードと決定性が最優先。
2.3 最低限の“品質ゲート”
静的に落とせる赤を先に落とすのがコスパ最強と考えています。個人開発でもこの4点だけで劇的に安定します。
- Format:機械が整形(差分があれば失敗)
- Lint:未使用・未定義・PEP8違反などの衛生チェック
- Import整列:順序/グルーピングの自動検査
- Type:mypyで型不整合を検知
ここまではコードを実行せずに発見できる欠陥。PR段階で確実に落とすのを最低限行いたいです。
3. 旧来構成とRuffの背景(なぜ2つの流派があるのか)
3.1 旧来構成が生まれた理由(Flake8 / Black / isort / mypy)
過去には役割ごとに最良の専門ツールを組み合わせるのが自然な進化でした。
-
pycodestyle / pyflakes → Flake8
- コード規約(E/W)と軽微なバグ検知(F)を拡張可能なハブとして一体化。
- プラグイン生態系が非常に厚い(bugbear, comprehensions, pep8-naming など)。
-
isort(2013〜)
- import の並び・グルーピングを自動整列。Blackと整合できる profile="black" を提供。
-
Black(2018〜)
- “意見ated”整形で書き方議論を終わらせる。チーム開発の生産性を一気に上げた。
-
mypy(本格普及は2016〜)
- Pythonでも実用的な静的型を。大規模化とIDE補完に寄与。
-
強み
- プラグインの選択肢が圧倒的。コンプラ要件やチームの好みに細かく合わせられる。
-
弱み
- ツールが分かれて設定が分散しがち(.flake8, setup.cfg, pyproject…)。
- 速度:Python実装&多プロセス起動で、リポが大きくなるほど待ち時間が気になる。
その結果、「柔軟だけど遅い/散らばる」をどう解消するか、という問いが残りました。
3.2 Ruffが登場した理由(Rust実装の統合リンタ+フォーマッタ)
Ruff(2022〜)のねらいは、「速い・一体化・設定は1カ所に集約すること」の要です。
-
Rust実装で爆速:大規模リポでも体感が軽い。
-
多くのFlake8プラグイン規則を内蔵:
- 例:F(pyflakes相当)、E/W(pycodestyle相当)、I(isort相当)、B(bugbear相当)、C4(comprehensions相当)、N(pep8-naming相当)、UP(pyupgrade相当)など。
- 1ツールでlint+import整列まで賄える。
-
ruff format:Black互換のフォーマッタ(細部の差分はありつつ、実運用で問題ない範囲)。
-
強み
- 速度とシンプルさ。pyproject.toml に設定を寄せられる。
- “最初の一歩”を非常に始めやすい(ツール数が減る・CIも速い)。
-
注意点
- 完全な互換ではない:ニッチなFlake8プラグインや極端なカスタムは未対応なことがある。
- 型は内包しない:mypy は引き続き外部(=構成は「Ruff + mypy」)。
4. 構造的な違いとチェック観点のマッピング
4.1 ツール構成の違い(アーキテクチャ)
觀点 | 旧来(Flake8/Black/isort/mypy) | Ruff + mypy |
---|---|---|
実装言語 | Python | Rust(lint/format)+Python(mypy) |
役割分担 | 4ツール分離(lint/format/import/type) | 2ツール集約(lint+format+import / type) |
設定 | .flake8 / pyproject / setup.cfgなどに分散 | pyproject.tomlに集約しやすい |
互換性 | プラグイン非常に豊富 | 速い(並列・Rust実装) |
速度 | 並(規模が大きいと遅く感じやすい) | 主要プラグイン由来規則は多く内蔵/未対応分もあり |
自動修正 | Black/isortが主体、Flake8は検知中心 | ruff check --fix が広範に自動修正 |
4.2 「今回のチェック観点」での1対1対応
※本記事の比較では“公平性”のため自動修正(--fix)を無効化し、同じ種類の検査を実施。
チェック観点 | 旧来で使うコマンド | Ruffでの対応 | 主な差分・補足 |
---|---|---|---|
Format(整形) | black --check . | ruff format --check . | Black互換を目指すが細部差分あり。比較検証は“--check”で公平化 |
Lint(衛生検査) | flake8 . | ruff check .(select=["E","F",...]) | ルールコードは大枠互換するが完全一致ではない。Ruffは多くのFlake8プラグイン規則を内包 |
Import整列 | isort --check-only . | ruff check . の規則 I | Ruffはリンタ側でimport整列。旧来は専用ツール(isort) |
Type | mypy . | mypy . | どちらもmypy(Ruffは型チェッカーを内包しない) |
どちらもmypy(Ruffは型チェッカーを内包しない) | E203/W503はBlackと相性悪いため無視 | Ruff側はE203だけ無視で十分(W503はRuff未対応) |
使い分けの指針:
- スピードとシンプルさ重視→ Ruff。
- 特定のFlake8プラグインに強く依存→ 旧来か、Ruffの対応状況を確認してから移行。
5. 比較の設計(旧来 vs Ruff)
公平に比較するためのルール:
- Python:3.11(固定)
- ランナー/キャッシュ条件:完全一致
- 検査:format / lint / import / type(双方とも --fix なし)
- 計測方法:各ステップ開始前後のUNIX秒差を採取 → CSV/JSONをartifactに保存
6. 前提ファイル(最小例)
black
flake8
isort
mypy
ruff
[tool.black]
line-length = 88
[tool.isort]
profile = "black"
[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_ignores = true
warn_redundant_casts = true
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E203"]
7. ワークフロー:旧来スタック(比較用)
name: compare-classic
on: [push, pull_request]
jobs:
classic:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: requirements-dev.txt
- name: Install dev deps
run: |
python -m pip install -U pip
pip install -r requirements-dev.txt
- name: Black (check)
id: t_black
run: |
s=$(date +%s); black --check .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: isort (check-only)
id: t_isort
run: |
s=$(date +%s); isort --check-only .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: Flake8
id: t_flake8
run: |
s=$(date +%s); flake8 .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: mypy
id: t_mypy
run: |
s=$(date +%s); mypy .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: Export timings (summary + artifacts)
run: |
printf "workflow,sha,black,isort,flake8,mypy\n" > timings.csv
printf "classic,%s,%s,%s,%s,%s\n" \
"${GITHUB_SHA}" \
"${{ steps.t_black.outputs.duration }}" \
"${{ steps.t_isort.outputs.duration }}" \
"${{ steps.t_flake8.outputs.duration }}" \
"${{ steps.t_mypy.outputs.duration }}" >> timings.csv
jq -n --arg wf "classic" --arg sha "$GITHUB_SHA" \
--argjson black ${{ steps.t_black.outputs.duration || 0 }} \
--argjson isort ${{ steps.t_isort.outputs.duration || 0 }} \
--argjson flake8 ${{ steps.t_flake8.outputs.duration || 0 }} \
--argjson mypy ${{ steps.t_mypy.outputs.duration || 0 }} \
'{workflow:$wf, sha:$sha, black:$black, isort:$isort, flake8:$flake8, mypy:$mypy}' > timings.json
{
echo "### Classic timings (sec)"
echo "- Black: ${{ steps.t_black.outputs.duration }}"
echo "- isort: ${{ steps.t_isort.outputs.duration }}"
echo "- Flake8: ${{ steps.t_flake8.outputs.duration }}"
echo "- mypy: ${{ steps.t_mypy.outputs.duration }}"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
with:
name: classic-timings-${{ github.sha }}
path: |
timings.csv
timings.json
8. ワークフロー:Ruff構成(比較用)
name: compare-ruff
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: requirements-dev.txt
- name: Install dev deps
run: |
python -m pip install -U pip
pip install -r requirements-dev.txt
- name: Ruff format (check)
id: t_rfmt
run: |
s=$(date +%s); ruff format --check .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: Ruff check (no-fix)
id: t_rlint
run: |
s=$(date +%s); ruff check .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: mypy
id: t_mypy
run: |
s=$(date +%s); mypy .; echo "duration=$(( $(date +%s) - s ))" >> $GITHUB_OUTPUT
- name: Export timings (summary + artifacts)
run: |
printf "workflow,sha,ruff_format,ruff_check,mypy\n" > timings.csv
printf "ruff,%s,%s,%s,%s\n" \
"${GITHUB_SHA}" \
"${{ steps.t_rfmt.outputs.duration }}" \
"${{ steps.t_rlint.outputs.duration }}" \
"${{ steps.t_mypy.outputs.duration }}" >> timings.csv
jq -n --arg wf "ruff" --arg sha "$GITHUB_SHA" \
--argjson rfmt ${{ steps.t_rfmt.outputs.duration || 0 }} \
--argjson rchk ${{ steps.t_rlint.outputs.duration || 0 }} \
--argjson mypy ${{ steps.t_mypy.outputs.duration || 0 }} \
'{workflow:$wf, sha:$sha, ruff_format:$rfmt, ruff_check:$rchk, mypy:$mypy}' > timings.json
{
echo "### Ruff timings (sec)"
echo "- ruff format: ${{ steps.t_rfmt.outputs.duration }}"
echo "- ruff check : ${{ steps.t_rlint.outputs.duration }}"
echo "- mypy : ${{ steps.t_mypy.outputs.duration }}"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
with:
name: ruff-timings-${{ github.sha }}
path: |
timings.csv
timings.json
9. 検証方針と結果
9.1 検証方針
検証の方針は以下の通りとなります。
- 同一コミットで compare-classic と compare-ruff を起動
- その結果の時間を見て比較する
9.2 検証結果
compare-ruff の結果
compare-classic の結果
compare-ruff:19s、compare-classic:21s(同条件・小規模リポ)
今回はシンプルな小規模の構成で行ったが、それでもわずかではあるが時間に差分が出ました。ファイル数が多いほど Ruff の優位が出やすい想定です。(詳細はGithubリポジトリ参照)
また設定面では Ruff は pyproject 集約でメンテが軽く、工具(ツール)数も少ないです。
10. まとめ
- 個人開発での最小CIは、まず format / lint / import / type を機械化するだけでリターンが大きい。
- Ruff + mypyは「速い・シンプル・設定集約」で最初の一歩に最適。
- Flake8プラグイン依存が強いプロジェクトは、旧来構成を残す or 段階移行が無難。