T-CREATOR

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

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 つのポイント

  1. 環境の統一: Node.js バージョンとパッケージマネージャーを固定し、一貫した実行環境を構築する
  2. 段階的なチェック: 変更ファイルのみのチェックから全体チェックまで、用途に応じて使い分ける
  3. 詳細な結果表示: JSON 形式での結果出力と、わかりやすい形式での表示を組み合わせる
  4. プルリクエスト連動: 自動コメント投稿により、レビュー効率を向上させる
  5. キャッシュの活用: 依存関係と ESLint キャッシュを活用し、実行時間を短縮する

継続運用のための 3 つのポイント

  1. 定期的な設定見直し: ESLint ルールとワークフロー設定を定期的にアップデートする
  2. チーム共有: 設定変更時はチーム全体に共有し、ローカル環境との整合性を保つ
  3. メトリクス収集: エラー発生頻度や修正時間を測定し、継続的な改善を行う

GitHub Actions による ESLint 自動化を導入することで、手動チェックの負担を大幅に軽減し、より高品質なコードを継続的に維持できる開発環境を構築できます。チーム開発においても、コードレビューの効率化と品質向上の両立が実現できますね。

関連リンク