0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

旧来構成 vs Ruff 構成の比較用ワークフロー

Posted at

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. 前提ファイル(最小例)

requirements-dev.txt
black
flake8
isort
mypy
ruff
pyproject.toml(Black互換・mypy設定/RuffはE,F,I+E203無視)

[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. ワークフロー:旧来スタック(比較用)

.github/workflows/compare-classic.yml
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構成(比較用)

.github/workflows/compare-ruff.yml
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 の結果

image.png

compare-classic の結果

image.png

compare-ruff:19s、compare-classic:21s(同条件・小規模リポ)

今回はシンプルな小規模の構成で行ったが、それでもわずかではあるが時間に差分が出ました。ファイル数が多いほど Ruff の優位が出やすい想定です。(詳細はGithubリポジトリ参照)

また設定面では Ruff は pyproject 集約でメンテが軽く、工具(ツール)数も少ないです。

10. まとめ

  • 個人開発での最小CIは、まず format / lint / import / type を機械化するだけでリターンが大きい。
  • Ruff + mypyは「速い・シンプル・設定集約」で最初の一歩に最適。
  • Flake8プラグイン依存が強いプロジェクトは、旧来構成を残す or 段階移行が無難。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?