PR

Node.js プロジェクトでGitHub Actions CI/CDを構築する:実践的なワークフロー設定から本番デプロイまで

Node.jsプロジェクトでGitHub Actionsを使ったCI/CDパイプラインを構築したいけれど、「どこから始めればいいか分からない」「設定が複雑そう」と感じていませんか?この記事では、実際のNode.jsプロジェクトを例に、段階的にCI/CDパイプラインを構築する方法を詳しく解説します。

シリーズ記事

前提:サンプルNode.jsプロジェクトの準備

この記事では、以下の構成のExpressアプリケーションを例に進めます。

プロジェクト構成

my-node-app/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
├── src/
│   ├── app.js
│   ├── routes/
│   └── utils/
├── tests/
│   ├── unit/
│   └── integration/
├── package.json
├── package-lock.json
├── .eslintrc.js
├── jest.config.js
└── README.md

package.json の設定例

{
  "name": "my-node-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch",
    "lint": "eslint src/ tests/",
    "lint:fix": "eslint src/ tests/ --fix",
    "build": "npm run lint && npm test",
    "security-audit": "npm audit"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5",
    "helmet": "^7.0.0"
  },
  "devDependencies": {
    "jest": "^29.5.0",
    "supertest": "^6.3.3",
    "eslint": "^8.42.0",
    "nodemon": "^2.0.22"
  }
}

段階1:基本的なCIワークフローの構築

最初のワークフロー:単純なテスト実行

.github/workflows/ci.yml を作成します:

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test

ワークフローの動作確認

  1. 上記ファイルをリポジトリにコミット
  2. GitHub の「Actions」タブで実行状況を確認
  3. 各ステップのログを確認してエラーがないかチェック

段階2:複数Node.jsバージョンでのテスト

マトリックス戦略の導入

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
        
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test
        
      - name: Display Node.js version
        run: node --version

条件分岐による最適化

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
        
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test
        
      # カバレッジは最新版でのみ実行(効率化)
      - name: Run tests with coverage
        if: matrix.node-version == '20'
        run: npm run test:coverage
        
      # セキュリティ監査も最新版でのみ実行
      - name: Security audit
        if: matrix.node-version == '20'
        run: npm run security-audit

段階3:リンティングとコード品質チェック

ESLintの統合

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run ESLint
        run: npm run lint
        
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
        
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test

Prettierの追加

  code-style:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Check code formatting
        run: npx prettier --check "src/**/*.js" "tests/**/*.js"
        
      - name: Run ESLint
        run: npm run lint

段階4:テストカバレッジとレポート

カバレッジレポートの生成と保存

  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests with coverage
        run: npm run test:coverage
        
      - name: Upload coverage reports
        uses: actions/upload-artifact@v3
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7
          
      - name: Display coverage summary
        run: |
          echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY
          cat coverage/lcov-report/index.html | grep -A 10 "Coverage" >> $GITHUB_STEP_SUMMARY

Codecovとの連携

      - name: Upload to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: true

段階5:ビルドプロセスの自動化

アプリケーションビルドの追加

  build:
    needs: [lint, test]  # lint と test が成功後に実行
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build application
        run: npm run build
        
      - name: Create deployment package
        run: |
          mkdir -p deploy
          cp -r src/ deploy/
          cp package*.json deploy/
          cd deploy && npm ci --production
          
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: app-build
          path: deploy/
          retention-days: 30

段階6:セキュリティとパフォーマンスチェック

セキュリティ脆弱性の検査

  security:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run security audit
        run: npm audit --audit-level moderate
        
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

Docker セキュリティスキャン

  docker-security:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Build Docker image
        run: docker build -t my-app:latest .
        
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:latest'
          format: 'sarif'
          output: 'trivy-results.sarif'
          
      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

段階7:デプロイメント自動化

Herokuへのデプロイ

.github/workflows/deploy.yml を作成:

name: Deploy

on:
  push:
    branches: [main]
  release:
    types: [published]

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Deploy to Heroku staging
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: "my-app-staging"
          heroku_email: ${{ secrets.HEROKU_EMAIL }}
          
  deploy-production:
    if: github.event_name == 'release'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Deploy to Heroku production
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: "my-app-production"
          heroku_email: ${{ secrets.HEROKU_EMAIL }}

Vercelへのデプロイ

  deploy-vercel:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build for production
        run: npm run build
        
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

段階8:通知とモニタリング

Slack通知の追加

  notify:
    needs: [deploy-production]
    runs-on: ubuntu-latest
    if: always()
    
    steps:
      - name: Notify Slack
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          channel: '#deployments'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
          fields: repo,message,commit,author,action,eventName,ref,workflow

GitHub Issue作成(失敗時)

      - name: Create issue on failure
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: 'Deployment failed: ' + context.sha,
              body: 'Deployment failed for commit ' + context.sha + '. Please check the workflow logs.',
              labels: ['bug', 'deployment']
            })

完全版ワークフローファイル

CI ワークフロー(.github/workflows/ci.yml)

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '18'

jobs:
  lint:
    name: Code Quality
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Check code formatting
        run: npx prettier --check "src/**/*.js" "tests/**/*.js"
        
      - name: Run ESLint
        run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
        
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test
        
      - name: Run tests with coverage
        if: matrix.node-version == '20'
        run: npm run test:coverage
        
      - name: Upload coverage reports
        if: matrix.node-version == '20'
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

  security:
    name: Security Scan
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run security audit
        run: npm audit --audit-level moderate

  build:
    name: Build
    needs: [lint, test, security]
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build application
        run: npm run build
        
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: app-build-${{ github.sha }}
          path: dist/
          retention-days: 7

トラブルシューティング

よくある問題と解決方法

1. npm ci が失敗する

原因:package-lock.json と package.json の不整合

解決

# ローカルで package-lock.json を再生成
rm package-lock.json
npm install
git add package-lock.json
git commit -m "Fix package-lock.json"

2. テストがローカルでは通るがCIで失敗

原因:環境差異、タイムゾーン問題、ファイルパス問題

解決

      - name: Debug environment
        run: |
          echo "Node version: $(node --version)"
          echo "NPM version: $(npm --version)"
          echo "Working directory: $(pwd)"
          echo "Files in directory:"
          ls -la
          echo "Environment variables:"
          env | grep NODE

3. キャッシュが効かない

原因:キャッシュキーの設定ミス

解決

      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

パフォーマンス最適化のベストプラクティス

1. 依存関係インストールの高速化

      - name: Setup Node.js with cache
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'          # ビルトインキャッシュを活用
          
      - name: Install dependencies
        run: npm ci --prefer-offline  # オフラインモード優先

2. 並列実行の活用

jobs:
  lint:
    # ...
  test:
    # ...
  security:
    # ...
  # これらは並列実行される
  
  build:
    needs: [lint, test, security]  # 上記完了後に実行
    # ...

3. 条件付き実行の活用

      - name: Integration tests (main branch only)
        if: github.ref == 'refs/heads/main'
        run: npm run test:integration
        
      - name: E2E tests (release only)
        if: github.event_name == 'release'
        run: npm run test:e2e

まとめ

Node.jsプロジェクトでのGitHub Actions CI/CD構築のポイント:

段階的な構築

  • シンプルから開始:基本的なテスト実行から始める
  • 機能を順次追加:リンティング→カバレッジ→セキュリティ→デプロイ
  • 継続的な改善:実際の運用で見つかった問題を解決

効率的な設定

  • 適切なキャッシュ利用:npm キャッシュとビルドキャッシュ
  • 並列実行の活用:独立したジョブは並列で実行
  • 条件分岐の使用:必要な時だけ重い処理を実行

運用面の考慮

  • 適切な通知設定:成功・失敗の適切な通知
  • セキュリティ対策:依存関係とコンテナのスキャン
  • モニタリング:実行時間と使用量の監視

この設定により、Node.jsプロジェクトで高品質なCI/CDパイプラインを構築できます。次回は、Python プロジェクトでの具体的な実装方法を詳しく解説していきます!

タイトルとURLをコピーしました