GitHub Actions CI/CD for Monorepo Projects

Complete GitHub Actions CI/CD guide for Turborepo monorepos: affected-only builds, parallel jobs, caching strategies, environment-based deploys, and security best practices.

E
ECOSIRE Research and Development Team
|2026年3月19日6 分で読める1.3k 語数|

Monorepo プロジェクトの GitHub アクション CI/CD

モノリポジトリの CI/CD パイプラインは、単一アプリケーションのパイプラインとは根本的に異なる課題に直面しています。5 つのアプリケーションのうち 1 つだけが変更された場合、どうすれば高速で信頼性の高いパイプラインを実行できるでしょうか?単純にコミットごとにすべてを構築すると、2 分のパイプラインが 15 分のボトルネックになり、開発者はコミットをバッチ処理することで回避できます。これはまさに防止しようとしているものです。

このガイドでは、Turborepo モノリポジトリ向けの実稼働 GitHub Actions セットアップについて説明します。影響を受けるのみのビルド、並列ジョブ実行、Turbo リモート キャッシュ統合、環境ベースのデプロイメント ゲート、およびログへの機密漏洩やフォークされた PR 実行を防ぐセキュリティ パターンについて説明します。

重要なポイント

  • チェックアウトで fetch-depth: 2 を使用して、Turbo の影響を受けるパッケージの検出を有効にします
  • lint、型チェック、テスト、ビルドを並列ジョブで実行します。これらを順番にチェーンしないでください。
  • マトリックス ジョブを使用すると、YAML を複製することなく、複数の Node.js バージョンまたはオペレーティング システム間でテストできます。
  • Turbo リモート キャッシュ認証情報を YAML の環境変数ではなく GitHub シークレットとして保存します
  • 本番展開ジョブで environment: 保護ルールを使用し、手動承認を必要とする
  • paths-filter アクションにより、ドキュメントまたは非コード ファイルのみが変更された場合に CI を完全にスキップできます
  • シークレットをログに出力しない - ランタイム生成のシークレットには ::add-mask:: を使用する
  • 再利用可能なワークフロー (workflow_call) により、複数のリポジトリ ワークフロー間の重複を排除します

CI のリポジトリ構造

適切な CI セットアップは、開発ワークフローを反映しています。 Turborepo モノリポジトリで機能する構造は次のとおりです。

.github/
  workflows/
    ci.yml          — Runs on every PR and push to main
    deploy.yml      — Runs on push to main (after CI passes)
    security.yml    — Weekly security scans
    cleanup.yml     — Scheduled: delete stale preview deployments
  actions/
    setup-pnpm/
      action.yml    — Reusable: install Node + pnpm + cache

コア CI ワークフロー

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

# Cancel in-progress runs when a new run starts
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}
  NODE_VERSION: '22'
  PNPM_VERSION: '10.28.0'

jobs:
  # Fast path: skip if only docs/assets changed
  changes:
    runs-on: ubuntu-latest
    outputs:
      code: ${{ steps.filter.outputs.code }}
      docs: ${{ steps.filter.outputs.docs }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            code:
              - 'apps/**/*.ts'
              - 'apps/**/*.tsx'
              - 'packages/**/*.ts'
              - 'turbo.json'
              - 'package.json'
              - 'pnpm-lock.yaml'
            docs:
              - 'apps/docs/**'
              - '**/*.md'

  lint:
    needs: changes
    if: needs.changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Required for turbo affected detection

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm turbo run lint

      - name: Type check
        run: pnpm turbo run type-check

  test:
    needs: changes
    if: needs.changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17-alpine
        env:
          POSTGRES_DB: ecosire_test
          POSTGRES_USER: ecosire
          POSTGRES_PASSWORD: password
        ports:
          - 5433:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run unit tests
        run: pnpm turbo run test
        env:
          DATABASE_URL: postgresql://ecosire:password@localhost:5433/ecosire_test
          REDIS_URL: redis://localhost:6379

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./apps/api/coverage/lcov.info

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build all apps
        run: pnpm turbo run build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.STAGING_API_URL }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ github.sha }}
          path: |
            apps/web/.next/
            apps/api/dist/
          retention-days: 1  # Keep briefly for deploy job

Playwright との E2E テスト ジョブ

  e2e:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: [chromium, firefox, mobile-chrome]
      fail-fast: false  # Don't cancel other browsers if one fails

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: cd apps/web && npx playwright install ${{ matrix.project }} --with-deps

      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-${{ github.sha }}

      - name: Start test servers
        run: |
          cd apps/api && node dist/main.js &
          cd apps/web && npx next start &
          npx wait-on http://localhost:3000 http://localhost:3001/health
        env:
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Run E2E tests
        run: cd apps/web && npx playwright test --project=${{ matrix.project }}
        env:
          BASE_URL: http://localhost:3000

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report-${{ matrix.project }}
          path: apps/web/playwright-report/
          retention-days: 7

セキュリティ スキャンのワークフロー

# .github/workflows/security.yml
name: Security Scan

on:
  schedule:
    - cron: '0 2 * * 1'  # Weekly Monday 2am UTC
  push:
    paths:
      - '**/package.json'
      - 'pnpm-lock.yaml'

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '10.28.0'

      - name: Run pnpm audit
        run: pnpm audit --audit-level moderate
        continue-on-error: true  # Don't fail CI, just report

      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: test
          args: --severity-threshold=high

  secret-scanning:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for secret scanning

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

導入ワークフロー

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'production'
        type: choice
        options:
          - production
          - staging

# Only one production deploy at a time
concurrency:
  group: deploy-${{ inputs.environment }}
  cancel-in-progress: false  # Don't cancel in-flight deploys

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: '10.28.0'

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Run quality checks
        run: |
          pnpm turbo run lint type-check test
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

  deploy:
    needs: quality-gate
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}  # Requires approval for production
    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e

            cd /opt/ecosire/app

            echo "Pulling latest changes..."
            git pull origin main

            echo "Installing dependencies..."
            pnpm install --frozen-lockfile

            echo "Building..."
            TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} \
            TURBO_TEAM=${{ vars.TURBO_TEAM }} \
            npx turbo run build

            echo "Running migrations..."
            pnpm --filter @ecosire/db db:migrate

            echo "Restarting services..."
            pm2 restart ecosystem.config.cjs --update-env

            echo "Health check..."
            sleep 10
            curl -f https://ecosire.com/api/health || exit 1
            curl -f https://api.ecosire.com/api/health || exit 1

            echo "Deploy complete!"

      - name: Notify on success
        if: success()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.repos.createCommitComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              commit_sha: context.sha,
              body: `Deployed to ${process.env.ENVIRONMENT} at ${new Date().toISOString()}`
            })
        env:
          ENVIRONMENT: ${{ inputs.environment }}

      - name: Notify on failure
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Deploy failed: ${process.env.ENVIRONMENT}`,
              body: `Deploy to ${process.env.ENVIRONMENT} failed at ${new Date().toISOString()}\n\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
            })
        env:
          ENVIRONMENT: ${{ inputs.environment }}

キャッシュ戦略

効果的なキャッシュは、2 分間の CI 実行と 8 分間の CI 実行の違いです。

# Reusable setup step (extract to composite action)
# .github/actions/setup-pnpm/action.yml
name: Setup pnpm
runs:
  using: composite
  steps:
    - uses: pnpm/action-setup@v4
      with:
        version: '10.28.0'

    - uses: actions/setup-node@v4
      with:
        node-version: '22'
        cache: 'pnpm'

    # Turbo cache (separate from pnpm cache)
    - uses: actions/cache@v4
      with:
        path: .turbo
        key: turbo-${{ runner.os }}-${{ github.sha }}
        restore-keys: |
          turbo-${{ runner.os }}-

    - name: Install dependencies
      run: pnpm install --frozen-lockfile
      shell: bash

セキュリティのベストプラクティス

シークレットをエコーしないでください:

# Wrong — secret appears in logs
- run: echo "API key is ${{ secrets.API_KEY }}"

# If you must generate and use runtime secrets
- run: |
    echo "::add-mask::$GENERATED_SECRET"
    GENERATED_SECRET=$(generate-secret)
    echo "GENERATED_SECRET=$GENERATED_SECRET" >> $GITHUB_ENV

権限を制限する:

# Minimum permissions at workflow level
permissions:
  contents: read
  actions: read
  checks: write  # Add only what you need

jobs:
  test:
    permissions:
      contents: read  # Override at job level if needed

プルリクエストのインジェクションを防止します:

# For PRs from forks — never expose secrets
on:
  pull_request:

jobs:
  test:
    # Secrets not available in fork PRs by default
    # Use pull_request_target only for trusted actions
    runs-on: ubuntu-latest

よくある質問

特定のパッケージが変更された場合にのみワークフローをトリガーするにはどうすればよいですか?

dorny/paths-filter を使用して変更されたパッケージを検出し、フィルター出力をジョブ条件に渡します。 Turbo の組み込みの影響を受ける検出については、Turbo が以前のコミットと比較できるように、チェックアウト ステップで fetch-depth: 2 を確認してください。 pnpm turbo run test --filter="...[HEAD^1]" を使用して、変更されたパッケージとその依存パッケージに対してのみテストを実行します。

CI でデータベースの移行をどのように処理すればよいですか?

テストの後、最終的なビルド アーティファクトが作成される前に実行される別のジョブで移行を実行します。 CI で専用のテスト データベース URL を使用します (開発および運用とは別)。 CI から本番環境に対する移行を自動的に実行しないでください。本番データベースの変更には、GitHub 環境保護ルールを介した手動承認のある workflow_dispatch を使用してください。

複数の環境にわたってシークレットを管理する最善の方法は何ですか?

保護ルールを備えた GitHub 環境 ([設定] > [環境]) を使用します。 stagingproduction に個別の環境を作成します。環境固有のシークレットは、リポジトリ レベルではなく、各環境の下に保存します。共有シークレット (Turbo リモート キャッシュ トークンなど) の場合は、リポジトリ レベルのシークレットを使用します。ジョブ構成の environment: キーにより、環境固有のシークレットと保護ルールが有効になります。

CI での pnpm インストールを高速化するにはどうすればよいですか?

actions/setup-node@v4cache: 'pnpm' は、pnpm ストアをキャッシュします。大規模なモノリポジトリの場合は、キャッシュを復元した後に pnpm install --prefer-offline も使用します。 pnpm ストア キャッシュとターボ リモート キャッシュを組み合わせると、通常、キャッシュ ヒット時にインストール + ビルド時間が 60 ~ 80% 短縮されます。

PR とメイン ブランチのプッシュで異なるワークフローを実行するにはどうすればよいですか?

on トリガー条件を使用します。メインには on.push.branches、PR には on.pull_request.branches を使用します。 PR のより高速なテストのサブセット (lint、単体テスト) とメインの完全なスイートを実行できます。 PR イベントではなく、メインへのプッシュ時にのみデプロイ ジョブを実行するには、github.event_name == 'push' 条件を使用します。


次のステップ

適切に設計された CI/CD パイプラインは、高速エンジニアリング チームの基盤です。これにより、自信を持って頻繁に出荷し、リグレッションを自動的に検出できます。 ECOSIRE は、メインへのプッシュごとに、lint、タイプチェック、1,301 の単体テスト、Playwright E2E、およびゼロダウンタイムの SSH デプロイメントを備えた GitHub Actions CI を実行します。

CI/CD アーキテクチャのコンサルティング、Turborepo モノリポのセットアップ、または完全な DevOps サポートが必要な場合は、当社のエンジニアリング サービスをご覧ください

E

執筆者

ECOSIRE Research and Development Team

ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。

WhatsAppでチャット