GitHub ActionsのReusable Workflows:CI/CDをDRYに保つ

Shunku

はじめに

組織が成長するにつれて、多くのリポジトリで似たようなワークフローパターンが繰り返されることに気づくでしょう。Reusable Workflows(再利用可能なワークフロー)を使えば、ワークフローを一度定義して複数の他のワークフローから呼び出すことができ、DRY(Don't Repeat Yourself)原則をCI/CDに適用できます。

この記事では、Reusable Workflowsを効果的に作成・使用する方法を説明します。

なぜReusable Workflowsなのか?

flowchart TB
    subgraph Before["変更前: 重複したワークフロー"]
        R1["Repo A: ci.yml<br/>(100行)"]
        R2["Repo B: ci.yml<br/>(100行)"]
        R3["Repo C: ci.yml<br/>(100行)"]
    end

    subgraph After["変更後: Reusable Workflow"]
        Central["Central: build.yml<br/>(100行)"]
        C1["Repo A: ci.yml<br/>(10行)"]
        C2["Repo B: ci.yml<br/>(10行)"]
        C3["Repo C: ci.yml<br/>(10行)"]
        Central --> C1
        Central --> C2
        Central --> C3
    end

    style Before fill:#ef4444,color:#fff
    style After fill:#22c55e,color:#fff
メリット 説明
一貫性 すべてのリポジトリで同じワークフローロジック
保守性 一度更新すればすべてに適用
複雑さの軽減 呼び出し側のワークフローがシンプルに
カプセル化 実装の詳細を隠蔽

Reusable Workflowの作成

基本構造

Reusable Workflowはworkflow_callトリガーを使用します:

# .github/workflows/reusable-build.yml
name: Reusable Build Workflow

on:
  workflow_call:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build

inputsの追加

カスタマイズ用の入力を定義します:

name: Reusable Build

on:
  workflow_call:
    inputs:
      node-version:
        description: 'Node.jsバージョン'
        required: false
        type: string
        default: '20'

      working-directory:
        description: '作業ディレクトリ'
        required: false
        type: string
        default: '.'

      build-command:
        description: '実行するビルドコマンド'
        required: false
        type: string
        default: 'npm run build'

      environment:
        description: 'デプロイ環境'
        required: false
        type: string

jobs:
  build:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

      - run: npm ci
      - run: ${{ inputs.build-command }}

inputの型

説明
string テキスト値 'production'
boolean 真偽値 true
number 数値 42

secretsの追加

シークレットを安全に渡します:

on:
  workflow_call:
    inputs:
      environment:
        type: string
        required: true

    secrets:
      npm-token:
        description: 'NPM認証トークン'
        required: true

      deploy-key:
        description: 'デプロイ用SSHキー'
        required: false

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

      - name: Setup NPM auth
        run: echo "//registry.npmjs.org/:_authToken=${{ secrets.npm-token }}" >> ~/.npmrc

      - name: Deploy
        if: secrets.deploy-key != ''
        run: ./deploy.sh
        env:
          SSH_KEY: ${{ secrets.deploy-key }}

secretsの継承

secrets: inheritを使用して呼び出し元のすべてのシークレットを渡します:

on:
  workflow_call:
    secrets:
      npm-token:
        required: true
      # または呼び出し時に 'inherit' を使用してすべてのシークレットを渡す

outputsの追加

Reusable Workflowから値を返します:

name: Build and Get Version

on:
  workflow_call:
    outputs:
      version:
        description: 'ビルドしたバージョン'
        value: ${{ jobs.build.outputs.version }}

      artifact-name:
        description: 'アップロードしたアーティファクトの名前'
        value: ${{ jobs.build.outputs.artifact-name }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      artifact-name: build-${{ steps.version.outputs.version }}

    steps:
      - uses: actions/checkout@v4

      - name: Get version
        id: version
        run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT

      - run: npm ci
      - run: npm run build

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ steps.version.outputs.version }}
          path: dist/

Reusable Workflowの呼び出し

基本的な呼び出し

name: CI

on:
  push:
    branches: [main]

jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml

inputsとsecretsを使用

name: CI

on:
  push:
    branches: [main]

jobs:
  build:
    uses: owner/repo/.github/workflows/reusable-build.yml@main
    with:
      node-version: '20'
      working-directory: './frontend'
      environment: 'production'
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}
      deploy-key: ${{ secrets.DEPLOY_KEY }}

  # またはすべてのシークレットを継承
  build-inherit:
    uses: ./.github/workflows/reusable-build.yml
    with:
      node-version: '20'
    secrets: inherit

outputsの使用

name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml
    with:
      node-version: '20'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: ${{ needs.build.outputs.artifact-name }}

      - name: Deploy version
        run: echo "Deploying version ${{ needs.build.outputs.version }}"

実践例

再利用可能なNode.js CIワークフロー

# .github/workflows/reusable-node-ci.yml
name: Reusable Node.js CI

on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
      run-lint:
        type: boolean
        default: true
      run-tests:
        type: boolean
        default: true
      upload-coverage:
        type: boolean
        default: false

    outputs:
      test-result:
        value: ${{ jobs.test.outputs.result }}

jobs:
  lint:
    if: inputs.run-lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    if: inputs.run-tests
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.test.outputs.result }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci

      - name: Run tests
        id: test
        run: |
          npm test -- --coverage
          echo "result=success" >> $GITHUB_OUTPUT

      - name: Upload coverage
        if: inputs.upload-coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

再利用可能なDockerビルド&プッシュ

# .github/workflows/reusable-docker.yml
name: Build and Push Docker Image

on:
  workflow_call:
    inputs:
      image-name:
        type: string
        required: true
      dockerfile:
        type: string
        default: 'Dockerfile'
      context:
        type: string
        default: '.'
      push:
        type: boolean
        default: false
      platforms:
        type: string
        default: 'linux/amd64'

    secrets:
      registry-username:
        required: true
      registry-password:
        required: true

    outputs:
      image-tag:
        value: ${{ jobs.build.outputs.tag }}
      image-digest:
        value: ${{ jobs.build.outputs.digest }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tags }}
      digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.registry-username }}
          password: ${{ secrets.registry-password }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ inputs.image-name }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix=
            type=semver,pattern={{version}}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: ${{ inputs.context }}
          file: ${{ inputs.dockerfile }}
          platforms: ${{ inputs.platforms }}
          push: ${{ inputs.push }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

再利用可能なデプロイワークフロー

# .github/workflows/reusable-deploy.yml
name: Deploy to Environment

on:
  workflow_call:
    inputs:
      environment:
        type: string
        required: true
      artifact-name:
        type: string
        required: true
      url:
        type: string
        required: true

    secrets:
      deploy-token:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment }}
      url: ${{ inputs.url }}

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: ${{ inputs.artifact-name }}
          path: ./dist

      - name: Deploy
        run: |
          echo "Deploying to ${{ inputs.environment }}"
          # デプロイロジックをここに
        env:
          DEPLOY_TOKEN: ${{ secrets.deploy-token }}

      - name: Verify deployment
        run: curl -f ${{ inputs.url }}/health

Starter Workflows

組織用のテンプレートワークフローを作成:

.github/
├── workflow-templates/
│   ├── node-ci.yml
│   ├── node-ci.properties.json
│   ├── docker-build.yml
│   └── docker-build.properties.json

node-ci.properties.json:

{
  "name": "Node.js CI",
  "description": "組織の標準Node.js CIワークフロー",
  "iconName": "nodejs",
  "categories": ["JavaScript", "Node"]
}

node-ci.yml:

name: Node.js CI

on:
  push:
    branches: [$default-branch]
  pull_request:
    branches: [$default-branch]

jobs:
  build:
    uses: org/.github/.github/workflows/reusable-node-ci.yml@main
    with:
      node-version: '20'
      run-lint: true
      run-tests: true

制限事項と考慮点

ネストの深さ

Reusable Workflowsは最大4レベルまでネストできます:

flowchart TB
    A["呼び出し元ワークフロー"] --> B["Reusable 1"]
    B --> C["Reusable 2"]
    C --> D["Reusable 3"]
    D --> E["Reusable 4"]
    E -.->|"許可されない"| F["Reusable 5"]

    style E fill:#f59e0b,color:#fff
    style F fill:#ef4444,color:#fff

その他の制限

制限 説明
最大ネスト 4レベルまで
環境変数 呼び出し元のジョブレベルでは設定不可
strategy/matrix 呼び出し元のmatrixをreusable内で使用不可
concurrency 各ワークフローレベルで個別

回避策

matrix値をinputsとして渡す:

# 呼び出し元
jobs:
  test:
    strategy:
      matrix:
        node: [18, 20, 22]
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: ${{ matrix.node }}

ベストプラクティス

1. ワークフローをバージョン管理

# セマンティックバージョニングタグを使用
uses: owner/repo/.github/workflows/build.yml@v1.2.0

# または不変性のためにSHA
uses: owner/repo/.github/workflows/build.yml@abc123

2. 徹底したドキュメント

name: Reusable Build

# 詳細な説明を追加
# このワークフローは、異なる環境向けにカスタマイズ可能な
# 設定でNode.jsアプリケーションをビルドします。

on:
  workflow_call:
    inputs:
      node-version:
        description: |
          使用するNode.jsバージョン。サポート:
          - LTSバージョン (18, 20)
          - 現行バージョン (22)
        type: string
        default: '20'

3. 妥当なデフォルト値を提供

inputs:
  node-version:
    default: '20'  # 最も一般的なLTS
  run-tests:
    default: true  # 安全なデフォルト
  deploy:
    default: false  # 危険な操作はデフォルトでオフ

4. 入力を検証

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate environment
        if: inputs.environment != 'staging' && inputs.environment != 'production'
        run: |
          echo "Invalid environment: ${{ inputs.environment }}"
          exit 1

まとめ

機能 説明
workflow_call Reusable Workflow用のトリガー
inputs カスタマイズ可能なパラメータ
secrets 安全な資格情報の受け渡し
outputs 呼び出し元への戻り値
secrets: inherit 呼び出し元のすべてのシークレットを渡す

Reusable Workflowsは、組織のCI/CDパイプライン全体で一貫性を維持し、重複を削減するのに役立ちます。

参考資料

  • O'Reilly - Learning GitHub Actions, Chapter 12
  • Packt - GitHub Actions Cookbook, Chapter 5
  • GitHub Docs - Reusing Workflows