- はじめに
- Reusable Workflowsの詳解
- Composite Actionsの構築
- Reusable Workflows と Composite Actions の比較
- モノレポCI/CDの最適化
- 本番運用ガイド
- 障害事例とトラブルシューティング
- 運用上の注意事項
- 参考資料

はじめに
組織のリポジトリが10個を超えると、CI/CDパイプラインの重複が目につきはじめる。ビルド・テスト・デプロイのYAMLがリポジトリごとにコピーして貼り付けられた形で散らばっており、ひとつのセキュリティパッチを当てるだけでも数十のワークフローを一つずつ修正しなければならない。Node.jsのバージョンを18から20に上げるだけの単純な変更が、30のリポジトリにまたがるPRへ膨れ上がる経験をしたことがあるはずだ。
GitHub Actionsはこの問題を解決するために2つの再利用メカニズムを提供する。Reusable Workflowsはワークフロー全体をテンプレート化して呼び出せるようにし、Composite Actionsは複数のステップをひとつのアクションにまとめてモジュール化できるようにする。
ここにモノレポ環境まで重なると、複雑さは一気に上がる。変更されたパッケージだけをビルドし、依存グラフを追跡し、キャッシュをパッケージごとに分離しなければならない。本記事ではこの3つのテーマを実践的なコードとともに深く扱う。
Reusable Workflowsの詳解
workflow_callトリガーの構造
Reusable Workflowは on: workflow_call トリガーを定義するところから始まる。呼び出し側(caller)は uses キーワードでこのワークフローを参照し、inputs、secrets、outputsを通じてデータをやり取りする。
2025年11月のアップデート以降の主な制約は次のとおりだ:
- ネスト呼び出しは最大 10段階 までサポート (AがBを呼び出し、BがCを呼び出すチェーン)
- ひとつのワークフローファイルから最大 50個 のReusable Workflowを呼び出せる
- 呼び出し側と呼び出される側が同じ組織に属するか、呼び出される側のリポジトリがパブリックである必要がある
envコンテキストは呼び出されるワークフローには渡されない
中央集約型のワークフロー管理パターン
大規模な組織では .github リポジトリや別途の platform-workflows リポジトリにReusable Workflowをまとめておき、すべてのチームがそれを参照するようにする。このパターンの核心はバージョンタグの管理だ。
org-platform/
.github/
workflows/
build-node.yml # Node.jsビルドパイプライン
build-python.yml # Pythonビルドパイプライン
deploy-k8s.yml # Kubernetesデプロイパイプライン
security-scan.yml # セキュリティスキャン共通パイプライン
Reusable Workflowの定義例
以下はNode.jsアプリケーションのビルド・テスト・デプロイをテンプレート化したReusable Workflowだ。
# org-platform/.github/workflows/build-node.yml
name: Reusable Node.js Build
on:
workflow_call:
inputs:
node-version:
description: 'Node.jsのバージョン'
required: false
type: string
default: '20'
working-directory:
description: '作業ディレクトリ'
required: false
type: string
default: '.'
run-e2e:
description: 'E2Eテストを実行するかどうか'
required: false
type: boolean
default: false
artifact-name:
description: 'ビルドアーティファクト名'
required: false
type: string
default: 'build-output'
secrets:
NPM_TOKEN:
description: 'npmレジストリのトークン'
required: false
SONAR_TOKEN:
description: 'SonarQube分析のトークン'
required: false
outputs:
build-version:
description: 'ビルドされたバージョン'
value: ${{ jobs.build.outputs.version }}
test-coverage:
description: 'テストカバレッジ'
value: ${{ jobs.test.outputs.coverage }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
run: npm ci
- name: Build
working-directory: ${{ inputs.working-directory }}
run: npm run build
- name: Extract version
id: version
working-directory: ${{ inputs.working-directory }}
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.working-directory }}/dist/
retention-days: 7
test:
runs-on: ubuntu-latest
needs: build
outputs:
coverage: ${{ steps.coverage.outputs.percentage }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
run: npm ci
- name: Run unit tests
working-directory: ${{ inputs.working-directory }}
run: npm run test -- --coverage
- name: Extract coverage
id: coverage
working-directory: ${{ inputs.working-directory }}
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "percentage=$COVERAGE" >> "$GITHUB_OUTPUT"
- name: SonarQube analysis
if: secrets.SONAR_TOKEN != ''
uses: SonarSource/sonarqube-scan-action@v3
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
e2e:
if: inputs.run-e2e
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.working-directory }}/dist/
- name: Run E2E tests
working-directory: ${{ inputs.working-directory }}
run: npm run test:e2e
Caller Workflowの例
上のReusable Workflowを呼び出すcallerワークフローは次のとおりだ。
# my-service/.github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
uses: org-platform/.github/workflows/build-node.yml@v2.3.0
with:
node-version: '20'
working-directory: '.'
run-e2e: ${{ github.ref == 'refs/heads/main' }}
artifact-name: 'my-service-build'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
deploy-staging:
needs: build-and-test
if: github.ref == 'refs/heads/develop'
uses: org-platform/.github/workflows/deploy-k8s.yml@v2.3.0
with:
environment: staging
image-tag: ${{ needs.build-and-test.outputs.build-version }}
secrets: inherit
deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main'
uses: org-platform/.github/workflows/deploy-k8s.yml@v2.3.0
with:
environment: production
image-tag: ${{ needs.build-and-test.outputs.build-version }}
secrets: inherit
secrets: inherit を使えばcallerのすべてのシークレットが自動的に渡される。セキュリティが重要な環境では、必要なシークレットだけを明示的に渡すのがよい。
Composite Actionsの構築
action.ymlの構造と中心となる概念
Composite Actionは複数のステップをひとつの再利用可能なアクションにまとめる。Reusable Workflowと違ってジョブ単位ではなくステップ単位で動作し、ひとつのジョブの中に複数のComposite Actionを含められる。
JavaScript/Docker/Compositeアクションの比較
| 項目 | JavaScript Action | Docker Action | Composite Action |
|---|---|---|---|
| 実行環境 | Node.jsランタイム | Dockerコンテナ | ランナーで直接実行 |
| 起動時間 | 速い (数秒) | 遅い (イメージのプル) | 速い (数秒) |
| プラットフォーム互換 | Linux/macOS/Windows | Linux専用 | ランナーのOSによって異なる |
| 複雑なロジック | 最適 | 適する | シェルスクリプト程度 |
| 既存ツールの活用 | npmパッケージを使う | どんなツールでもインストールできる | 他のアクションを組み合わせられる |
| 保守の難易度 | 中 (ビルドが必要) | 低 (Dockerfile) | 低 (YAMLのみ) |
| シークレットへのアクセス | 直接アクセスできる | 直接アクセスできる | 環境変数でのみ渡す |
Composite Actionの定義例
以下はDockerイメージのビルドとプッシュをモジュール化したComposite Actionだ。
# .github/actions/docker-build-push/action.yml
name: 'Docker Build and Push'
description: 'Dockerイメージをビルドしてレジストリにプッシュします'
inputs:
registry:
description: 'コンテナレジストリのURL'
required: true
image-name:
description: 'イメージ名'
required: true
dockerfile:
description: 'Dockerfileのパス'
required: false
default: './Dockerfile'
context:
description: 'ビルドコンテキストのパス'
required: false
default: '.'
build-args:
description: 'ビルド引数 (改行区切り)'
required: false
default: ''
push:
description: 'イメージをプッシュするかどうか'
required: false
default: 'true'
outputs:
image-digest:
description: 'ビルドされたイメージのダイジェスト'
value: ${{ steps.build.outputs.digest }}
image-tag:
description: 'イメージのタグ'
value: ${{ steps.meta.outputs.tags }}
runs:
using: 'composite'
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.registry }}/${{ inputs.image-name }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=ref,event=tag
type=semver,pattern=v{{version}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ inputs.build-args }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Print image info
shell: bash
run: |
echo "Image digest: ${{ steps.build.outputs.digest }}"
echo "Image tags: ${{ steps.meta.outputs.tags }}"
Composite Actionの使用例
# my-service/.github/workflows/build-image.yml
name: Build Docker Image
on:
push:
branches: [main]
paths:
- 'src/**'
- 'Dockerfile'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
id: docker
uses: ./.github/actions/docker-build-push
with:
registry: ghcr.io
image-name: ${{ github.repository }}
build-args: |
APP_VERSION=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
- name: Deploy notification
run: |
echo "Deployed image with digest: ${{ steps.docker.outputs.image-digest }}"
Reusable Workflows と Composite Actions の比較
2つのメカニズムは用途が異なる。選択の基準をはっきりさせて初めて、パイプラインのアーキテクチャがすっきりする。
| 項目 | Reusable Workflows | Composite Actions |
|---|---|---|
| 動作する単位 | ジョブ(Job)単位 | ステップ(Step)単位 |
| シークレットへのアクセス | secrets キーワードで直接渡す | 環境変数でしか渡せない |
| ネスト呼び出し | 最大10段階 | 最大10段階 |
| 条件付き実行 | ジョブ単位の if が可能 | ステップ単位の if が可能 |
| ランナーの指定 | 呼び出される側で指定できる | 呼び出し側のランナーで実行 |
| 呼び出し方 | jobs.xxx.uses: | steps.xxx.uses: |
| outputsの受け渡し | ワークフローのoutputsに対応 | ステップのoutputsに対応 |
| 環境(environment) | 別のenvironmentを指定できる | 呼び出し側のenvironmentを共有 |
| 最大呼び出し数 | ワークフローあたり50個 | 制限なし |
| 向いているシナリオ | パイプライン全体のテンプレート | 共通ステップのモジュール |
選択のガイド:
- CI/CDパイプライン全体を標準化したいならReusable Workflowsを使う。ビルド・テスト・デプロイの全工程をひとつのテンプレートにして、チームごとにinputsだけを変えて渡す。
- 特定の作業(Dockerビルド、Slack通知、キャッシュ復元など)をモジュール化したいならComposite Actionsを使う。複数のワークフローからステップ単位で再利用する。
- 2つを組み合わせるのが最も強力だ。Reusable Workflowの中からComposite Actionを呼び出せば、パイプライン全体の流れはワークフローで管理し、細かいステップはアクションとしてモジュール化できる。
モノレポCI/CDの最適化
モノレポで最も重要な原則は変更されたパッケージだけをビルドすることだ。すべての変更に対してリポジトリ全体をビルドすると、CI時間は指数関数的に増える。
dorny/paths-filterを活用した変更検知
dorny/paths-filter は、PRやpushで変更されたファイルパスをもとにbooleanのフィルターを生成する。このフィルターを後続ジョブの条件として使えば、変更されたパッケージだけを選択的にビルドできる。
動的matrix戦略とfromJSONパターン
静的なmatrixは常にすべての組み合わせを実行する。モノレポでは、変更されたパッケージの一覧を動的に生成してmatrixに注入するパターンが欠かせない。fromJSON 関数を使えばJSON文字列をmatrixの値に変換できる。
モノレポのpath-filter + dynamic matrix YAML
以下は、モノレポで変更検知と動的matrixを組み合わせた実践的なワークフローだ。
# .github/workflows/monorepo-ci.yml
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.filter.outputs.changes }}
api-changed: ${{ steps.filter.outputs.api }}
web-changed: ${{ steps.filter.outputs.web }}
shared-changed: ${{ steps.filter.outputs.shared }}
steps:
- uses: actions/checkout@v4
- name: Detect changed packages
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'packages/api/**'
- 'packages/shared/**'
web:
- 'packages/web/**'
- 'packages/shared/**'
shared:
- 'packages/shared/**'
docs:
- 'packages/docs/**'
build-matrix:
needs: detect-changes
if: needs.detect-changes.outputs.packages != '[]'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Build dynamic matrix
id: set-matrix
run: |
CHANGES='${{ needs.detect-changes.outputs.packages }}'
MATRIX=$(echo "$CHANGES" | jq -c '{package: .}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
build:
needs: build-matrix
if: needs.build-matrix.outputs.matrix != ''
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.build-matrix.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build --workspace=packages/${{ matrix.package }}
- name: Test package
run: npm run test --workspace=packages/${{ matrix.package }}
integration-test:
needs: [detect-changes, build]
if: >-
needs.detect-changes.outputs.api-changed == 'true' ||
needs.detect-changes.outputs.web-changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install all dependencies
run: npm ci
- name: Run integration tests
run: npm run test:integration
deploy:
needs: [detect-changes, build, integration-test]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- package: api
changed: ${{ needs.detect-changes.outputs.api-changed }}
- package: web
changed: ${{ needs.detect-changes.outputs.web-changed }}
fail-fast: false
steps:
- name: Skip if not changed
if: matrix.changed != 'true'
run: echo "Skipping deploy for ${{ matrix.package }} (no changes)"
- uses: actions/checkout@v4
if: matrix.changed == 'true'
- name: Deploy
if: matrix.changed == 'true'
run: |
echo "Deploying ${{ matrix.package }}..."
# 実際のデプロイ処理
キャッシング最適化のYAML
モノレポでのキャッシュ戦略は、パッケージごとの分離が肝になる。グローバルキャッシュとパッケージ別キャッシュを階層的に管理する。
# .github/workflows/cache-optimized.yml
name: Cache Optimized Build
on:
push:
branches: [main]
pull_request:
env:
TURBO_CACHE_DIR: .turbo
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
# 第1段階: npm依存関係のキャッシュ (package-lock.jsonベース)
- name: Setup Node.js with dependency cache
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# 第2段階: Turborepoのリモートキャッシュではなくローカルキャッシュを使う
- name: Cache Turborepo
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
# 第3段階: Next.jsのビルドキャッシュ (ページ単位の増分ビルド)
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: packages/web/.next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('packages/web/**/*.ts', 'packages/web/**/*.tsx') }}
restore-keys: |
nextjs-${{ runner.os }}-
# 第4段階: ESLintのキャッシュ
- name: Cache ESLint
uses: actions/cache@v4
with:
path: .eslintcache
key: eslint-${{ runner.os }}-${{ hashFiles('.eslintrc*') }}-${{ github.sha }}
restore-keys: |
eslint-${{ runner.os }}-${{ hashFiles('.eslintrc*') }}-
eslint-${{ runner.os }}-
# 第5段階: Jestのキャッシュ
- name: Cache Jest
uses: actions/cache@v4
with:
path: /tmp/jest_rs
key: jest-${{ runner.os }}-${{ hashFiles('**/jest.config.*') }}
restore-keys: |
jest-${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build with Turborepo
run: npx turbo run build --cache-dir=.turbo
- name: Test with Turborepo
run: npx turbo run test --cache-dir=.turbo
- name: Lint with cache
run: npx turbo run lint --cache-dir=.turbo
上のキャッシュ戦略を適用すると、一般的なモノレポでは次のような効果が得られる:
| キャッシュ対象 | キャッシュミス時 | キャッシュヒット時 | 削減率 |
|---|---|---|---|
| npm依存関係 | 45〜90秒 | 5〜10秒 | 80〜90% |
| Turborepoビルド | 120〜300秒 | 3〜8秒 | 95%+ |
| Next.js増分ビルド | 60〜180秒 | 10〜30秒 | 70〜85% |
| ESLint分析 | 30〜60秒 | 5〜15秒 | 60〜75% |
| Jestテスト | キャッシュ不可 | transformキャッシュ | 20〜40% |
本番運用ガイド
ワークフローのバージョン管理戦略
Reusable WorkflowとComposite Actionの参照の仕方によって、安定性とセキュリティは大きく変わる。
| 参照方式 | 例 | 利点 | 欠点 |
|---|---|---|---|
| ブランチ | uses: org/repo/.github/workflows/ci.yml@main | 常に最新バージョン | 予告なく壊れることがある |
| タグ | uses: org/repo/.github/workflows/ci.yml@v2.3.0 | 安定しており、SemVerの意味が伝わる | タグを上書きされうる |
| コミットSHA | uses: org/repo/.github/workflows/ci.yml@a1b2c3d4 | 最も安全で、変わらない | 可読性が低い |
推奨戦略: 開発中はブランチ参照、本番ではSHA参照を使う。DependabotやRenovateを設定しておけば、SHAが更新されたときに自動でPRを作ってくれる。
組織全体のガバナンスと標準化
組織レベルで次を強制する:
- 必須ワークフロー(Required Workflows): Organization Settingsで、特定のReusable Workflowをすべてのリポジトリに強制実行できる。セキュリティスキャンやライセンス検査などに使う。
- CODEOWNERS:
.github/workflows/ディレクトリにCODEOWNERSを設定し、ワークフロー変更時にプラットフォームチームのレビューを強制する。 - ワークフロー権限の制限: Organizationレベルで
GITHUB_TOKENの既定権限をreadに設定し、必要な権限だけをワークフローで明示させる。
セキュリティ: OIDCと最小権限の原則
クラウドへのデプロイでは、長期の資格情報(アクセスキーなど)をシークレットに保存する代わりに、OIDC (OpenID Connect)を使って一時的なトークンを発行してもらう。
# OIDCを使ったAWSデプロイの例
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: ap-northeast-2
role-session-name: github-actions-${{ github.run_id }}
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production \
--service my-api \
--force-new-deployment
この方式はシークレット漏洩のリスクを根本から取り除き、IAM Roleのtrust policyで特定のリポジトリとブランチだけを許可できる。
障害事例とトラブルシューティング
事例1: シークレットの受け渡し失敗でデプロイが停止
症状: Reusable WorkflowからのAWSデプロイが失敗する。エラーメッセージは「credentials not found」。
原因: callerワークフローで secrets: inherit ではなく明示的なシークレットの受け渡しを使っていたが、新しく追加された AWS_ROLE_ARN シークレットを渡していなかった。
教訓: secrets: inherit は便利だが、どのシークレットが渡っているのかを追いにくい。本番環境では明示的な受け渡しを勧めるが、新しいシークレットを追加したときにcallerの更新を忘れないよう、CIに検証ステップを追加しておくこと。
解決パターン:
# シークレットの受け渡し検証を含むcallerワークフロー
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Validate required secrets
run: |
MISSING=""
if [ -z "$AWS_ROLE" ]; then MISSING="$MISSING AWS_ROLE_ARN"; fi
if [ -z "$NPM_TK" ]; then MISSING="$MISSING NPM_TOKEN"; fi
if [ -n "$MISSING" ]; then
echo "::error::Missing required secrets:$MISSING"
exit 1
fi
env:
AWS_ROLE: ${{ secrets.AWS_ROLE_ARN }}
NPM_TK: ${{ secrets.NPM_TOKEN }}
deploy:
needs: validate
uses: org-platform/.github/workflows/deploy-k8s.yml@v2.3.0
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
事例2: キャッシュミスでCI時間が急増
症状: 月曜の朝になるたびに、モノレポのCI時間が普段の5分から25分に増える。
原因: GitHub Actionsのキャッシュは7日間使われないと自動的に削除される。週末のあいだにキャッシュが失効し、月曜の最初のビルドですべてのキャッシュがミス扱いになっていた。
解決: スケジュールワークフローで週末もキャッシュを更新し、restore-keys パターンを階層的に設定する。
デバッグのチェックリスト
問題が起きたら次の順番で点検する:
- ワークフロー権限の確認:
permissionsブロックが必要な権限をすべて含んでいるか - シークレットのスコープ確認: Organizationのシークレットか、Repositoryのシークレットか、Environmentのシークレットか
- キャッシュキーのパターン確認:
hashFilesのパスが実在するファイルを指しているか - Reusable Workflowへのアクセス権限: 呼び出される側のリポジトリのSettingsでActionsへのアクセスを許可したか
- matrixの値の検証:
fromJSONに渡される文字列が有効なJSONかどうかを確認 - 環境変数のスコープ: Reusable Workflowにはcallerの
envが渡されない点を忘れない
運用上の注意事項
呼び出し制限と同時実行の管理
GitHub Actionsには次のような制限がある:
- 同時実行ジョブ: Freeプラン20個、Teamプラン60個、Enterpriseプラン180個
- ワークフロー実行キュー: リポジトリあたり最大500個
- matrixの最大組み合わせ: 256個
- ワークフローの実行時間: 最大6時間 (self-hostedは無制限)
同時実行の問題を防ぐには concurrency グループを設定する:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
同じブランチへのデプロイが同時に実行されるのを防ぎ、cancel-in-progress: false によって実行中のデプロイをキャンセルしない。
環境変数のスコープに注意
Reusable Workflowで最もよくある誤りは、環境変数のスコープを取り違えることだ:
envキーワードで定義した環境変数はReusable Workflowには渡されないvarsコンテキスト(Repository/Organizationの変数)はReusable WorkflowからアクセスできるgithubコンテキストはReusable Workflowではcallerの値が渡される
必要な値は必ず inputs で明示的に渡さなければならない。
大規模モノレポの性能のヒント
- Sparse checkoutの利用: 変更されたパッケージのディレクトリだけをチェックアウトして、checkoutの時間を短縮する。
- uses: actions/checkout@v4
with:
sparse-checkout: |
packages/api
packages/shared
package.json
package-lock.json
sparse-checkout-cone-mode: false
- Turborepo Remote Cache: VercelのRemote Cacheや自前でホストするキャッシュサーバーを使えば、ローカル開発とCIのあいだでビルドキャッシュを共有できる。
- Affectedなパッケージだけを実行: Nxの
nx affectedやTurborepoの変更検知を活用し、依存グラフをもとに影響を受けたパッケージだけをビルドする。 - 並列ジョブへの分散: matrix戦略でパッケージごとのビルドを並列化しつつ、sharedライブラリは先行ビルドとして分離する。