ESLint エラーを見逃さない GitHub Actions 連携方法

GitHub Actions を使って ESLint のチェックを自動化することで、コードの品質を保ちながら開発効率を大幅に向上させることができます。手動でのチェック作業から解放され、プルリクエスト時に自動的にコード品質を検証する仕組みを構築できるんですね。
この記事では、GitHub Actions と ESLint を連携させる基本的な設定方法から、エラーを見逃さないための応用的な設定まで、実際のコード例とエラーメッセージを交えて詳しく解説していきます。
背景
手動チェックの限界とヒューマンエラー
現代の JavaScript や TypeScript プロジェクトでは、コードの品質を保つために ESLint による静的解析が欠かせません。しかし、開発者が手動でチェックを行う場合、以下のような問題が発生しがちです:
手動チェックで発生する典型的な問題
# | 問題点 | 発生頻度 | 影響度 |
---|---|---|---|
1 | チェック忘れ | 高 | 中 |
2 | ローカル環境の差異 | 中 | 高 |
3 | 設定ファイルの不一致 | 中 | 高 |
4 | 時間的制約による省略 | 高 | 中 |
実際に、手動チェックを忘れた場合に発生する典型的なエラーがこちらです:
javascript// 手動チェックを忘れがちなコード例
const userData = {
name: '田中太郎',
email: 'tanaka@example.com',
// ← カンマ抜けエラー
};
function getUserData() {
return userData;
// ← セミコロン抜けエラー
}
このようなコードをプッシュしてしまうと、以下のような ESLint エラーが発生します:
goerror Parsing error: Unexpected token, expected "," at line 4:5
error Missing semicolon semi
プルリクエスト時のコード品質管理の課題
チーム開発では、プルリクエスト時にコード品質をチェックする必要がありますが、手動での管理には限界があります:
プルリクエスト時の品質管理課題
typescript// レビュー時に見落としがちな問題コード
interface UserData {
name: string;
email: string;
age?: number;
}
// 未使用の変数(ESLint で検出可能)
const unusedVariable = 'これは使われていません';
// 型安全性の問題
function processUser(user: UserData): void {
console.log(user.name);
// age が undefined の可能性を考慮していない
if (user.age > 18) {
console.log('成人です');
}
}
このコードは一見問題なさそうですが、ESLint を実行すると以下のエラーが検出されます:
vbneterror 'unusedVariable' is assigned a value but never used @typescript-eslint/no-unused-vars
error Object is possibly 'undefined' @typescript-eslint/strict-boolean-expressions
課題
ESLint エラーの見逃しが発生する原因
ESLint エラーの見逃しは、主に以下の原因で発生します:
1. 環境依存による設定差異
開発者のローカル環境と本番環境で ESLint の設定や Node.js のバージョンが異なる場合、エラーの検出結果に差が生じます:
json// package.json の設定例
{
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"eslint": "^8.50.0",
"@typescript-eslint/eslint-plugin": "^6.7.0"
}
}
バージョン違いにより、以下のようなエラーが環境によって検出されたりされなかったりします:
goerror Parsing error: This syntax requires an explicit type parameter list
error '@typescript-eslint/eslint-plugin' version mismatch
2. 設定ファイルの読み込み順序問題
ESLint の設定ファイルが複数存在する場合、読み込み順序により異なる結果が生じることがあります:
javascript// .eslintrc.js が存在する場合
module.exports = {
extends: ['@next/next/core-web-vitals'],
rules: {
'no-unused-vars': 'error'
}
};
// package.json にも eslintConfig が存在する場合
{
"eslintConfig": {
"extends": ["react-app"],
"rules": {
"no-unused-vars": "warn" // 設定が競合
}
}
}
GitHub Actions 設定時の落とし穴
GitHub Actions で ESLint を設定する際によく発生する問題を見ていきましょう:
1. 依存関係のインストール漏れ
yaml# 問題のあるワークフロー例
name: ESLint Check
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 依存関係のインストールを忘れがち
- name: Run ESLint
run: npx eslint . --ext .js,.jsx,.ts,.tsx
この設定では以下のエラーが発生します:
arduinoError: Cannot find module 'eslint'
Error: ENOENT: no such file or directory, open 'package.json'
2. Node.js バージョンの指定漏れ
yaml# Node.js バージョンを指定していない例
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: yarn install
この場合、GitHub Actions のデフォルト Node.js バージョンが使用され、プロジェクトの要求バージョンと異なる可能性があります:
javascriptError: The engine "node" is incompatible with this module
Error: Expected version ">=18.0.0". Got "16.20.0"
解決策
基本的な GitHub Actions ワークフロー設定
確実に ESLint エラーを検出するための基本的なワークフロー設定を作成しましょう:
yaml# .github/workflows/eslint.yml
name: ESLint Code Quality Check
# プルリクエストとプッシュ時に実行
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
eslint:
name: ESLint Check
runs-on: ubuntu-latest
steps:
# リポジトリのチェックアウト
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 全履歴を取得してdiffチェック可能にする
# Node.js のセットアップ(バージョン固定)
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.17.0' # プロジェクトに合わせて指定
cache: 'yarn' # Yarnのキャッシュを有効化
# 依存関係のインストール
- name: Install dependencies
run: yarn install --frozen-lockfile
# ESLint の実行
- name: Run ESLint
run: yarn lint
env:
NODE_ENV: production # 本番環境と同じ条件でチェック
この基本設定により、以下のメリットが得られます:
- 環境の統一: Node.js バージョンを固定することで一貫した結果を保証
- 依存関係の確実なインストール:
--frozen-lockfile
でロックファイルと完全一致 - キャッシュの活用: 実行時間の短縮
エラー検出を強化する設定オプション
より確実にエラーを検出するための強化設定を追加しましょう:
yaml# エラー検出強化版のワークフロー
name: Enhanced ESLint Check
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
eslint:
name: ESLint Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.17.0'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
# 設定ファイルの存在確認
- name: Verify ESLint config
run: |
if [ ! -f .eslintrc.js ] && [ ! -f .eslintrc.json ] && [ ! -f eslint.config.js ]; then
echo "Error: ESLint configuration file not found"
exit 1
fi
# 詳細なESLintチェック実行
- name: Run ESLint with detailed output
run: |
yarn lint \
--format=json \
--output-file=eslint-results.json \
--max-warnings=0 # 警告もエラーとして扱う
continue-on-error: true # エラーがあっても次のステップを実行
# エラー結果の解析と表示
- name: Parse and display results
run: |
if [ -f eslint-results.json ]; then
echo "ESLint Results Summary:"
node -e "
const results = JSON.parse(require('fs').readFileSync('eslint-results.json', 'utf8'));
let totalErrors = 0;
let totalWarnings = 0;
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
if (result.errorCount > 0 || result.warningCount > 0) {
console.log(\`\n📁 \${result.filePath}\`);
result.messages.forEach(msg => {
const icon = msg.severity === 2 ? '❌' : '⚠️';
console.log(\` \${icon} Line \${msg.line}:\${msg.column} - \${msg.message} (\${msg.ruleId})\`);
});
}
});
console.log(\`\n📊 Summary: \${totalErrors} errors, \${totalWarnings} warnings\`);
if (totalErrors > 0) {
console.log('❌ ESLint check failed due to errors');
process.exit(1);
}
"
fi
# 結果ファイルをアーティファクトとして保存
- name: Upload ESLint results
uses: actions/upload-artifact@v3
if: always()
with:
name: eslint-results
path: eslint-results.json
複数環境対応とマトリクス実行
異なる Node.js バージョンや OS での動作を確認するマトリクス実行設定:
yaml# マトリクス実行による複数環境テスト
name: Cross-Platform ESLint Check
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
eslint-matrix:
name: ESLint on ${{ matrix.os }} with Node ${{ matrix.node-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ['16.20.0', '18.17.0', '20.5.0']
# 失敗許容設定(特定の組み合わせのみ)
exclude:
- os: windows-latest
node-version: '16.20.0' # Windows + Node16 は除外
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint --format=compact
env:
NODE_ENV: test
具体例
TypeScript/Next.js プロジェクトでの実装例
実際の Next.js プロジェクトでの完全な設定例を見てみましょう:
yaml# .github/workflows/nextjs-eslint.yml
name: Next.js ESLint Quality Gate
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint-and-type-check:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.17.0'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
# Next.js の設定確認
- name: Verify Next.js configuration
run: |
if [ ! -f next.config.js ] && [ ! -f next.config.mjs ]; then
echo "Warning: Next.js config file not found"
fi
# ESLint設定の確認
if [ -f .eslintrc.json ]; then
echo "✅ ESLint configuration found"
cat .eslintrc.json
fi
# TypeScript型チェック
- name: TypeScript type check
run: yarn tsc --noEmit
# ESLint実行(Next.js専用設定)
- name: Run ESLint for Next.js
run: |
yarn lint \
--format=@next/eslint-plugin-next \
--max-warnings=0 \
--cache \
--cache-location=.eslintcache
# 特定のNext.jsルールのチェック
- name: Check Next.js specific rules
run: |
yarn lint \
--rule '@next/next/no-img-element: error' \
--rule '@next/next/no-html-link-for-pages: error' \
--ext .js,.jsx,.ts,.tsx \
pages/ components/ lib/
この設定で検出される典型的な Next.js エラー例:
typescript// components/Header.tsx - Next.js特有のエラー例
import Link from 'next/link';
export default function Header() {
return (
<header>
{/* ESLint エラー: @next/next/no-img-element */}
<img src='/logo.png' alt='Logo' />
{/* ESLint エラー: @next/next/no-html-link-for-pages */}
<a href='/about'>About</a>
{/* 正しい書き方 */}
<Link href='/contact'>
<a>Contact</a>
</Link>
</header>
);
}
発生するエラーメッセージ:
perlerror Do not use `<img>` element. Use `<Image />` from `next/image` instead @next/next/no-img-element
error Do not use `<a>` element to navigate to `/about`. Use `<Link />` from `next/link` instead @next/next/no-html-link-for-pages
Yarn を使った依存関係管理との連携
Yarn の特性を活かした効率的な CI 設定:
yaml# Yarn最適化版のワークフロー
name: Optimized Yarn + ESLint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Fast ESLint with Yarn
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js with Yarn caching
uses: actions/setup-node@v4
with:
node-version: '18.17.0'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
# Yarn の設定確認
- name: Configure Yarn
run: |
yarn config set network-timeout 300000
yarn config set registry https://registry.npmjs.org/
# Yarn バージョン確認
echo "Yarn version: $(yarn --version)"
# ロックファイルの整合性確認
if ! yarn check --integrity; then
echo "❌ yarn.lock integrity check failed"
exit 1
fi
# 高速インストール
- name: Install dependencies with Yarn
run: |
yarn install \
--frozen-lockfile \
--prefer-offline \
--silent
env:
YARN_CACHE_FOLDER: ~/.yarn-cache
# 依存関係の監査
- name: Audit dependencies
run: yarn audit --level moderate
continue-on-error: true
# ESLint実行(Yarnスクリプト使用)
- name: Run ESLint via Yarn
run: |
# package.jsonのscriptsを確認
if yarn run --silent lint --help > /dev/null 2>&1; then
echo "✅ Running ESLint via yarn lint"
yarn lint --format=stylish --color
else
echo "❌ yarn lint script not found in package.json"
exit 1
fi
対応する package.json
の設定例:
json{
"name": "nextjs-eslint-project",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint --dir . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "next lint --fix --dir . --ext .js,.jsx,.ts,.tsx",
"lint:strict": "next lint --max-warnings 0",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.5.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"eslint": "^8.50.0",
"eslint-config-next": "^13.5.0",
"typescript": "^5.2.0"
}
}
プルリクエスト連動の設定例
プルリクエスト時に詳細なコメントを自動投稿する設定:
yaml# プルリクエスト連動版
name: PR ESLint Review
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
eslint-pr-review:
name: ESLint PR Review
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # PRコメント投稿権限
steps:
- name: Checkout PR code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.17.0'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
# 変更ファイルのみをESLintチェック
- name: Get changed files
id: changed-files
run: |
# PRで変更されたJavaScript/TypeScriptファイルのみ取得
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep -E '\.(js|jsx|ts|tsx)$' | tr '\n' ' ')
echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "Changed files: $CHANGED_FILES"
# 変更ファイルのみESLint実行
- name: Run ESLint on changed files
if: steps.changed-files.outputs.files != ''
run: |
yarn lint \
--format=json \
--output-file=eslint-pr-results.json \
${{ steps.changed-files.outputs.files }}
continue-on-error: true
# ESLint結果をPRコメントとして投稿
- name: Comment ESLint results on PR
if: steps.changed-files.outputs.files != ''
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
if (!fs.existsSync('eslint-pr-results.json')) {
console.log('No ESLint results file found');
return;
}
const results = JSON.parse(fs.readFileSync('eslint-pr-results.json', 'utf8'));
let totalErrors = 0;
let totalWarnings = 0;
let commentBody = '## 🔍 ESLint Results\n\n';
results.forEach(result => {
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
if (result.errorCount > 0 || result.warningCount > 0) {
const fileName = result.filePath.replace(process.cwd() + '/', '');
commentBody += `### 📁 \`${fileName}\`\n\n`;
result.messages.forEach(msg => {
const icon = msg.severity === 2 ? '❌' : '⚠️';
const severity = msg.severity === 2 ? 'Error' : 'Warning';
commentBody += `${icon} **${severity}** (Line ${msg.line}:${msg.column}): ${msg.message}\n`;
commentBody += ` Rule: \`${msg.ruleId}\`\n\n`;
});
}
});
if (totalErrors === 0 && totalWarnings === 0) {
commentBody += '✅ No ESLint issues found in changed files!\n';
} else {
commentBody += `### 📊 Summary\n`;
commentBody += `- **Errors**: ${totalErrors}\n`;
commentBody += `- **Warnings**: ${totalWarnings}\n\n`;
if (totalErrors > 0) {
commentBody += '❌ Please fix the errors before merging.\n';
}
}
// 既存のESLintコメントを削除
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
for (const comment of comments) {
if (comment.body.includes('🔍 ESLint Results')) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
});
}
}
// 新しいコメントを投稿
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
// エラーがある場合はワークフローを失敗させる
if (totalErrors > 0) {
core.setFailed(`ESLint found ${totalErrors} error(s)`);
}
この設定により、プルリクエストに以下のようなコメントが自動投稿されます:
markdown## 🔍 ESLint Results
### 📁 `components/UserProfile.tsx`
❌ **Error** (Line 15:7): 'userName' is assigned a value but never used
Rule: `@typescript-eslint/no-unused-vars`
⚠️ **Warning** (Line 23:12): Missing return type on function
Rule: `@typescript-eslint/explicit-function-return-type`
### 📊 Summary
- **Errors**: 1
- **Warnings**: 1
❌ Please fix the errors before merging.
まとめ
GitHub Actions と ESLint を連携させることで、コードの品質を自動的に保証し、開発効率を大幅に向上させることができます。
効果的な活用のための 5 つのポイント
- 環境の統一: Node.js バージョンとパッケージマネージャーを固定し、一貫した実行環境を構築する
- 段階的なチェック: 変更ファイルのみのチェックから全体チェックまで、用途に応じて使い分ける
- 詳細な結果表示: JSON 形式での結果出力と、わかりやすい形式での表示を組み合わせる
- プルリクエスト連動: 自動コメント投稿により、レビュー効率を向上させる
- キャッシュの活用: 依存関係と ESLint キャッシュを活用し、実行時間を短縮する
継続運用のための 3 つのポイント
- 定期的な設定見直し: ESLint ルールとワークフロー設定を定期的にアップデートする
- チーム共有: 設定変更時はチーム全体に共有し、ローカル環境との整合性を保つ
- メトリクス収集: エラー発生頻度や修正時間を測定し、継続的な改善を行う
GitHub Actions による ESLint 自動化を導入することで、手動チェックの負担を大幅に軽減し、より高品質なコードを継続的に維持できる開発環境を構築できます。チーム開発においても、コードレビューの効率化と品質向上の両立が実現できますね。
関連リンク
- blog
うちのチーム、これやってない?アジャイル開発を腐らせる、ありがちなアンチパターン 10 選と処方箋
- blog
CD パイプラインを構築して、開発チームを「リリース疲れ」から解放しよう
- blog
見積もりが全然当たらないあなたへ。プランニングポーカーで楽しく、納得感のある見積もりをするコツ
- blog
「QA は最後の砦」という幻想を捨てる。開発プロセスに QA を組み込み、手戻りをなくす方法
- blog
ドキュメントは「悪」じゃない。アジャイル開発で「ちょうどいい」ドキュメントを見つけるための思考法
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体
- review
瞬時に答えが出る脳に変身!『ゼロ秒思考』赤羽雄二が贈る思考力爆上げトレーニング
- review
関西弁のゾウに人生変えられた!『夢をかなえるゾウ 1』水野敬也が教えてくれた成功の本質
- review
「なぜ私の考えは浅いのか?」の答えがここに『「具体 ⇄ 抽象」トレーニング』細谷功