目次
背景
ESLintに新しいルールを追加すると、既存の全コードがそのルールに違反していないか検査されます。大規模モノレポでは新しいルールを追加するたびに大量の既存コードを修正する必要があり、影響範囲が大きくなる問題があります。
例えば、新しいルールを1つ追加しただけで、数十〜数百のファイルでエラーが検出されるケースがあります。こうなると新しいルールの導入コストが高くなりすぎて、コード品質の改善が進みにくくなります。
理想的には、修正されたファイルのみを検査して影響範囲を抑えながら、段階的に品質を向上させたいところです。
課題
各アプリのCIワークフローでは、ESLintをプロジェクト全体に対して実行していました。
- name: ESLint
run: yarn lint:app-a
この方式の問題点は以下の通りです。
- 新ルール追加時に既存の全ファイルが検査対象になる
- 既存コードの大量修正が必要になり、PRの影響範囲が大きくなる
- 結果として新ルールの導入をためらうようになる
検討したアプローチ
案A: 設定ファイルを2つに分離して運用
最初に検討した方法は、ESLintの設定ファイルを既存ルール用と新規ルール用の2つに分けて運用することでした。
.eslintrc.cjs ← 既存ルール(全ファイル検査)
.eslintrc.new-rules.cjs ← 新規ルール(変更ファイルのみ検査)
# 既存ルール: プロジェクト全体に実行
- name: ESLint (existing rules)
run: yarn lint:app-a
# 新規ルール: 変更されたファイルにのみ実行
- name: ESLint (new rules)
run: npx eslint -c .eslintrc.new-rules.cjs $CHANGED_FILES
この方式は既存ルールの全体検査を維持しながら、新規ルールのみを段階的に導入できるという利点があります。しかし、実際の運用を考えるといくつかの問題がありました。
- 設定ファイルの管理が複雑になる: 2つの設定ファイルを常に同期する必要がある
- ルールの昇格作業が必要になる: 新規ルールが全コードに適用可能になったら
.eslintrc.new-rules.cjsから.eslintrc.cjsに移す作業が発生する - どのルールがどこにあるか把握しにくい: ルールが2箇所に分散し、管理ポイントが増える
案B: 全ルールを変更ファイルにのみ適用(採用)
次に検討した方法は、設定ファイルは1つのまま検査対象を変更ファイルに限定することでした。
.eslintrc.cjs ← 既存ルール + 新規ルール全て含む(変更ファイルのみ検査)
この方式の懸念は「既存ルールが全ファイルに適用されないと品質が下がるのでは?」という点でした。しかし、以下の理由から実質的な問題がないと判断しました。
- 既存コードはすでに既存ルールをパスしている: 全体検査をしなくても、既存ファイルが突然違反状態になることはない
- 変更されていないファイルは検査する必要がない: 違反が発生するのはコードが変更された時だけ
- 設定ファイルが1つなので管理がシンプル: 新規ルールを追加する時は
.eslintrc.cjsに追加するだけ
結果的に設定のシンプルさと運用のしやすさを優先して案Bを採用しました。
解決: PRで変更されたファイルにのみESLintを実行
GitHub APIを使用してPRの変更ファイル一覧を取得し、変更されたファイルにのみESLintを実行するように修正しました。
- name: Get changed files and run ESLint
uses: actions/github-script@v7
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const filtered = files
.filter(f => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
.filter(f => f.filename.startsWith('apps/app-a/src/'))
.filter(f => /\.(ts|tsx|js|jsx)$/.test(f.filename))
.map(f => f.filename);
if (filtered.length === 0) {
core.info('No matching files to lint.');
return;
}
await exec.exec('npx', [
'eslint',
'-c',
'apps/app-a/.eslintrc.cjs',
...filtered,
]);
ポイント解説
1. GitHub APIで変更ファイルを取得
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
github.paginateを使用してPRの変更ファイル一覧を取得します。ページネーションに対応しているため、変更ファイルが多い場合でも全件取得できます。
2. 対象ファイルのフィルタリング
const filtered = files
.filter((f) => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
.filter((f) => f.filename.startsWith('apps/app-a/src/'))
.filter((f) => /\.(ts|tsx|js|jsx)$/.test(f.filename))
.map((f) => f.filename);
3段階でフィルタリングを行います。
| フィルタ | 目的 |
|---|---|
statusチェック | 追加、修正、リネーム、コピーされたファイルのみ対象(削除されたファイルは除外) |
startsWithチェック | 該当アプリのsrc/配下のファイルのみ対象 |
| 拡張子チェック | .ts、.tsx、.js、.jsxファイルのみ対象 |
3. 変更ファイルがない場合はスキップ
if (filtered.length === 0) {
core.info('No matching files to lint.');
return;
}
対象ファイルがない場合はESLintの実行をスキップします。例えば、設定ファイルやドキュメントのみが変更された場合はESLintを実行しません。
4. 変更ファイルにのみESLintを実行
await exec.exec('npx', [
'eslint',
'-c',
'apps/app-a/.eslintrc.cjs',
...filtered,
]);
yarn lint:app-a(プロジェクト全体)の代わりに、フィルタリングされたファイルのみを引数として渡してESLintを実行します。各アプリに適用する際は、src/パスとESLint設定ファイルのパスを変更するだけです。
動作確認
テスト用のPRで実際の動作を確認しました。
新しいESLintルールを追加した場合:
- 変更したファイル: ESLintエラーが検出された
- 変更していないが問題のあるファイル: ESLintエラーが検出されなかった
これにより、新ルール追加時に既存コードに影響なくCIが通過することを確認できました。
まとめ
| Before | After | |
|---|---|---|
| 検査対象 | プロジェクト全体 | PRで変更されたファイルのみ |
| 新ルール追加時の影響 | 全ファイルの修正が必要 | 変更ファイルのみ対応すればOK |
| 既存コード品質改善 | 一括対応が必要 | 修正時に段階的に改善 |
この方式により、新しいESLintルールを負担なく追加できるようになり、コード品質を段階的に向上させることができるようになりました。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。