コンテンツにスキップ

推奨設定例

Pythonプロジェクト向けの推奨構成例(pyproject.toml・prek・タスクランナー・CI設定)。 設定して実行するところから始める場合ははじめにを参照。

pyproject.toml

pyfltr本体の設定([tool.pyfltr])と、呼び出される各ツール(ruff / mypy / pytest)の設定を1つのpyproject.tomlにまとめた例。

  • preset = "latest": 各時点での推奨ツール構成。詳細はプリセット設定を参照
  • python = true: 言語カテゴリpythonに属する推奨ツール(ruff-format / ruff-check / mypy / pylint / pyright / pytest / uv-sort / arid)を一式有効化する
    • Python系ツール一式は本体依存に同梱されているため、uvx pyfltr単発で利用できる
    • dev依存に固定する場合はuv add --dev "pyfltr[python]"(pip環境ではpip install pyfltr)を使う
  • pylint-args: pylintに追加で渡す引数。--load-plugins=pylint_pydanticはpyfltrが自動で追加するため個別指定不要
    • mypyの--enable-error-code=unused-awaitableもpyfltrが自動で追加するため、mypy-argsへの個別指定は不要
    • Python 3.15でpylintを実行する場合は--jobs=4を指定しない。 並列実行が用いるdill(0.4.1時点)がPython 3.15に対応しておらず、pylintがco_lnotabの参照で異常終了する
  • [tool.pylint."messages control"]: pylintのdisableリストをpyproject.tomlに集約することで、 .pylintrcを別途配置する必要がなくなり設定の所在がpyproject.toml1ファイルにまとまる
    • ruffのDカテゴリがmissing-*-docstring相当を検出するため、 pylint側の同系ルールは無効化しても品質低下は招かない
    • missing-class-docstringはテストクラスに対する儀礼的docstring付与を無効化する
    • missing-module-docstringは__init__.py等でモジュールdocstring要求を緩和する目的で無効化する
  • [tool.pylint."messages control"]のdisableリストへ"duplicate-code"(R0801)を追加し、 重複コードの検出はaridへ委ねる
    • aridはRust実装の重複コードチェッカーで、R0801と同じ目的のチェックを大幅に短い所要時間で実行する。 Python系ツール一式と同じくpyfltr本体依存に同梱されるため、追加の導入手順は不要である
    • 検出範囲はR0801と一致しない。R0801はファイルをまたぐ重複だけを対象とするのに対し、 aridは同一ファイル内の重複も既定の対象に含める
  • [tool.arid]: aridのチェック条件をpyproject.tomlへ集約する。 min-linesは重複とみなす最小の実効行数を指し、 コメント・docstring・import・関数シグネチャを除いた行数で判定する
    • aridの既定値は4であり、そのままでは短い定型的な一致まで報告される。 閾値10はpylintのR0801で運用していた値と同じで、ノイズ抑制と実重複の捕捉を両立する規模として選定した
    • 検出件数はコードベースの内容と閾値で変動する。 閾値を自プロジェクト向けに見直す場合は手元で計測し直す
    • 検出時は実重複か否かを判別する。実重複であれば共通化のリファクタリングを第一候補とし、 意図的な並行実装や共通化すべきでない類似は理由コメントを添えて # arid: disableと# arid: enableで囲んで個別抑制する。 disableリストからの"duplicate-code"の除去や、根拠を示さない閾値変更はしない
  • 実装コードとテストコードを分けず、対象全体をmin-lines = 10でチェックする構成を第一推奨とする。 テストコードの定型的な重複が多くこの構成を維持できない場合は、 テストコードを別系統でチェックする妥協案へ切り替える
  • ruffの per-file-ignores: テストコード(**_test.py)とpackage init(__init__.py)のdocstring要求を除外する実用的な調整

uvx pyfltrでの実行ではpyproject.tomlにpyfltrを記述する必要はなく、[tool.pyfltr]セクションのみで完結する。 dev依存に固定する場合のみ[dependency-groups] devに"pyfltr[python]"を追加する(後置の併記例)。

[tool.uv]
exclude-newer = "1 day"
exclude-newer-package = { project-package = false, release-tool = false }

[tool.pyfltr]
preset = "latest"
python = true
pylint-args = ["--jobs=4"]

[tool.pylint."messages control"]
disable = [
    "broad-exception-caught",
    "duplicate-code",
    "fixme",
    "invalid-name",
    "line-too-long",
    "logging-fstring-interpolation",
    "logging-not-lazy",
    "missing-class-docstring",
    "missing-function-docstring",
    "missing-module-docstring",
    "no-else-return",
    "too-few-public-methods",
    "too-many-arguments",
    "too-many-boolean-expressions",
    "too-many-branches",
    "too-many-instance-attributes",
    "too-many-lines",
    "too-many-locals",
    "too-many-nested-blocks",
    "too-many-positional-arguments",
    "too-many-public-methods",
    "too-many-return-statements",
    "too-many-statements",
]

[tool.arid]
min-lines = 10

[tool.ruff]
# https://docs.astral.sh/ruff/configuration/
line-length = 128

[tool.ruff.lint]
# https://docs.astral.sh/ruff/linter/#rule-selection
select = [
    # pydocstyle
    "D",
    # pycodestyle
    "E",
    # Pyflakes
    "F",
    # pyupgrade
    "UP",
    # flake8-bugbear
    "B",
    # flake8-simplify
    "SIM",
    # flake8-import-conventions
    "ICN",
    # isort
    "I",
]
ignore = [
    "D107", # Missing docstring in `__init__`
    "D415", # First line should end with a period
    "D403", # First word of the first line should be properly capitalized(日本語docstringにそぐわないため)
]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
"**_test.py" = ["D"]
"**/__init__.py" = ["D104"]  # Missing docstring in public package

[tool.mypy]
# https://mypy.readthedocs.io/en/stable/config_file.html
allow_redefinition = true
check_untyped_defs = true
ignore_missing_imports = true
strict_optional = true
strict_equality = true
warn_no_return = true
warn_redundant_casts = true
warn_unused_configs = true
show_error_codes = true

[tool.pytest.ini_options]
# https://docs.pytest.org/en/latest/reference/reference.html#ini-options-ref
addopts = [
    "--showlocals",
    "-p", "no:cacheprovider",
    "--maxfail=5",
    "--durations=30",
    "--durations-min=0.5",
    "--timeout=60",
    # コア数追従(-n auto 等)を採用せず固定値とする(CI環境での逆効果とメモリ消費増を避けるため)
    "-n", "4",
    # 未実行テストを動的分配し、特定ファイルへの偏りによる律速を回避する
    "--dist=worksteal",
]
log_level = "DEBUG"
xfail_strict = true
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "session"
asyncio_default_test_loop_scope = "session"
filterwarnings = [
    "error::ResourceWarning",
    "error::pytest.PytestUnraisableExceptionWarning",
    "error::RuntimeWarning",
]

[tool.mypy]のうちignore_missing_importsとallow_redefinitionはチェックを緩和する指定である。 ignore_missing_imports = trueは解決できないimportのエラーを抑止するため、 型スタブの欠落や誤ったモジュール名を検出しない。 allow_redefinition = trueは注釈のない変数を無関係な型で再定義することを許容するため、 変数の使い回しによる型の取り違えを検出しない。 いずれもチェックは通過するが欠陥は残りうる。厳格さを優先する場合はfalseとする。

asyncio_default_fixture_loop_scope・asyncio_default_test_loop_scopeの"session"は pytest-asyncioの既定(それぞれfixtureスコープ・functionスコープ)より分離を弱める指定である。 上記の例は-n 4でpytest-xdistの並列実行を行うため、workerごとに別プロセスのevent loopとなる。 同一worker内では割り当てられたテストが1つのevent loopを共有し、 loopを閉じるテストや解放されない非同期資源は同じworkerの後続テストへ波及する。 テスト間の独立性を優先する場合は、両設定へ"function"を明示指定する。 asyncio_default_fixture_loop_scopeを未設定にすると、pytest-asyncioが非推奨警告を送出する。 この警告が表示されるかはpytest本体の版で決まり、pytest 8.3.5では表示され、8.4.0以降は表示されない。 pytestがconfig段階の警告収集コンテキストをrecord=Falseで開くためである。 コマンドラインの-W alwaysでは復活せず、環境変数PYTHONWARNINGS=alwaysを与えた場合のみ表示される。 警告の送出自体は版によらず続くため、設定を削除せず値を変更する。

-p no:cacheproviderはpytestのcacheproviderプラグインを無効化する。 このプラグインは--lf・--ff・--nf・--lfnf・--cache-show・--cache-clearの各オプションと、 ini設定cache_dirおよびcache fixtureを提供する。 stepwiseプラグインは--sw・--sw-skip・--sw-resetを提供する。 pytestはcacheproviderを無効化した場合、これに依存するstepwiseも連動して無効化する。 上記の例のようにaddoptsへ書いた場合、両プラグインが提供する上記のオプションは引数として受理されるが効果を持たない。 --lfを指定しても対象は全件実行となり、--cache-showを指定してもキャッシュ内容を表示せずテストを実行する。 pytestは組み込みプラグインを読み込むかどうかをコマンドラインの-p指定だけで決めるため、 addoptsへ書いた無効化は読み込みより後に評価される。 このとき無効化が取り消すのはプラグインの登録だけで、定義済みのオプションは残る。 コマンドラインで-p no:cacheproviderを直接渡した場合は、読み込みより前に評価されるため オプションが定義されず、--lf等を併せて指定するとunrecognized argumentsでエラー終了する。 この差は組み込みプラグインに限る。pytest-xdistのように外部パッケージとして読み込むプラグインは addoptsの評価より後に読み込まれるため、addoptsへ書いた無効化でもオプションが定義されない。 cache fixtureを要求するテストは、addoptsへ書いた場合はAttributeErrorで、 コマンドラインで渡した場合はfixture不存在で、いずれも失敗する。 これらを利用する場合は、この指定を外す。

filterwarningsの3件はテストの資源解放の抜けを検出する指定である。 ファイルやソケットを閉じないままGCされるとResourceWarningが、 awaitされないまま破棄されたコルーチンではRuntimeWarningが送出される。 いずれもGC時の__del__内で送出されるため、エラー化した結果は例外として送出できない。 pytestのunraisableexceptionプラグインがこれをPytestUnraisableExceptionWarningへ包み直す。 テストの成否を決めるのは包み直した後のPytestUnraisableExceptionWarningであり、 ResourceWarning・RuntimeWarningだけをエラー化しても失敗しない。 3件は相互に依存し、いずれを欠いても検出できない。 全警告をエラー化するfilterwarnings = ["error"]でも検出できるが、 外部ライブラリのDeprecationWarningまで失敗へ変えるため、除外エントリの継続的な保守を要する。 上記の3件へ限定すると、エラー化の対象は資源解放と非同期呼び出しの取りこぼしに限られる。 依存ライブラリが同じカテゴリの警告を送出する場合は、その警告が出たテストも失敗する。 導入時はまず全件を実行し、失敗するテストの警告の発生元を確認する。 自プロジェクトの解放の抜けはテスト側の資源解放で是正し、 依存ライブラリ由来のものはignoreエントリで個別に除外する。 pytestはfilterwarningsを後勝ちで適用するため、ignoreエントリは上記の3件より後ろへ置く。

--timeout=60はpytest-timeoutプラグインが必要。 値60は個々のテストが1分以内に完了する前提に基づく。 統合テストや低速なCIランナーでは間欠失敗の原因となるため、 個別のテストで上書きする場合は@pytest.mark.timeout(秒数)を、無効化する場合は@pytest.mark.timeout(0)を指定する。 マーカーは--timeoutより優先される。

-n 4はpytest-xdistプラグインが必要で、4プロセス並列でテストを実行する。 コア数追従(-n auto等)は採用せず固定値とする(CI環境で逆効果になり得ることと、メモリ消費の増大を避けるため)。 --dist=workstealはpytest-xdistのwork-stealingスケジューラを有効化し、 テストファイル間の実行時間差が大きい場合でも特定workerへ偏らせない。 ランナー別の有効条件は次の通り。

  • python-runner = "direct"の指定ではpyfltrのvenv配下のpytestを直接起動するため 本体依存のpytest-timeout・pytest-xdistがそのまま利用できる
  • uvx pyfltrをuv.lock不在のディレクトリで実行する標準シナリオでは、uv指定の前提が満たされず shutil.whichによるdirectフォールバックが発生する
    • この場合もpyfltrの本体venv同梱のpytestが採用されるため本体依存のプラグインが有効
  • uv指定(既定)でcwdのuv.lockにpytestが登録されている場合は、 利用者プロジェクトのvenvでpytestが解決される
    • プロジェクト側にpytest-timeout・pytest-xdistを導入する (uv add --dev pytest-timeout pytest-xdist等)
  • uvx指定(per-tool直接指定でpytest用の独立環境が生成される場合)は、 その環境側へのpytest-timeout・pytest-xdistの導入が別途必要
  • pytest-xdistの並列実行下では、ポート番号・一時ファイル名・グローバル状態の競合に注意する
    • 間欠失敗するテストは並列前提に修正する。 切り分けのため一時的に並列を止める場合は-n 0でaddoptsの並列度を上書きする
    • -p no:xdistはプラグイン自体を無効化するため-n・--distが未定義となる。 上記の例のようにaddoptsへ両者を書いた構成ではunrecognized argumentsでエラー終了する
    • 各workerは自身へ割り当たったテストだけを実行するため、 1つのモジュールが複数workerへまたがるとworkerごとにモジュールスコープのfixtureが実行される
    • 初期化を1回しか許さないプロセス内のグローバル状態は、 モジュールスコープのfixtureで初期化せずsessionスコープへ移す
    • sessionスコープのfixtureもworkerごとに1回実行されるため、 プロセス外の共有資源をworker間で1回だけ初期化する用途には別途排他が必要となる
    • モジュール単位で同一workerへ割り当てたい場合は--dist=loadfile・--dist=loadscopeを選ぶ

テストコードを別系統でチェックする妥協案

テストコードには、同じ組み立てを並べる定型的な重複が実装コードより多く現れる。 第一推奨(対象全体をmin-lines = 10でチェックする構成)を維持できない場合は、 実装コードとテストコードを別系統でチェックし、テストコード側の閾値だけを緩める。

aridはパスごとに閾値を変える設定を持たないため、2回の実行へ分けて実現する。 組込みのaridはarid-excludeでテストコードを対象から外し、 テストコード向けにはカスタムコマンドを登録する。

[tool.pyfltr]
preset = "latest"
python = true
pylint-args = ["--jobs=4"]
# 組込みのaridは実装コードだけを対象とする
arid-exclude = ["tests"]

# テストコード向けのarid。実装コードより緩い閾値で検査する
[tool.pyfltr.custom-commands.arid-tests]
type = "linter"
path = "uv"
args = ["run", "--frozen", "arid", "--project-root", ".", "--min-lines", "15"]
targets = "tests/*.py"
fast = true

[tool.arid]
min-lines = 10

前提条件と制約は次の通り。

  • カスタムコマンドはuv run --frozen aridでaridを起動するため、 プロジェクトのuv環境へpyfltrをdev依存として導入しておく(uv add --dev pyfltr)。 uvx pyfltr単発で実行する構成では、その環境にaridが無く、カスタムコマンドの起動が失敗する
  • arid-excludeとカスタムコマンドのtargetsはテストコードをtests直下へ置く構成を前提とする。 別のディレクトリ構成では両方の値を合わせて変更する
  • 実行を2回へ分けるため、実装コードとテストコードにまたがる重複は検出されなくなる
  • カスタムコマンドへerror-patternは指定しない。 aridのテキスト出力は重複の要約と位置を別々の行へ出力するため、 1行からfile・line・messageを取り出せないためである。 重複の位置はツールの出力そのものから読む
  • 前項の結果として、テストコード側の検出は構造化された診断にならない。 終了コードは重複の検出を反映するためチェックの成否は変わらないが、 --output-format=github-annotationsと--output-format=sarifにはこの重複が現れず、 重複の位置はツール出力のテキストとしてのみ得られる。 出力が長い場合はそのテキストが末尾で切り詰められる。 CIで重複の位置を機械的に扱う必要がある場合は、第一推奨の単一系統の構成を選ぶ
  • テストコード側の閾値15はR0801の閾値10より緩い。 R0801が検出していたテストコードの重複のうち、10行以上15行未満のものは検出されなくなる

typosの許可語設定

プロジェクト固有の許可語がある場合はpyproject.tomlの[tool.typos]セクションに追記する。 typos-cliはpyproject.tomlの[tool.typos]を公式にサポートしているため、_typos.tomlを別ファイルとして管理する必要はない。

[tool.typos.default.extend-words]
teh = "teh"
hte = "hte"

識別子(変数名・関数名)単位で許可したい場合は[tool.typos.default.extend-identifiers]を使う。 詳細はtypos公式ドキュメントを参照。

依存の脆弱性監査の有効化(任意)

依存パッケージの脆弱性をpyfltrの枠組みでまとめて監査したい場合はuv-auditを有効化する。 uv audit(uv 0.11.2以降)がpyproject.tomlを対象にPython依存の既知脆弱性を調べる。 サブコマンド自体は0.10.8で追加されたが、0.10.8・0.10.9は監査対象の件数を報告するのみで 脆弱性を検出しない。 0.10.10以降は検出するが、0.11.1までは検出時も終了コード0を返す。 非0の終了コードを返すのは0.11.2以降である (uv 0.11.2のリリースノートと uv PR #18512)。 pyfltrは終了コード0のツールを出力によらず成功として扱うため、0.11.2未満は実行前チェックで拒否する。 0.10.10以上0.11.1以下でuv-auditを利用していた場合は、uvを0.11.2以降へ更新する。 更新しないまま実行すると解決失敗として扱われ、uv-audit-severityによる警告への格下げもできない。 外部脆弱性データベースへ問い合わせるためネットワーク接続が必須で結果が変動する。 ネットワークが不安定なCIで失敗扱いを避けたい場合はuv-audit-severity = "warning"で警告扱いに切り替える。

[tool.pyfltr]
uv-audit = true
# ネットワーク不調時に失敗ではなく警告として扱う場合
# uv-audit-severity = "warning"

既定引数uv-audit-args = ["audit", "--preview-features", "audit", "--frozen", "--no-progress"]は --frozenを含み、監査時にuv.lockを書き換えない。 --preview-features auditはuv auditが実験的機能である旨の警告を抑止する。

脆弱性監査の結果はコード変更と無関係に外部データベースの更新で変動する。 このためコミット毎やpre-commitではなく、日次・週次の定期実行に向く。 監査ツールのみをまとめて実行する場合は--commands=auditを指定する。 GitHub Actionsではscheduleトリガーの専用ワークフローへ切り出し、 通常のpush/PR用CIへ混在させない構成を推奨する。 SARIF出力(--output-format=sarif)とgithub/codeql-action/upload-sarifを 組み合わせると、GitHub Code Scanningにアラート管理を委ねられる。 同一の脆弱性は1件のアラートに集約され、解消後の定期実行で自動クローズされる。

この構成は公開リポジトリで利用できる。 非公開リポジトリでCode Scanningを使うにはGitHub Code Securityライセンスを要する (公式ドキュメント)。 非公開リポジトリでは、追加費用なく有効化できるDependabot alertsを脆弱性通知の主な手段とする。 そのうえでSARIFはファイルへ出力し、監査ツールの実行有無と検出結果の判別に用いる。 pyfltrはツール実行の失敗をすべて終了コード1へ正規化するため、終了コードだけでは脆弱性の検出とツールの異常を区別できない。 SARIF内にそのツールのruns要素が存在するかで実行がスキップされたかを検出する。 終了コードが非0の場合に限り、resultsが空かどうかで脆弱性の検出とツールの異常を区別する。 脆弱性を検出した場合はワークフローを失敗させて通知する。

JS/TSを併用するプロジェクトでの推奨設定

JS/TSを併用するプロジェクトでは、js-runnerをプロジェクトのパッケージマネージャーに合わせる。 既定のpnpxはツールを都度取得するため、CIで毎回ダウンロードが発生する。 pnpmやnpmなど、プロジェクトで使用しているパッケージマネージャーを指定すると、 package.jsonで管理済みのパッケージを再利用できる。

[tool.pyfltr]
js-runner = "pnpm"

pnpm / npm / yarn / directではtextlint-packagesは無視される(package.json側でインストールする前提のため)。 textlintのプリセットやルールもpackage.jsonのdevDependenciesで管理する。

詳細は設定項目(ツール別)の「js-runner経由で実行するツール」を参照。

日本語Markdownを含むプロジェクトでの推奨設定

日本語のMarkdownを含むプロジェクトではcolloquial-checkを有効化する。 LLMが頻繁に出力する口語的な日本語表現を検出する内蔵linterで、既定は無効(opt-in)。

[tool.pyfltr]
colloquial-check = true

colloquial-check-severityの既定はwarningのため、有効化してもCI・pre-commitは失敗しない。 既存の文書に口語表現が残っている場合は有効化した時点から警告が出るため、一度まとめて是正する。 チェック対象の既定は全ファイル(*)であり、日本語Markdownだけでなくソースコードや設定ファイルに含まれる日本語もチェックする。 辞書に一致する正当な用法を含むファイルはcolloquial-check-excludeで除外し、 対象を特定の拡張子へ限定する場合はcolloquial-check-targetsを指定する。

詳細は設定項目(ツール別)の「colloquial-check」を参照。

.pre-commit-config.yaml

default_language_version:
  python: python3

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-ast
      - id: debug-statements
  - repo: local
    hooks:
      - id: pyfltr
        name: pyfltr
        entry: uvx pyfltr fast
        types_or: [python, markdown, toml, yaml]
        require_serial: true
        language: system

注意: default_language_versionにはプロジェクトが要求するPythonバージョンを指定する。 python3は実行環境が解決するPythonを使う指定であり、版を固定する場合はpython3.12のように書く。 この指定が適用されるのはlanguage: pythonのフックに限る。 上記の例ではpre-commit/pre-commit-hooks側の2件が対象で、language: systemのpyfltrは対象外である。 PEP 695型パラメーター構文(def f[T](): ...)を使用するプロジェクトではPython 3.12以上が必要となる。 指定した版が古いと、check-astやdebug-statementsなどPythonで実装されたフックがSyntaxErrorで失敗する。 メッセージは版により異なり、Python 3.11ではSyntaxError: expected '('、 3.9・3.10ではSyntaxError: invalid syntaxとなる。いずれも終了コード1で終わる。 指定した版をprek・pre-commitのいずれも解決できない場合は、フックの実行前に failed to find interpreterで終了コード3となる。 prekは指定した版を自動取得するため、この形になるのはuv管理のPythonも見つからない場合に限る。

ポイント。

  • uvx pyfltr fast: uvがキャッシュするため2回目以降は実用速度で動作する
    • dev依存にpyfltrを加えている場合はentry: uv run --frozen pyfltr fastに置き換えてもよい
    • uvxが最新版を解決するのは初回だけで、以降はキャッシュ済みの版を再利用する。常に最新版を使う場合はentry: uvx pyfltr@latest fastと指定する
    • キャッシュが更新されて版が上がると、コードを変更していなくても新版のルール追加によりコミットが失敗しうる。版の変化に伴う失敗を避ける場合はdev依存へ固定し、上記の置き換えを採用する
    • uvxはpyproject.tomlの[tool.uv]を読まないため、exclude-newerによる公開直後版の回避はこの起動方法へ適用されない
  • fast: mypy / pylint / pytestなど重いコマンドを除外した高速サブセット
    • 変更ファイルに依らず毎回確かめたいテストがある場合は、任意でpytest-fast-targets = ["*_invariant_test.py"]のように対象のテストファイルを指定すると、そのテストだけをfastで実行できる(設定)
    • formatterがファイルを修正しただけではフックを失敗と判定しない
  • types_or: 必要な種別を列挙する
    • markdownはtextlint / markdownlint、TOML(pyproject.toml)でuv-sort、YAMLはactionlint
    • pre-commit・prekはtypes_orに一致するファイルがコミットに含まれない限りhook自体が起動されない。有効化したツールの対象種別が欠けていると、その種別だけを変更したコミットでチェックが実行されない
    • 有効化するツールを増やした場合はtypes_orの追随要否を確認する。hadolint(Dockerfile)・shellcheck・shfmt(シェル)などはpresetに含まれないため、個別に有効化したときは対応する種別を追加する
  • require_serial: true: pyfltr自身が内部で並列化するため、pre-commit側での多重起動を抑止する

Git indexで.agents/skillsをsymlinkとして管理するプロジェクトでは、 git ls-files --stage .agents/skillsの先頭が120000となる。 Windowsなどのcore.symlinks=false環境ではこのsymlinkはリンク先文字列を内容とする通常ファイルとしてcheckoutされる。 詳細はGit公式資料のcore.symlinksを参照。

同じプロジェクトでend-of-file-fixerを採用する場合は、次のhook単位の除外を追加する。 通常ファイル化したリンク先文字列へ末尾改行が追加され、Git symlinkのblob相当と異なる内容になることを防ぐ。

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: end-of-file-fixer
        exclude: '(^|/)\.agents/skills$'

pre-commitはexcludeをPython正規表現のre.searchで判定する。 prekはRustのfancy-regexを検索形式で判定し、高度な正規表現機能はPythonのreと異なる場合がある。 ここで示した指定は両方で利用でき、リポジトリ直下と入れ子の.agents/skillsへ一致する。 類似名や配下のファイルには一致しない。 詳細はpre-commit公式資料の正規表現と、 prek公式資料のfiles・exclude設定を参照。 実行して確認した結果、同じ通常ファイルを変更しなかったtrailing-whitespaceとmixed-line-endingは除外しない。

pre-commit・prek統合の自動スキップなど双方向の挙動はトラブルシューティングを参照。

pyfltrとpre-commit・prekの呼び出し方法

推奨構成はpyfltrをlocal hookとして登録し、通常実行ではpre-commit・prekのhookも統合する。 呼出し機構、再帰抑制、SKIPの設定はCLIコマンドの統合説明を参照。

タスクランナー

pyfltrを呼び出すタスクランナーの設定例。 言語を問わずuvx pyfltrを利用できる。 pre-commit・prekはいずれもpyfltrの依存に含まれる。 uvx pre-commit・uvx prekで利用可能になる。 以降の例はprekを既定として示すが、prek関連コマンドをpre-commitへ置き換えても同様に動作する。 prekはworkspace rootからサブディレクトリの設定ファイルも再帰探索するため、 例では--config=.pre-commit-config.yamlで対象設定を固定している。 pre-commitへ置き換える場合は、この指定を削除する。

Makefile

uvx方式ではuvがキャッシュするため2回目以降は実用速度で動作する。 最新版を解決するのは初回だけで、以降はキャッシュ済みの版を再利用する。 常に最新版を使う場合はuvx pyfltr@latest ...と指定する。

.PHONY: setup format test

# 開発環境のセットアップ
setup:
    uvx prek --config=.pre-commit-config.yaml install

# フォーマット + 軽量lint(開発時の手動実行用。自動修正あり)
format:
    uvx pyfltr fast

# 全チェック実行(これを通過すればコミット可能)
test:
    uvx pyfltr run

dev依存にpyfltrを固定する運用ではUV_FROZENでlockfileを尊重しつつ、 uv sync後にuv run pyfltr ...を呼び出す形に置き換えることができる。

export UV_FROZEN := 1

help:
    @cat Makefile

# 開発環境のセットアップ
setup:
    uv sync --all-groups --all-extras
    uvx prek --config=.pre-commit-config.yaml install

# 依存パッケージをアップグレードし全テスト実行
update:
    env --unset UV_FROZEN uv sync --upgrade --all-groups --all-extras
    uvx prek --config=.pre-commit-config.yaml autoupdate
    $(MAKE) test

# フォーマット + 軽量lint(開発時の手動実行用。自動修正あり)
format:
    uv run pyfltr fast

# 全チェック実行(これを通過すればコミット可能)
test:
    uv run pyfltr run

.PHONY: help setup update format test

updateターゲットが用いるautoupdateはprek --helpのコマンド一覧に現れないエイリアスである。 正規名はupdateで、どちらの名前を指定しても同じ動作をする(prek 0.4.11で確認)。 例がautoupdateを採用するのは、pre-commitから移行する際の書き換えが実行ファイル名の置換だけで済むためである。

pre-commitのフックを導入済みの作業ツリーでprek installを実行すると、 既存のフックは.git/hooks/pre-commit.legacyへ退避され、以後のコミットではprekのフックと併せて実行される。 退避したフックはpre-commitを導入した環境を参照するため、その環境からpre-commitを外すとコミットのたびに失敗する。 移行時はuvx prek --config=.pre-commit-config.yaml install --forceを1回実行し、退避したフックを除去する(prek 0.5.5で確認)。 --forceは既存のフックを無条件に上書きするため、setupターゲットへは含めない。

uv 0.12.3では、プロジェクト設定と利用者設定にあるmap型のexclude-newer-packageがキー単位で合成される。 同じパッケージ名を両方へ記述した場合は、プロジェクト設定の値が優先される。 これはuv 0.12.3で実行して確認した結果であり、uv公式文書は辞書のキー単位での合成を明記していない。

利用者設定から追加されたキーもuv.lockの[options]へ記録される場合がある。 追跡するuv.lockの生成に必要なキーは、利用者設定へ依存させずプロジェクトの[tool.uv]へ全て列挙する。 上記のproject-packageとrelease-toolは例であるため、各プロジェクトで公開待機の対象外にするパッケージ名へ読み替える。

利用者設定を含まない環境でもロックを再現できることは、空の一時ディレクトリを指定して確認できる。

empty_xdg="$(mktemp -d)"
(
  env --unset UV_FROZEN XDG_CONFIG_HOME="$empty_xdg" uv lock --check
  lock_status=$?
  rmdir "$empty_xdg"
  exit "$lock_status"
)

この手作業による隔離チェックは、任意の時点でロックの再現性を診断するために使う。 後掲のCI例では、matrixの最小Python版でワークフロー実行ごとに同じ条件を継続してチェックする。

mise.toml

言語を問わず利用可能。

[tools]
# 既存のツール指定は残したまま追記する
uv = "latest"

[tasks.setup]
description = "開発環境のセットアップ"
run = [
  "...",
  "uvx prek --config=.pre-commit-config.yaml install",
]

[tasks.format]
description = "フォーマット + 軽量lint(開発時の手動実行用。自動修正あり)"
run = [
  "uvx pyfltr fast",
]

[tasks.test]
description = "全チェック(pyfltr runがprekまたはpre-commitを内部で呼び出す)"
run = [
  "uvx pyfltr run",
]

[tasks.ci]
description = "CI向け全チェック (差分検知で失敗)"
run = [
  "uvx pyfltr ci",
]

ポイント:

  • setup: 開発環境のセットアップ
  • format: pyfltr fast(fix段→formatter段→軽量linter段 + 内部prek・pre-commit統合)を実行する
    • prek-fastとpre-commit-fastが既定値trueのため、有効化しているprekまたはpre-commitのhookもこの1コマンドで実行される
  • test: ローカル開発用
    • pyfltr runが有効なprekまたはpre-commitを内部で呼び出すため、1コマンドで全チェックが完結する
  • ci: CI用
    • pyfltr ciはformatter差分も含めて失敗扱いにする

actionlintのようにインストールrootへ実行ファイル以外を展開するツールの配置設定は、 推奨設定例(非Pythonプロジェクト)の「miseで配置を変えるツール設定」を参照。 インストール実体は同一マシンの全プロジェクトで共有されるため、言語を問わず同じ方針をとる。

.markdownlint-cli2.yaml

markdownlint-cli2が読み込む設定ファイル。$schemaを指定してエディタ補完を有効化する。

$schema: https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.20.0/schema/markdownlint-cli2-config-schema.json
config:
  # 行幅上限は設けず、1文1行運用(writing-standards準拠)で可読性を確保する
  line-length: false
  # コードブロック内でタブ文字を許可(Makefileなど用)
  no-hard-tabs:
    code_blocks: false

.textlintrc.yaml

textlintで技術文書向けの複数プリセットと誤用語チェックを併用する例。 対応するtextlint-packagesの設定例は本ページ後半の「textlint-packagesのカスタマイズ」節を参照。

緩和対象は口語表現・比喩表現・冗長表現を検出するルールを除いたうえで、次のいずれかに当たるものとする。 自動修正が原文の表記を書き換え、修正の往復を生むもの。 他のルールまたは文書側の規範と同一の観点を重複して扱うもの。 専門用語の複合語や技術文の記述量で機械的な閾値超過が頻発するもの。 文書側で採る記法や自然な表現を誤って検出するもの。

言い換えの適否を文脈ごとに判断するルールは、無効化せずseverityをwarningへ下げて残す。 textlintは警告だけの場合に終了コード0で終わり、pyfltrもtextlintをsucceededとして扱ったうえで指摘を表示する。 AIが書いた日本語に出やすい語を検出するpreset-ai-words-jaがこれに当たる。

rules:
  preset-ja-technical-writing:
    # ラベル型見出し("ポイント:", "例:" など)のため、文末句点の強制を無効化する
    ja-no-mixed-period: false
    # 技術文書における自然な助詞連結(「〜かどうかを検討するか」など)が頻出するため無効化する
    no-doubled-joshi: false
    # 全角丸括弧の閉じが改行をまたぐ書き方をfalse positiveとして誤検出するため無効化する
    no-unmatched-pair: false
    # 入れ子括弧の閉じ`))`連続をfalse positiveとして誤検出するため無効化する
    ja-no-successive-word: false
    # 引用文や詳細な技術説明で既定値超過が避けられないため緩和する。
    # 文長の抑制自体は冗長表現の抑止に寄与するため上限は残す
    sentence-length:
      max: 150
    # 専門用語の複合語で機械的な閾値超過が頻発し、回避に言い換えの手作業を要するため無効化する
    max-kanji-continuous-len: false
    # ドキュメントを常体(である調)で統一する方針のため
    no-mix-dearu-desumasu:
      preferInHeader: ""
      preferInBody: "である"
      preferInList: "である"
      strict: false
  preset-jtf-style:
    # no-mix-dearu-desumasuと同一の観点を重複検出するため無効化する
    "1.1.1.本文": false
    # 見出しの書き方は文書側の規範で扱うため無効化する
    "1.1.2.見出し": false
    "1.1.3.箇条書き":
      shouldUsePoint: false # 箇条書きは「。」をつけない
    # 和文の半角ピリオド・カンマ禁止ルール。`.gitignore`等のコード識別子の半角ピリオドを
    # 句点へ自動変換して破壊するため無効化する(lintは通過し検出できないため)
    "1.2.1.句点(。)と読点(、)": false
    "4.1.3.ピリオド(.)、カンマ(,)": false
    # 半角大かっこの全角自動変換がコード識別子・記法ラベルを破壊するため無効化する
    "4.3.2.大かっこ[]": false
    # 和文に隣接する半角丸かっこを全角へ自動変換し、表記の修正往復が頻発するため無効化する
    "4.3.1.丸かっこ()": false
    # 改行折り返し時に全角括弧の前後スペースをfalse positiveとして誤検出するため無効化する
    "3.3.かっこ類と隣接する文字の間のスペースの有無": false
    # コロン終端のラベル記法を多用するため無効化する
    "4.2.7.コロン(:)": false
  ja-no-abusage: true
  preset-ai-words-ja:
    # 言い換えの適否は文脈ごとに書き手が判断するため、検査を止めない警告として扱う
    no-ai-words:
      severity: warning
    # プリセットの説明と異なりpresetの有効化だけで動作し、severityを明示しないとerrorのまま残る
    no-short-topic-comma:
      severity: warning

textlint-packagesのカスタマイズ

追加のtextlintプリセットを使う場合はtextlint-packagesにパッケージ名を列挙する (pnpx / npx起動時に--package / -pとして展開される)。

[tool.pyfltr]
textlint-packages = [
    "textlint-rule-preset-ja-technical-writing",
    "textlint-rule-preset-jtf-style",
    "textlint-rule-ja-no-abusage",
    "textlint-rule-preset-ai-words-ja",
]

共通のコマンドライン引数を追加したい場合は textlint-args を使う。 lint専用のオプション(--format=compact など)は textlint-lint-args に分離する。

[tool.pyfltr]
textlint-args = []
textlint-lint-args = ["--format", "compact"]

旧版のtextlint-args = ["--format", "compact", ...]をそのまま引き継いでもクラッシュしない。 pyfltrはfix段の起動コマンドから--formatペアを自動除去するため。 ただし新規設定ではtextlint-lint-argsに書くことを推奨する。

lint段を高速化するためにtextlint-argsへ--cacheを設定してよい。 pyfltrはfix段の起動コマンドへ--no-cacheを加えるため、自動修正ではtextlintのキャッシュを使わない。

呼び出し方の使い分け

状況に応じてpyfltrの呼び出し方を以下のいずれかから選ぶ。 コンテナ外では「キャッシュ済みの版を使う」(uvx pyfltr ...)、「常に最新版を使う」(uvx pyfltr@latest ...)、 「dev依存にバージョンを固定する」(uv run pyfltr ...)のいずれかをプロジェクト判断で選択する。

状況 呼び出し方 補足
公式Dockerイメージ内(CI推奨構成) pyfltr ... イメージ同梱の本体を直接呼ぶ。uvキャッシュ経由の解決を経由しない
コンテナ外・キャッシュ済みの版を使う uvx pyfltr ... 初回のみ最新を解決し、以降はキャッシュを再利用する。ローカル開発で使用
コンテナ外・常に最新版を使う uvx pyfltr@latest ... 毎回キャッシュをリフレッシュして最新を解決する。軽量CIで使用
コンテナ外・dev依存に固定する uv run pyfltr ... uv add --dev "pyfltr[python]"済みのプロジェクトで使う。UV_FROZENとの併用が有効

CI

GitHub Actionsでpyfltrを実行する構成の例。 リリース時に発行する公式Dockerイメージghcr.io/ak110/pyfltrをcontainer:として利用する形が標準的。 uv / pnpm / mise / hadolint / pinact / shellcheck等が同梱されているため、 セットアップステップを毎回実行する必要がない。 キャッシュディレクトリは/cache配下にまとめて配置済み(uvは/cache/uv、pnpmは/cache/pnpm、miseは/cache/mise)。

Dockerイメージにpyfltr本体を同梱しているため、CI内ではpyfltrを直接呼び出す。 uvx pyfltrを使うとコンテナ起動ごとにuvキャッシュ経由のツール解決が実行され、コンテナ同梱版を使う利点が薄れるため。

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
    container:
      image: ghcr.io/ak110/pyfltr:latest
    defaults:
      run:
        shell: bash
    env:
      UV_PYTHON: ${{ matrix.python-version }}
    steps:
      - uses: actions/checkout@v7

      - name: 利用者設定に依存しないロック検証
        if: ${{ matrix.python-version == '3.11' }}
        run: |
          set -euo pipefail
          config_dir="$(mktemp -d)"
          trap 'rmdir "$config_dir"' EXIT
          # UV_FROZENではlockfileの最新性を検査しないため、この検査だけ解除する。
          env --unset=UV_FROZEN XDG_CONFIG_HOME="$config_dir" uv lock --check

      - name: Cache /cache
        uses: actions/cache@v6
        with:
          path: /cache
          key: pyfltr-cache-${{ runner.os }}-py${{ matrix.python-version }}-${{ github.run_id }}-${{ github.run_attempt }}
          restore-keys: pyfltr-cache-${{ runner.os }}-py${{ matrix.python-version }}-

      # 静的解析は基準とする1つの版だけで実行し、他の版はpytestで実行時互換性だけを検証する。
      - name: Run pyfltr(全検査)
        if: ${{ matrix.python-version == '3.15' }}
        run: pyfltr ci --output-format=github-annotations

      - name: Run pyfltr(pytestのみ)
        if: ${{ matrix.python-version != '3.15' }}
        run: pyfltr ci --commands=pytest --output-format=github-annotations

ポイント。

  • image: ghcr.io/ak110/pyfltr:latest: vX.Y.Zタグも併発行されるため、再現性を重視する場合は固定タグを指定する
    • イメージにはUV_FROZEN=1とpnpm config set minimum-release-age 1440が事前設定されているため、 CIワークフロー側で同じ環境変数や設定を再指定する必要はない
    • 依存同期ではUV_FROZEN=1を維持し、ロックの最新性を調べる追加ステップのuv子プロセスだけで解除する
  • defaults.run.shell: bash: GitHub Actionsのcontainer:既定シェルはshであり、 既存ワークフローで多用されるset -euo pipefail等のbash前提の記述を通すために指定する
  • UV_PYTHON: uv runが必要なCPythonをロックファイルとmatrix値に従って自動取得する
    • actions/setup-pythonは不要
    • 単一バージョンで十分な場合はstrategy.matrixとUV_PYTHONを省ける
  • actions/cache: /cache配下を一括キャッシュする
    • uv / pnpm / miseのキャッシュは内容アドレス指定のため、ロックファイル変更時も追加分がそのまま蓄積される
    • 固定キーは最初の保存後に更新されず、キャッシュの内容が初回保存時点のまま据え置かれる
    • キーへgithub.run_idとgithub.run_attemptを併記し、restore-keysで直近のキャッシュへフォールバックする
    • github.run_idはワークフローの再実行(Re-run)で変化しないため、github.run_idだけでは 再実行時にキーが完全一致し、actions/cacheが新しい内容を保存しない。 同じワークフローの再実行中に変化した/cacheの内容を後続の実行へ引き継ぐため、github.run_attemptを併記する
    • 復元はtar -xfによる展開で、既存ファイルを保持するオプションを指定しない。 コンテナーイメージ側で更新されたツールも、旧キャッシュに同名パスがあれば上書きされる。 上記のキー構成は実行ごとに新しいキーを生成するだけであり、 イメージ同梱ツールの更新をCIで検知する用途には応えない
    • 固定キーからこの構成へ移行する場合、restore-keysは前方一致で判定するため 移行前の固定キー(末尾のハイフンを持たない)には一致しない。 移行後、そのブランチかつ同一pathでの初回実行だけがキャッシュ無しで実行される。 移行前の固定キーをrestore-keysの2行目へ併記すると、この初回のキャッシュ不使用を避けられる。 ただし効果は移行時の1回に限られる。 このprefixのキャッシュが保存された後は1行目が一致するため、併記した行は使われない
    • 実行のたびに新しいエントリが増えるため、リポジトリのキャッシュ上限(既定10GB)と、 7日間アクセスのないエントリが自動削除される仕様を前提に運用する
  • pyfltr ci: イメージ同梱のpyfltrをそのまま使う
    • uvキャッシュを介した解決を毎回経由せず、コンテナビルド時に確定したバージョンで実行できる
    • 特定バージョンに固定したい場合はimage:のタグ(vX.Y.Z)で揃える
  • 基準版だけの全チェック: 静的解析(型チェック・pylint・文書チェックなど)はPythonの版により結果が変わり得るが、 対応版ごとに重複実行すると所要時間が版数に比例して増える。 利用比重が最も高い最新安定版だけでpyfltr ciを実行し、 他の版は--commands=pytestで実行時互換性だけを検証する
    • tokenize・astなどPythonの版で解析結果が変わる標準ライブラリを使うプロジェクトでは、 pytestを全対応版で実行して版差を検出する
    • 対応版を追加・変更する場合は、全チェックを担う版が1つだけになるよう両ステップのifを更新する
    • 全対応版で静的解析まで実行する場合は、pytestのみのステップを削除し、 全チェックステップのifを外す
  • --output-format=github-annotations: ::error file=... / ::warning file=...形式の行を標準出力へ出力する
    • プル要求の該当ファイル行にコメントとして表示される
  • GitHub Actionsのピン留め: preset = "latest"で有効になるpinactが、pyfltr ciの中でuses:のピン留めを確かめる
    • pinact run --checkをworkflowの独立したstepとして置かない。 pyfltr run・pyfltr fastもpush前に同じ内容を確かめるため、ピン留めしていないuses:をpush前に見つけられる
    • 本ページの設定例は読みやすさのためuses:をタグで書いている。 そのまま写すとpinactが失敗するため、写した後にpinact runを実行してSHAへピン留めする

失敗時のログ保存(任意)

CIログのみでは失敗原因を特定できない場合(手元で再現しづらい環境固有の失敗等)に備え、 実行アーカイブを失敗時のジョブ成果物として保存する構成を追加できる。

    env:
      UV_PYTHON: ${{ matrix.python-version }}
      PYFLTR_CACHE_DIR: /tmp/pyfltr-ci-archive
    steps:
      # pyfltrを実行するステップは前掲の構成のまま

      - name: 失敗時に実行アーカイブを保存
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: pyfltr-archive-${{ runner.os }}-py${{ matrix.python-version }}
          path: ${{ env.PYFLTR_CACHE_DIR }}/runs
          retention-days: 7

PYFLTR_CACHE_DIRを明示するのは、既定の保存先(platformdirs.user_cache_dir)が container:ジョブでは$HOME上書きの影響を受け不安定になり得るため。依存キャッシュ (/cache配下)とは別パスを選び、アーカイブが依存キャッシュへ混入しないようにする。 保存先をジョブのenvへ置くのはpyfltrを実行するステップが版ごとに分かれても保存先の指定を1箇所に保つためである。

追加のシステムパッケージが必要な場合

公式Dockerイメージの既定ユーザーは非root(sudo無し)のため、aptでシステムパッケージを追加するには container.optionsで--user rootを指定する。 ただし--user root実行時はcheckoutステップが用意したワークスペースの所有者と、コンテナ実行ユーザー(root)が一致しない。 この不一致によりpre-commit等のgit操作がdubious ownershipで停止する。 git config --global --add safe.directoryで対象ワークスペースを信頼対象に追加して回避する。 PDFを画像化するpdf2imageが必要とするpoppler-utilsの導入とsafe.directory設定を含めた例を次に示す。

    container:
      image: ghcr.io/ak110/pyfltr:latest
      options: --user root
    steps:
      - uses: actions/checkout@v7
      - name: safe.directory の設定
        run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
      - name: システムパッケージ導入
        run: |
          apt-get update
          apt-get install -y --no-install-recommends poppler-utils

Dockerイメージを使わない場合(setup-uv方式)

自前runner制約等でDockerイメージを使用できない場合は、astral-sh/setup-uvでuvを導入し、Node.js / pnpmを別途セットアップする。 UV_FROZEN・PYTHONDEVMODE等の環境変数やpnpm config set minimum-release-age 1440は ワークフロー側で個別指定が必要となる(Dockerイメージでは事前設定済み)。

env:
  PYTHONDEVMODE: "1"
  UV_FROZEN: "1"

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: astral-sh/setup-uv@v10
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true
      - uses: actions/setup-node@v7
        with:
          node-version: "lts/*"
      - uses: pnpm/action-setup@v6
        with:
          version: latest
      - run: pnpm config set minimum-release-age 1440 --global
      - if: ${{ matrix.python-version == '3.15' }}
        run: uvx pyfltr ci --output-format=github-annotations
      - if: ${{ matrix.python-version != '3.15' }}
        run: uvx pyfltr ci --commands=pytest --output-format=github-annotations
      - run: uv cache prune --ci

uv cache prune --ciはプレビルドwheelをキャッシュから除去し、ソースビルドwheelのみ保持する。 GitHub Actionsのような高帯域環境ではキャッシュサイズを削減できる。 帯域が限られる環境では--ciを外し、プレビルドwheelもキャッシュに残す方が再取得を回避できる。

PRの差分ファイルのみを対象にする

PR(プルリクエスト)で変更したファイルだけを対象に実行する場合は--changed-sinceを使う。 ベースブランチとの差分ファイルのみにチェック対象を限定し、大規模リポジトリでの実行時間を短縮できる。

      - uses: actions/checkout@v7
        with:
          # 既定の単一コミット取得ではorigin/mainのリモート追跡refを生成しないため全履歴を取得する
          fetch-depth: 0

      - name: Test with pyfltr (changed files only)
        run: uvx pyfltr ci --changed-since=origin/main --output-format=github-annotations

--changed-since=origin/mainはorigin/mainからの差分ファイルに限定して実行する。 対象はgit diff --name-onlyが返すコミット差分・trackedファイルの作業ツリー差分・staged差分の和集合となり、 untrackedの新規ファイルは対象外。 gitが不在またはrefが解決できない場合は警告を出力して全体実行へフォールバックする。

actions/checkoutの既定はfetch-depth: 1で、リモート追跡refはトリガーとなったref分だけを生成する。 pull_requestイベントではrefs/remotes/pull/配下だけを生成しorigin/mainが存在しないため、 fetch-depth: 0を指定しないと差分の基準refを解決できない。 この場合もジョブは失敗せず全体実行へフォールバックするため、短縮効果が失われた状態に気付きにくい。

GitLab CIでMerge Requestへ表示する

GitLab CIでは--output-format=code-qualityでCode Climate JSON issue形式のサブセットを出力する。 これをartifacts:reports:codequalityとしてアップロードするとMerge Request画面のCode Quality widgetに反映される。 MR diffインライン表示はUltimate tier限定。

stages:
  - lint

pyfltr:
  stage: lint
  image: ghcr.io/astral-sh/uv:python3.13-bookworm
  variables:
    PYTHONDEVMODE: "1"
    UV_FROZEN: "1"
  script:
    - uvx pyfltr ci --output-format=code-quality --output-file=code-quality-report.json
  artifacts:
    when: always
    reports:
      codequality: code-quality-report.json

ポイント。

  • --output-format=code-quality: Code Climate JSON issue形式の配列を出力する
    • --output-fileを指定するとstdoutには従来のtext整形出力が並行して出るため、ジョブログで進捗を確認できる
  • artifacts:reports:codequality: 生成したJSONファイルをGitLabに取り込む
    • Merge Request画面のCode Quality widget(全tier)とMR diffインライン表示(Ultimate tier)に反映される
  • when: always: ジョブが失敗してもアーティファクトを残す指定
    • lintエラーでexit 1したときもレポートを取り込めるようにする
  • image: ghcr.io/astral-sh/uv:python3.13-bookworm: Node.js・pnpm・miseのいずれも含まないため、 Python以外のツールの扱いに注意する
    • textlint・markdownlintは言語カテゴリに属さず、presetを指定すると有効になる
    • js-runnerの既定値pnpxは論理設定値であり、PATH上のpnpmを使うpnpm dlx形式へ解決される
    • Markdownを含むリポジトリではpnpmが見つからず、textlintは終了コード127のfailedとなる (解決失敗ではないため{command}-severity = "warning"で警告へ格下げできる)
    • このイメージはmiseも含まないため、初期状態で有効なlycheeはbin-runnerでの解決に失敗し resolution_failedとなる。解決失敗は{command}-severityによる格下げの対象外のため、 markdownlint・textlintをfalseにしてもジョブはexit 1のままとなる
    • before_scriptでNode.js・pnpm・miseを導入するか、lycheeを含む該当ツールをfalseに設定する

Python以外のプロジェクトでの推奨設定例については推奨設定例(非Pythonプロジェクト)を参照。