monorepo-ci-optimizer について
monorepo の CI で、変更の影響を受けるパッケージだけをビルド・テストするためのスキルです。Turborepo、Nx、キャッシュ、並列実行、変更検出を扱います。
主な機能
- 01影響を受けるパッケージの検出
- 02Turborepo と Nx のワークフロー
- 03リモート・ワークスペースキャッシュ
- 04並列実行とマトリックス戦略
活用シーン
- 01プルリクエストで不要なビルドを省略
- 02monorepo の CI を構成
- 03CI 最適化の結果を追跡
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill monorepo-ci-optimizerインストール後、Claude Code で/monorepo-ci-optimizerを入力して読み込まれたか確認します。
詳細
モノレポ CI オプティマイザー
モノレポで影響を受けるビルドとテストのみを実行し、CI を高速化します。
影響範囲の検出方法
Turborepo を使用する
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for affected detection
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- name: Build affected
run: |
pnpm turbo run build --filter='...[origin/main]'
- name: Test affected
run: |
pnpm turbo run test --filter='...[origin/main]'
- name: Lint affected
run: |
pnpm turbo run lint --filter='...[origin/main]'
Nx を使用する
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Build affected projects
run: npx nx affected:build --base=$NX_BASE --head=$NX_HEAD
- name: Test affected projects
run: npx nx affected:test --base=$NX_BASE --head=$NX_HEAD --parallel=3
- name: Lint affected projects
run: npx nx affected:lint --base=$NX_BASE --head=$NX_HEAD
リモートキャッシュ(Turborepo)
- name: Setup Turborepo cache
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Build with remote cache
run: pnpm turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Nx Cloud との統合
- name: Setup Nx Cloud
run: |
npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Run affected
run: |
npx nx affected --target=build --parallel=3
npx nx affected --target=test --parallel=3
npx nx affected --target=lint --parallel=3
影響範囲の手動検出
// scripts/get-affected.ts
import * as path from "path";
import { execSync } from "child_process";
import * as fs from "fs";
interface Package {
name: string;
path: string;
dependencies: string[];
}
function getChangedFiles(base: string = "origin/main"): string[] {
const output = execSync(`git diff --name-only ${base}...HEAD`, {
encoding: "utf-8",
});
return output.split("\n").filter(Boolean);
}
function getPackages(): Package[] {
const packagesDir = path.join(process.cwd(), "packages");
return fs.readdirSync(packagesDir).map((name) => {
const packageJson = JSON.parse(
fs.readFileSync(path.join(packagesDir, name, "package.json"), "utf-8")
);
return {
name: packageJson.name,
path: `packages/${name}`,
dependencies: [
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.devDependencies || {}),
],
};
});
}
function getAffectedPackages(): string[] {
const changedFiles = getChangedFiles();
const packages = getPackages();
const affected = new Set<string>();
// Direct changes
changedFiles.forEach((file) => {
packages.forEach((pkg) => {
if (file.startsWith(pkg.path)) {
affected.add(pkg.name);
}
});
});
// Dependent packages
let changed = true;
while (changed) {
changed = false;
packages.forEach((pkg) => {
if (!affected.has(pkg.name)) {
const hasAffectedDep = pkg.dependencies.some((dep) =>
affected.has(dep)
);
if (hasAffectedDep) {
affected.add(pkg.name);
changed = true;
}
}
});
}
return Array.from(affected);
}
// Output for GitHub Actions
const affected = getAffectedPackages();
console.log(`::set-output name=packages::${affected.join(",")}`);
console.log(`Affected packages: ${affected.join(", ")}`);
影響を受けるパッケージのマトリックス戦略
detect-affected:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.affected.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: affected
run: |
PACKAGES=$(node scripts/get-affected.ts)
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
test-affected:
needs: detect-affected
if: needs.detect-affected.outputs.packages != ''
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJSON(needs.detect-affected.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install
- name: Test ${{ matrix.package }}
run: pnpm --filter ${{ matrix.package }} test
ワークスペースを考慮したキャッシュ
- name: Cache workspace builds
uses: actions/cache@v3
with:
path: |
packages/*/dist
packages/*/node_modules/.cache
key: ${{ runner.os }}-workspace-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('packages/**/*.ts') }}
restore-keys: |
${{ runner.os }}-workspace-${{ hashFiles('**/pnpm-lock.yaml') }}-
${{ runner.os }}-workspace-
並列実行
- name: Build all packages in parallel
run: pnpm turbo run build --parallel
- name: Test with controlled parallelism
run: pnpm turbo run test --concurrency=3
最適化指標
## Before Optimization
- Full build: 25 minutes
- All tests: 15 minutes
- Total: 40 minutes
- Runs on every PR
## After Affected Detection
- Affected build: 5 minutes (80% reduction)
- Affected tests: 3 minutes (80% reduction)
- Total: 8 minutes (80% reduction)
- Only runs necessary checks
## With Remote Caching
- Cache hit build: 30 seconds (98% reduction)
- Cache hit tests: 1 minute (93% reduction)
- Total: 1.5 minutes (96% reduction)
ベストプラクティス
- 完全な履歴を取得する:正確な差分を取得するため、
fetch-depth: 0を指定します - トポロジカル順序:依存関係を先にビルドします
- リモートキャッシュ:CI の実行間でキャッシュを共有します
- 並列実行:独立したタスクを同時に実行します
- インクリメンタルビルド:変更されたもののみを再ビルドします
- 依存関係グラフ:パッケージ間の関係を追跡します
- フルビルドを強制する:メインブランチへのマージ時に実行します
出力チェックリスト
- 影響範囲の検出を実装済み
- Turborepo または Nx を設定済み
- リモートキャッシュを有効化済み
- 並列実行を最適化済み
- パッケージのマトリックス戦略を設定済み
- ワークスペースを考慮したキャッシュを設定済み
- 依存関係グラフを文書化済み
- 最適化前後の指標を追跡済み
コメント
Markdown 対応