T-CREATOR

GitHub Actions のジョブ分割設計:needs と outputs でデータを安全に受け渡す

GitHub Actions のジョブ分割設計:needs と outputs でデータを安全に受け渡す

GitHub Actions でワークフローを構築する際、複雑な処理を単一のジョブで実行するのではなく、複数のジョブに分割したいケースは多いですよね。たとえば、ビルド結果を複数の環境でテストしたい場合や、デプロイ前に複数の検証ステップを順番に実行したい場合などです。

しかし、ジョブを分割すると「前のジョブで生成したデータを次のジョブで使いたい」という課題が出てきます。この記事では、GitHub Actions の needsoutputs を使って、ジョブ間で安全にデータを受け渡す方法を詳しく解説します。実践的なコード例とともに、ジョブ分割設計のベストプラクティスをご紹介しますね。

背景

CI/CD パイプラインにおけるジョブ分割の重要性

現代のソフトウェア開発では、CI/CD パイプラインが複雑化しています。単一のジョブで全ての処理を行うと、以下のような問題が発生するでしょう。

まず、処理時間が長くなり、フィードバックが遅れてしまいます。次に、エラーが発生した際にどの処理で失敗したのか特定しづらくなりますね。さらに、並列化できる処理があっても順次実行されるため、全体の実行時間が無駄に長くなってしまうのです。

以下の図は、CI/CD パイプラインにおけるジョブ分割の基本的な考え方を示しています。

mermaidflowchart TD
  start["コードプッシュ"] --> build["ビルド<br/>ジョブ"]
  build --> test1["単体テスト<br/>ジョブ"]
  build --> test2["統合テスト<br/>ジョブ"]
  build --> lint["Lint チェック<br/>ジョブ"]
  test1 --> deploy["デプロイ<br/>ジョブ"]
  test2 --> deploy
  lint --> deploy
  deploy --> done["完了"]

この図から分かるように、ビルド後のテストや検証は並列実行できますが、デプロイは全てのテストが成功してから実行する必要があります。

ジョブ分割がもたらすメリット

ジョブを適切に分割すると、以下のようなメリットが得られます。

#メリット説明
1並列実行による高速化依存関係のないジョブを同時実行し、全体の処理時間を短縮できます
2再実行の効率化失敗したジョブだけを再実行でき、成功したジョブの結果を再利用できます
3責任の明確化各ジョブの役割が明確になり、メンテナンスしやすくなります
4デバッグの容易さどのステップで問題が発生したのか一目で分かるようになります
5リソースの最適化ジョブごとに異なる実行環境を選択でき、コストを最適化できます

これらのメリットを最大限に活かすには、ジョブ間でデータを安全かつ効率的に受け渡す仕組みが不可欠なのです。

課題

ジョブ間でのデータ共有の難しさ

GitHub Actions では、各ジョブは独立した仮想環境(ランナー)で実行されます。つまり、ジョブ A で生成したファイルやデータは、デフォルトではジョブ B からアクセスできません。

以下の図は、ジョブが独立した環境で実行される様子を示しています。

mermaidflowchart LR
  subgraph runner1["ランナー 1"]
    jobA["ジョブ A<br/>バージョン番号<br/>を生成"]
  end

  subgraph runner2["ランナー 2"]
    jobB["ジョブ B<br/>バージョン番号<br/>を使いたい"]
  end

  jobA -.->|"データが<br/>渡らない"| jobB

この図から、ジョブ間で直接データを共有できない課題が見て取れますね。

よくある間違ったアプローチ

ジョブ間でデータを共有しようとして、以下のような間違ったアプローチを取ってしまうケースがあります。

環境変数の誤用

ジョブ内で設定した環境変数は、そのジョブ内でしか有効ではありません。以下のコードは動作しません。

yamljobs:
  job-a:
    runs-on: ubuntu-latest
    steps:
      # このenv設定は他のジョブには影響しない
      - name: 環境変数を設定
        run: echo "VERSION=1.2.3" >> $GITHUB_ENV

ファイルシステムへの依存

各ジョブは異なるランナーで実行されるため、ファイルに書き込んでも他のジョブからは読み取れません。

yamljobs:
  job-a:
    runs-on: ubuntu-latest
    steps:
      # このファイルは他のジョブからアクセスできない
      - name: ファイルに書き込み
        run: echo "1.2.3" > version.txt

データ受け渡しに必要な要件

ジョブ間でデータを受け渡すには、以下の要件を満たす必要があります。

| # | 要件 | 重要度 | | --- | ------------------ | ---------------------------------------------- | --- | | 1 | データの永続化 | ジョブが終了してもデータが保持される仕組み | ★★★ | | 2 | 安全なアクセス制御 | 意図したジョブだけがデータにアクセスできること | ★★★ | | 3 | 型安全性 | データの形式が保証されていること | ★★☆ | | 4 | シンプルな API | 簡単に使える記法であること | ★★★ | | 5 | パフォーマンス | データの受け渡しがボトルネックにならないこと | ★★☆ |

これらの要件を満たす解決策が、GitHub Actions の needsoutputs なのです。

解決策

needs キーワードによる依存関係の定義

needs キーワードを使うと、ジョブ間の依存関係を明示的に定義できます。これにより、特定のジョブが完了してから次のジョブを実行する順序制御が可能になりますね。

基本的な needs の構文は以下のとおりです。

yamljobs:
  first-job:
    runs-on: ubuntu-latest
    steps:
      - name: 最初のジョブ
        run: echo "First job running"

続けて、依存関係を定義するコードです。

yamlsecond-job:
  # first-jobが成功してから実行される
  needs: first-job
  runs-on: ubuntu-latest
  steps:
    - name: 2番目のジョブ
      run: echo "Second job running"

複数のジョブに依存する場合は、配列形式で指定できます。

yamlfinal-job:
  # test-job と lint-job の両方が成功してから実行
  needs: [test-job, lint-job]
  runs-on: ubuntu-latest
  steps:
    - name: 最終ジョブ
      run: echo "All tests passed"

outputs による安全なデータの受け渡し

outputs を使うと、ジョブから他のジョブへ文字列データを安全に渡せます。outputs は GitHub Actions のメタデータとして保存され、needs で依存関係を定義したジョブからアクセスできるのです。

outputs の基本構文は以下のようになります。

yamljobs:
  produce-data:
    runs-on: ubuntu-latest
    # ジョブレベルでoutputsを定義
    outputs:
      version: ${{ steps.get-version.outputs.version }}
      build-time: ${{ steps.get-time.outputs.time }}

ステップでデータを生成し、outputs に設定するコードです。

yamlsteps:
  - name: バージョン番号を取得
    id: get-version
    run: |
      VERSION=$(date +%Y.%m.%d)
      echo "version=$VERSION" >> $GITHUB_OUTPUT

続けて、時刻情報を取得するステップです。

yaml- name: ビルド時刻を取得
  id: get-time
  run: |
    TIME=$(date +%H:%M:%S)
    echo "time=$TIME" >> $GITHUB_OUTPUT

別のジョブから outputs にアクセスするコードは以下のとおりです。

yamlconsume-data:
  needs: produce-data
  runs-on: ubuntu-latest
  steps:
    - name: データを使用
      run: |
        echo "Version: ${{ needs.produce-data.outputs.version }}"
        echo "Build Time: ${{ needs.produce-data.outputs.build-time }}"

outputs の仕組みと制約

outputs は GitHub Actions の内部メタデータとして保存されます。以下の図は、outputs がどのように保存され、アクセスされるかを示しています。

mermaidsequenceDiagram
  participant JobA as ジョブ A
  participant Meta as GitHub<br/>メタデータ
  participant JobB as ジョブ B

  JobA->>JobA: ステップでデータ生成
  JobA->>Meta: GITHUB_OUTPUT に<br/>書き込み
  Meta->>Meta: outputs として保存
  JobB->>Meta: needs 経由で<br/>アクセス
  Meta->>JobB: データを取得

outputs には以下のような制約があることを理解しておきましょう。

#制約事項詳細
1データ型文字列のみ(JSON 文字列として複雑なデータも扱える)
2サイズ制限1 つの output は 1 MB まで
3アクセス範囲needs で依存関係を定義したジョブからのみアクセス可能
4数の制限1 つのジョブにつき 256 個まで
5命名規則英数字、ハイフン、アンダースコアのみ使用可能

これらの制約を理解した上で使うことが、安全なデータ受け渡しの鍵となります。

GITHUB_OUTPUT の使い方

GitHub Actions では、$GITHUB_OUTPUT 環境変数を使ってステップの outputs を設定します。以前は set-output コマンドが使われていましたが、セキュリティ上の理由から非推奨になりました。

現在推奨される方法は以下のとおりです。

bash#!/bin/bash
# シェルスクリプトでoutputsを設定

# 単純な値の設定
echo "result=success" >> $GITHUB_OUTPUT

# 変数を使った設定
VERSION="1.2.3"
echo "version=$VERSION" >> $GITHUB_OUTPUT

複数行の値を設定する場合は、デリミタを使います。

bash#!/bin/bash
# 複数行のデータをoutputsに設定

echo "logs<<EOF" >> $GITHUB_OUTPUT
echo "Line 1 of log" >> $GITHUB_OUTPUT
echo "Line 2 of log" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

JavaScript や TypeScript のアクションからも設定できます。

typescript// TypeScriptでoutputsを設定
import * as core from '@actions/core';

// 値を設定
core.setOutput('status', 'completed');
core.setOutput('count', '42');

具体例

例 1:ビルド情報を複数のジョブで共有

実際のプロジェクトでよくあるユースケースとして、ビルド情報を生成して複数のテストジョブで使用する例を見てみましょう。

まず、ビルド情報を生成するジョブを定義します。

yamlname: ビルドとテスト

on:
  push:
    branches: [main, develop]

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

続けて、各情報を生成するステップを定義します。

yamlsteps:
  - name: コードをチェックアウト
    uses: actions/checkout@v4

  - name: バージョン番号を生成
    id: version
    run: |
      VERSION=$(date +%Y.%m.%d)-${{ github.run_number }}
      echo "value=$VERSION" >> $GITHUB_OUTPUT

アーティファクト名と短縮 SHA を生成するステップです。

yaml- name: アーティファクト名を生成
  id: artifact
  run: |
    NAME="build-${{ steps.version.outputs.value }}"
    echo "name=$NAME" >> $GITHUB_OUTPUT

- name: 短縮 SHA を取得
  id: sha
  run: |
    SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
    echo "short=$SHORT_SHA" >> $GITHUB_OUTPUT

ビルド情報を使って単体テストを実行するジョブは以下のようになります。

yamlunit-test:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: ビルド情報を表示
      run: |
        echo "Version: ${{ needs.build.outputs.version }}"
        echo "Artifact: ${{ needs.build.outputs.artifact-name }}"
        echo "SHA: ${{ needs.build.outputs.sha-short }}"

統合テストジョブも同様にビルド情報を参照できます。

yamlintegration-test:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: テスト環境を構築
      run: |
        echo "Testing version: ${{ needs.build.outputs.version }}"
        # ビルド情報を使ってテスト環境をセットアップ

例 2:条件付きデプロイメント

テスト結果に基づいて、デプロイを実行するかどうかを決定する例です。以下の図は、このワークフローの全体像を示しています。

mermaidflowchart TD
  test["テストジョブ"] --> check{"全テスト<br/>成功?"}
  check -->|"success"| deploy["本番デプロイ"]
  check -->|"failure"| skip["デプロイ<br/>スキップ"]
  deploy --> notify["通知"]
  skip --> notify

テスト実行と結果の出力を行うジョブです。

yamljobs:
  test:
    runs-on: ubuntu-latest
    outputs:
      test-result: ${{ steps.test.outputs.result }}
      test-coverage: ${{ steps.test.outputs.coverage }}
    steps:
      - name: テストを実行
        id: test
        run: |
          # テスト実行(例)
          yarn test --coverage

          # 結果を判定
          if [ $? -eq 0 ]; then
            echo "result=success" >> $GITHUB_OUTPUT
          else
            echo "result=failure" >> $GITHUB_OUTPUT
          fi

カバレッジ情報を取得するステップです。

yaml- name: カバレッジを取得
  run: |
    COVERAGE=$(cat coverage/coverage-summary.json | \
      jq -r '.total.lines.pct')
    echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT

テスト結果に基づいてデプロイを実行するジョブは以下のとおりです。

yamldeploy:
  needs: test
  # テストが成功した場合のみ実行
  if: needs.test.outputs.test-result == 'success'
  runs-on: ubuntu-latest
  steps:
    - name: デプロイ実行
      run: |
        echo "Deploying with coverage: ${{ needs.test.outputs.test-coverage }}%"
        # デプロイ処理

例 3:マトリックスビルドの結果集約

複数の環境でテストを実行し、全ての結果を集約する実践的な例を見てみましょう。

マトリックス戦略でテストを実行するジョブです。

yamljobs:
  test-matrix:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
    outputs:
      # マトリックスジョブはoutputsを直接使えない
      test-status: ${{ steps.test.outputs.status }}

各環境でテストを実行するステップです。

yamlsteps:
  - name: Node.js セットアップ
    uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}

  - name: テスト実行
    id: test
    run: |
      yarn install
      yarn test
      echo "status=passed" >> $GITHUB_OUTPUT

マトリックスジョブの結果を集約する専用ジョブを定義します。

yamlaggregate-results:
  needs: test-matrix
  runs-on: ubuntu-latest
  outputs:
    all-passed: ${{ steps.check.outputs.result }}
  steps:
    - name: 全結果をチェック
      id: check
      run: |
        # 全てのマトリックスジョブが成功したか確認
        echo "result=true" >> $GITHUB_OUTPUT

例 4:JSON データの受け渡し

複雑なデータ構造を outputs で受け渡す場合は、JSON 文字列を使用します。

JSON データを生成して outputs に設定するジョブです。

yamljobs:
  generate-config:
    runs-on: ubuntu-latest
    outputs:
      config: ${{ steps.create-config.outputs.json }}
    steps:
      - name: 設定JSONを生成
        id: create-config
        run: |
          CONFIG=$(cat <<EOF
          {
            "environment": "production",
            "region": "us-east-1",
            "replicas": 3,
            "features": ["feature-a", "feature-b"]
          }
          EOF
          )

JSON を outputs に設定する処理です。

yaml# 改行を削除してJSON文字列化
CONFIG_JSON=$(echo $CONFIG | jq -c .)
echo "json=$CONFIG_JSON" >> $GITHUB_OUTPUT

JSON データを受け取って使用するジョブは以下のとおりです。

yamluse-config:
  needs: generate-config
  runs-on: ubuntu-latest
  steps:
    - name: 設定を解析して使用
      run: |
        CONFIG='${{ needs.generate-config.outputs.config }}'

        # jqで値を取り出す
        ENV=$(echo $CONFIG | jq -r '.environment')
        REGION=$(echo $CONFIG | jq -r '.region')
        REPLICAS=$(echo $CONFIG | jq -r '.replicas')

        echo "Environment: $ENV"
        echo "Region: $REGION"
        echo "Replicas: $REPLICAS"

例 5:動的なジョブ生成

outputs を使って、実行時に動的にジョブの挙動を変更する高度な例です。

変更されたファイルを検出して、必要なジョブを判定します。

yamljobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      frontend-changed: ${{ steps.changes.outputs.frontend }}
      backend-changed: ${{ steps.changes.outputs.backend }}
      docs-changed: ${{ steps.changes.outputs.docs }}

ファイル変更を検出するステップです。

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

  - name: 変更ファイルを検出
    id: changes
    run: |
      # フロントエンドの変更チェック
      git diff --name-only HEAD^ HEAD | grep -q "^frontend/" && \
        echo "frontend=true" >> $GITHUB_OUTPUT || \
        echo "frontend=false" >> $GITHUB_OUTPUT

バックエンドとドキュメントの変更をチェックします。

yaml          # バックエンドの変更チェック
          git diff --name-only HEAD^ HEAD | grep -q "^backend/" && \
            echo "backend=true" >> $GITHUB_OUTPUT || \
            echo "backend=false" >> $GITHUB_OUTPUT

          # ドキュメントの変更チェック
          git diff --name-only HEAD^ HEAD | grep -q "^docs/" && \
            echo "docs=true" >> $GITHUB_OUTPUT || \
            echo "docs=false" >> $GITHUB_OUTPUT

フロントエンドに変更があった場合のみテストを実行するジョブです。

yamltest-frontend:
  needs: detect-changes
  if: needs.detect-changes.outputs.frontend-changed == 'true'
  runs-on: ubuntu-latest
  steps:
    - name: フロントエンドテスト
      run: |
        echo "Running frontend tests..."
        cd frontend && yarn test

バックエンドに変更があった場合のみテストを実行します。

yamltest-backend:
  needs: detect-changes
  if: needs.detect-changes.outputs.backend-changed == 'true'
  runs-on: ubuntu-latest
  steps:
    - name: バックエンドテスト
      run: |
        echo "Running backend tests..."
        cd backend && yarn test

ベストプラクティス

ジョブ分割と outputs を使う際のベストプラクティスをまとめます。

#プラクティス理由
1outputs には小さなデータのみサイズ制限(1MB)があり、大きなデータはアーティファクトを使用
2明確な命名規則build-version のように用途が分かる名前を使用
3JSON で複雑なデータを扱う構造化データは JSON 文字列として受け渡す
4エラーハンドリングを実装outputs 生成時のエラーを適切に処理
5ドキュメント化各 output の用途と形式をコメントで記載

以下は、これらのベストプラクティスを適用した例です。

yamljobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      # ビルドバージョン(形式: YYYY.MM.DD-RUN_NUMBER)
      build-version: ${{ steps.version.outputs.value }}
      # デプロイ可能かどうか(true/false)
      deployable: ${{ steps.validate.outputs.ready }}
      # ビルド統計情報(JSON文字列)
      build-stats: ${{ steps.stats.outputs.json }}

まとめ

GitHub Actions の needsoutputs を使ったジョブ分割設計について、詳しく解説してきました。重要なポイントを振り返ってみましょう。

まず、ジョブを適切に分割することで、並列実行による高速化、再実行の効率化、責任の明確化といったメリットが得られます。しかし、各ジョブは独立した環境で実行されるため、データの受け渡しには工夫が必要でした。

needs キーワードを使うことで、ジョブ間の依存関係を明示的に定義できます。これにより、特定のジョブが完了してから次のジョブを実行する順序制御が可能になりますね。

outputs を使うと、ジョブから他のジョブへ文字列データを安全に渡せます。$GITHUB_OUTPUT に書き込むことで、GitHub Actions のメタデータとしてデータが保存され、needs で依存関係を定義したジョブからアクセスできるのです。

実践例では、ビルド情報の共有、条件付きデプロイ、マトリックスビルドの結果集約、JSON データの受け渡し、動的なジョブ生成など、様々なユースケースを見てきました。これらのパターンを組み合わせることで、複雑な CI/CD パイプラインも整理された形で実装できるでしょう。

outputs には 1MB のサイズ制限があることや、文字列のみを扱えることなど、いくつかの制約があります。しかし、これらの制約を理解した上で適切に使えば、安全で保守性の高いワークフローを構築できますね。

ぜひ、皆さんのプロジェクトでも needsoutputs を活用して、効率的な CI/CD パイプラインを構築してみてください。ジョブを適切に分割し、データを安全に受け渡すことで、開発者体験が大きく向上することでしょう。

関連リンク