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 環境 ([設定] > [環境]) を使用します。 staging と production に個別の環境を作成します。環境固有のシークレットは、リポジトリ レベルではなく、各環境の下に保存します。共有シークレット (Turbo リモート キャッシュ トークンなど) の場合は、リポジトリ レベルのシークレットを使用します。ジョブ構成の environment: キーにより、環境固有のシークレットと保護ルールが有効になります。
CI での pnpm インストールを高速化するにはどうすればよいですか?
actions/setup-node@v4 と cache: '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 サポートが必要な場合は、当社のエンジニアリング サービスをご覧ください。
執筆者
ECOSIRE Research and Development Team
ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。
関連記事
AI-Powered Accounting Automation: What Works in 2026
Discover which AI accounting automation tools deliver real ROI in 2026, from bank reconciliation to predictive cash flow, with implementation strategies.
Payroll Processing: Setup, Compliance, and Automation
Complete payroll processing guide covering employee classification, federal and state withholding, payroll taxes, garnishments, automation platforms, and year-end W-2 compliance.
AI Agents for Business Automation: The 2026 Landscape
Explore how AI agents are transforming business automation in 2026, from multi-agent orchestration to practical deployment strategies for enterprise teams.