T-CREATOR

GitHub Actions が突然失敗するときの切り分け術:ログレベル・re-run・debug secrets

GitHub Actions が突然失敗するときの切り分け術:ログレベル・re-run・debug secrets

GitHub Actions の CI/CD パイプラインが突然失敗したとき、原因の特定に時間を要してしまい、開発チーム全体の作業が止まってしまった経験はありませんか。昨日まで正常に動作していたワークフローが、何の変更もないはずなのに急に失敗し始める。そんな予期しない問題に直面したとき、効率的に原因を突き止めるための切り分け術が必要になります。

本記事では、GitHub Actions で発生する突然の失敗に対して、ログレベルの調整、re-run 機能、debug secrets という 3 つの強力な武器を駆使した段階的な切り分け手法をご紹介します。これらの手法を体系的に活用することで、問題の根本原因を迅速に特定し、開発フローの停滞を最小限に抑えることができるでしょう。

失敗の典型パターンと初期対応

GitHub Actions の失敗には、いくつかの典型的なパターンが存在します。まずはこれらのパターンを理解し、初期対応の方針を立てることが重要です。

よくある失敗パターンの分類

GitHub Actions の失敗は、大きく以下の 4 つのカテゴリに分類できます。

mermaidflowchart TD
    failure[GitHub Actions 失敗] --> env[環境・依存関係問題]
    failure --> timing[タイミング・競合問題]
    failure --> config[設定・権限問題]
    failure --> infra[インフラ・リソース問題]

    env --> env1[依存関係の更新]
    env --> env2[Node.js バージョン変更]
    env --> env3[パッケージの非互換]

    timing --> timing1[ネットワーク遅延]
    timing --> timing2[外部API制限]
    timing --> timing3[並行実行の競合]

    config --> config1[Secrets の有効期限]
    config --> config2[権限設定の変更]
    config --> config3[環境変数の不整合]

    infra --> infra1[Runner リソース不足]
    infra --> infra2[GitHub サービス障害]
    infra --> infra3[外部サービス障害]

この分類により、失敗の性質を素早く判断し、適切な調査手法を選択できます。

各カテゴリには以下のような特徴があります。

カテゴリ特徴発生頻度調査優先度
環境・依存関係問題再現性が高い、特定の条件で必ず発生最優先
タイミング・競合問題間欠的発生、re-run で成功することが多い中優先
設定・権限問題突然発生、全てのジョブで影響高優先
インフラ・リソース問題外部要因、GitHub Status で確認可能低優先

まず確認すべき基本項目

失敗が発生した際の初期チェックリストです。これらの確認により、簡単に解決できる問題を早期に発見できます。

typescript// 基本的な確認項目のチェックリスト
interface BasicCheckList {
  // 1. GitHub Status の確認
  githubStatus: 'operational' | 'degraded' | 'down';

  // 2. 最近のコミット変更
  recentChanges: {
    workflowFiles: boolean;
    dependencies: boolean;
    configFiles: boolean;
  };

  // 3. Secrets と環境変数
  secretsStatus: {
    expiration: Date | null;
    lastUpdate: Date;
    accessPermissions: boolean;
  };

  // 4. Runner の状態
  runnerStatus: {
    type:
      | 'ubuntu-latest'
      | 'windows-latest'
      | 'macos-latest';
    resourceUsage: 'normal' | 'high' | 'exceeded';
  };
}

基本確認の実装例です。

yaml# .github/workflows/health-check.yml
name: Health Check
on:
  workflow_dispatch:
  schedule:
    - cron: '0 */6 * * *' # 6時間ごとに実行

jobs:
  health-check:
    runs-on: ubuntu-latest
    steps:
      - name: Check GitHub Status
        run: |
          curl -s https://www.githubstatus.com/api/v2/status.json | \
          jq '.status.indicator'

      - name: Verify Secrets Access
        env:
          TEST_SECRET: ${{ secrets.GITHUB_TOKEN }}
        run: |
          if [ -z "$TEST_SECRET" ]; then
            echo "::error::Secrets access failed"
            exit 1
          fi
          echo "Secrets access: OK"

これらの基本確認により、約 70%の問題は初期段階で原因を特定できます。

段階的切り分け手順

効率的な問題解決のため、以下の 3 段階のアプローチで切り分けを実施します。各段階で得られる情報を基に、次のステップを決定していきましょう。

Step1: ログレベルの調整と詳細確認

最初のステップでは、デフォルトのログでは見えない詳細情報を取得します。GitHub Actions では、2 つの debug ログレベルを活用できます。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant Repo as リポジトリ
    participant Runner as GitHub Runner
    participant Log as ログシステム

    Dev ->> Repo: debug secrets を設定
    Dev ->> Runner: ワークフロー実行
    Runner ->> Log: RUNNER_DEBUG ログ出力
    Runner ->> Log: STEP_DEBUG ログ出力
    Log ->> Dev: 詳細ログ確認
    Dev ->> Dev: 問題箇所の特定

段階的にログレベルを上げていく方法をご紹介します。

yaml# デバッグ用の環境変数設定例
name: Debug Workflow
on: [push, pull_request]

env:
  # Runner レベルのデバッグ情報を有効化
  ACTIONS_RUNNER_DEBUG: true
  # Step レベルの詳細デバッグ情報を有効化
  ACTIONS_STEP_DEBUG: true

jobs:
  debug-job:
    runs-on: ubuntu-latest
    steps:
      - name: Enable verbose logging
        run: |
          echo "::notice::Debug mode enabled"
          echo "Runner debug: $ACTIONS_RUNNER_DEBUG"
          echo "Step debug: $ACTIONS_STEP_DEBUG"

カスタムログ出力の実装方法です。

typescript// カスタムログ関数の実装
class GitHubActionsLogger {
  static info(message: string, data?: any): void {
    console.log(`::notice::${message}`);
    if (data) {
      console.log(JSON.stringify(data, null, 2));
    }
  }

  static error(
    message: string,
    file?: string,
    line?: number
  ): void {
    const location = file
      ? `file=${file},line=${line}`
      : '';
    console.log(`::error ${location}::${message}`);
  }

  static debug(message: string, context?: any): void {
    if (process.env.ACTIONS_STEP_DEBUG === 'true') {
      console.log(`::debug::${message}`);
      if (context) {
        console.log(
          `::debug::Context: ${JSON.stringify(context)}`
        );
      }
    }
  }
}

Step2: re-run による一時的問題の排除

re-run 機能を活用して、一時的な問題と恒久的な問題を区別します。再実行のパターンから問題の性質を判断できます。

yaml# re-run パターンの分析用ステップ
- name: Record execution attempt
  run: |
    # 実行回数をカウント
    ATTEMPT_COUNT=$(echo "${{ github.run_attempt }}" || echo "1")
    echo "Execution attempt: $ATTEMPT_COUNT"

    # 前回失敗の記録確認
    if [ "$ATTEMPT_COUNT" -gt "1" ]; then
      echo "::warning::This is a re-run (attempt #$ATTEMPT_COUNT)"
      echo "::notice::Checking for intermittent issues"
    fi

re-run の戦略的な活用方法です。

bash# GitHub CLI を使用した re-run コマンド例
# 失敗したジョブのみ再実行
gh run rerun $RUN_ID --failed

# 全体を再実行
gh run rerun $RUN_ID

# 特定のジョブのみ再実行
gh run rerun $RUN_ID --job $JOB_ID

re-run パターン分析のロジックです。

javascript// re-run パターンの分析ロジック
function analyzeRerunPattern(attempts) {
  const patterns = {
    intermittent: attempts.some(
      (a) => a.conclusion === 'success'
    ),
    persistent: attempts.every(
      (a) => a.conclusion === 'failure'
    ),
    degrading: attempts
      .map((a) => a.duration)
      .every((d, i, arr) => i === 0 || d >= arr[i - 1]),
  };

  if (patterns.intermittent) {
    return {
      type: 'timing_issue',
      recommendation:
        'Add retry logic or increase timeouts',
    };
  } else if (patterns.persistent) {
    return {
      type: 'configuration_issue',
      recommendation:
        'Check dependencies and environment setup',
    };
  } else if (patterns.degrading) {
    return {
      type: 'resource_issue',
      recommendation:
        'Optimize resource usage or upgrade runner',
    };
  }
}

Step3: debug secrets を活用した深掘り調査

最終段階では、debug secrets を活用して機密情報を安全に調査します。この手法により、通常は見えない環境変数や設定値を確認できます。

yaml# debug secrets の安全な活用例
- name: Debug environment (secure)
  env:
    # デバッグ用の一時的な出力制御
    DEBUG_MODE: ${{ secrets.DEBUG_MODE || 'false' }}
  run: |
    if [ "$DEBUG_MODE" = "true" ]; then
      echo "::notice::Debug mode activated"
      
      # 環境変数の一部を安全に出力
      echo "NODE_VERSION: $NODE_VERSION"
      echo "RUNNER_OS: $RUNNER_OS"
      echo "GITHUB_REF: $GITHUB_REF"
      
      # Secrets の存在確認(値は出力しない)
      if [ -n "${{ secrets.API_KEY }}" ]; then
        echo "::notice::API_KEY is available (length: ${#API_KEY})"
      else
        echo "::error::API_KEY is not available"
      fi
    fi

セキュアなデバッグ情報の収集方法です。

typescript// セキュアなデバッグ情報収集クラス
class SecureDebugger {
  static logSecretAvailability(
    secretName: string,
    secretValue?: string
  ): void {
    if (secretValue && secretValue.length > 0) {
      const maskedLength = Math.min(secretValue.length, 50);
      console.log(
        `::notice::${secretName} available (${maskedLength} chars)`
      );

      // 最初の2文字と最後の2文字のみ表示(デバッグ用)
      if (
        process.env.ACTIONS_STEP_DEBUG === 'true' &&
        secretValue.length > 4
      ) {
        const masked =
          secretValue.substring(0, 2) +
          '*'.repeat(Math.max(0, secretValue.length - 4)) +
          secretValue.substring(secretValue.length - 2);
        console.log(
          `::debug::${secretName} pattern: ${masked}`
        );
      }
    } else {
      console.log(
        `::error::${secretName} is not available or empty`
      );
    }
  }

  static validateEnvironment(): void {
    const requiredVars = [
      'NODE_ENV',
      'API_ENDPOINT',
      'VERSION',
    ];
    const missingVars = requiredVars.filter(
      (v) => !process.env[v]
    );

    if (missingVars.length > 0) {
      console.log(
        `::error::Missing environment variables: ${missingVars.join(
          ', '
        )}`
      );
    } else {
      console.log(
        '::notice::All required environment variables are present'
      );
    }
  }
}

各手法の具体的な実装

3 つの切り分け手法について、より詳細な実装方法と最適化のポイントをご説明します。

ログレベル設定の最適化

効果的なログ設定により、問題解決に必要な情報を効率的に収集できます。

yaml# 段階的ログレベル設定の例
name: Optimized Logging
on:
  workflow_dispatch:
    inputs:
      debug_level:
        description: 'Debug level (basic/advanced/full)'
        required: false
        default: 'basic'

jobs:
  logging-demo:
    runs-on: ubuntu-latest
    steps:
      - name: Set debug environment
        run: |
          case "${{ github.event.inputs.debug_level }}" in
            "basic")
              echo "ACTIONS_RUNNER_DEBUG=false" >> $GITHUB_ENV
              echo "ACTIONS_STEP_DEBUG=false" >> $GITHUB_ENV
              echo "CUSTOM_DEBUG=false" >> $GITHUB_ENV
              ;;
            "advanced") 
              echo "ACTIONS_RUNNER_DEBUG=true" >> $GITHUB_ENV
              echo "ACTIONS_STEP_DEBUG=false" >> $GITHUB_ENV
              echo "CUSTOM_DEBUG=true" >> $GITHUB_ENV
              ;;
            "full")
              echo "ACTIONS_RUNNER_DEBUG=true" >> $GITHUB_ENV
              echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV
              echo "CUSTOM_DEBUG=true" >> $GITHUB_ENV
              ;;
          esac

カスタムログ関数の拡張実装です。

typescript// 拡張ログ機能の実装
interface LogContext {
  step: string;
  job: string;
  workflow: string;
  timestamp: string;
}

class EnhancedLogger {
  private static context: LogContext;

  static setContext(context: Partial<LogContext>): void {
    this.context = {
      step: context.step || 'unknown',
      job:
        context.job || process.env.GITHUB_JOB || 'unknown',
      workflow:
        context.workflow ||
        process.env.GITHUB_WORKFLOW ||
        'unknown',
      timestamp: new Date().toISOString(),
    };
  }

  static logWithContext(
    level: 'info' | 'warning' | 'error',
    message: string,
    data?: any
  ): void {
    const contextStr = `[${this.context.workflow}/${this.context.job}/${this.context.step}]`;
    const timestamp = new Date().toISOString();

    switch (level) {
      case 'info':
        console.log(`::notice::${contextStr} ${message}`);
        break;
      case 'warning':
        console.log(`::warning::${contextStr} ${message}`);
        break;
      case 'error':
        console.log(`::error::${contextStr} ${message}`);
        break;
    }

    if (data && process.env.CUSTOM_DEBUG === 'true') {
      console.log(
        `::debug::${contextStr} Data: ${JSON.stringify(
          data,
          null,
          2
        )}`
      );
    }
  }

  static performance(
    operation: string,
    startTime: number
  ): void {
    const duration = Date.now() - startTime;
    const level = duration > 30000 ? 'warning' : 'info';

    console.log(
      `::${level}::Performance: ${operation} completed in ${duration}ms`
    );

    if (duration > 60000) {
      console.log(
        `::error::Performance: ${operation} exceeded 60s timeout threshold`
      );
    }
  }
}

re-run オプションの使い分け

re-run 機能を戦略的に活用するための判断基準と実装方法をご紹介します。

mermaidflowchart TD
    start[ワークフロー失敗] --> check{失敗の性質確認}
    check -->|特定ステップのみ失敗| single[単一ジョブ re-run]
    check -->|複数ジョブ失敗| multiple[全体 re-run]
    check -->|環境依存| env[新環境で re-run]

    single --> analyze1[失敗パターン分析]
    multiple --> analyze2[依存関係確認]
    env --> analyze3[環境差分確認]

    analyze1 --> decision1{成功率判定}
    analyze2 --> decision2{影響範囲判定}
    analyze3 --> decision3{環境問題判定}

    decision1 -->|50%以上成功| intermittent[間欠的問題として記録]
    decision1 -->|全て失敗| persistent[恒久的問題として調査]

    decision2 -->|局所的影響| isolate[問題の隔離]
    decision2 -->|広範囲影響| escalate[エスカレーション]

    decision3 -->|環境固有| config[設定見直し]
    decision3 -->|共通問題| infra[インフラ調査]

re-run 戦略の自動化実装です。

typescript// re-run 戦略の自動判定システム
interface RunAnalysis {
  runId: string;
  attempts: number;
  successRate: number;
  failurePattern:
    | 'consistent'
    | 'intermittent'
    | 'degrading';
  affectedJobs: string[];
}

class RerunStrategy {
  static analyzeFailurePattern(runs: any[]): RunAnalysis {
    const attempts = runs.length;
    const successCount = runs.filter(
      (r) => r.conclusion === 'success'
    ).length;
    const successRate = successCount / attempts;

    let failurePattern:
      | 'consistent'
      | 'intermittent'
      | 'degrading';

    if (successRate === 0) {
      failurePattern = 'consistent';
    } else if (successRate > 0 && successRate < 1) {
      failurePattern = 'intermittent';
    } else {
      // 成功率が悪化している場合
      const recentRuns = runs.slice(-3);
      const recentSuccessRate =
        recentRuns.filter((r) => r.conclusion === 'success')
          .length / recentRuns.length;
      failurePattern =
        recentSuccessRate < successRate
          ? 'degrading'
          : 'intermittent';
    }

    return {
      runId: runs[0].id,
      attempts,
      successRate,
      failurePattern,
      affectedJobs: this.extractAffectedJobs(runs),
    };
  }

  static recommendAction(analysis: RunAnalysis): string {
    if (analysis.failurePattern === 'consistent') {
      return 'deep_investigation';
    } else if (analysis.failurePattern === 'intermittent') {
      if (analysis.successRate > 0.7) {
        return 'add_retry_logic';
      } else {
        return 'investigate_timing_issues';
      }
    } else {
      return 'check_resource_constraints';
    }
  }

  private static extractAffectedJobs(
    runs: any[]
  ): string[] {
    const jobFailures = new Map<string, number>();

    runs.forEach((run) => {
      run.jobs?.forEach((job: any) => {
        if (job.conclusion === 'failure') {
          jobFailures.set(
            job.name,
            (jobFailures.get(job.name) || 0) + 1
          );
        }
      });
    });

    // 50%以上失敗しているジョブを抽出
    return Array.from(jobFailures.entries())
      .filter(([_, count]) => count / runs.length >= 0.5)
      .map(([name, _]) => name);
  }
}

debug secrets の安全な活用法

機密情報を漏洩させることなく、効果的なデバッグを実現する方法をご説明します。

yaml# セキュアなデバッグ設定の例
name: Secure Debug
on:
  workflow_dispatch:
    inputs:
      enable_debug:
        description: 'Enable secure debugging'
        type: boolean
        default: false

jobs:
  secure-debug:
    runs-on: ubuntu-latest
    if: github.event.inputs.enable_debug == 'true'
    environment: debug # 専用環境で実行

    steps:
      - name: Validate debug permissions
        run: |
          # デバッグ実行者の権限確認
          if [[ "${{ github.actor }}" != "admin-user" ]] && [[ "${{ github.actor }}" != "debug-user" ]]; then
            echo "::error::Unauthorized debug access"
            exit 1
          fi

      - name: Secure environment check
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: |
          # Secrets の存在と形式確認
          echo "Checking secrets availability..."

          # API_KEY の検証
          if [[ -n "$API_KEY" ]]; then
            KEY_LENGTH=${#API_KEY}
            echo "::notice::API_KEY present (length: $KEY_LENGTH)"
            
            # キーの形式確認(最初の文字のみチェック)
            if [[ "$API_KEY" =~ ^[A-Za-z0-9] ]]; then
              echo "::notice::API_KEY format appears valid"
            else
              echo "::warning::API_KEY format may be invalid"
            fi
          else
            echo "::error::API_KEY not found"
          fi

          # DB_PASSWORD の検証
          if [[ -n "$DB_PASSWORD" ]]; then
            PASS_LENGTH=${#DB_PASSWORD}
            echo "::notice::DB_PASSWORD present (length: $PASS_LENGTH)"
          else
            echo "::error::DB_PASSWORD not found"  
          fi

セキュアなデバッグ用のヘルパー関数実装です。

javascript// セキュアデバッグ用ユーティリティ
class SecureDebugUtils {
  /**
   * 文字列の最初と最後の文字以外をマスクする
   */
  static maskSensitiveValue(value, visibleChars = 2) {
    if (!value || value.length <= visibleChars * 2) {
      return '*'.repeat(8); // 短すぎる場合は完全にマスク
    }

    const start = value.substring(0, visibleChars);
    const end = value.substring(
      value.length - visibleChars
    );
    const middle = '*'.repeat(
      Math.max(4, value.length - visibleChars * 2)
    );

    return `${start}${middle}${end}`;
  }

  /**
   * 環境変数の安全な検証
   */
  static validateEnvironmentSecrets(requiredSecrets) {
    const results = {};

    requiredSecrets.forEach((secretName) => {
      const value = process.env[secretName];

      results[secretName] = {
        present: !!value,
        length: value ? value.length : 0,
        format: value
          ? this.detectFormat(value)
          : 'unknown',
        masked: value
          ? this.maskSensitiveValue(value)
          : null,
      };
    });

    return results;
  }

  /**
   * 値の形式を推定(セキュアに)
   */
  static detectFormat(value) {
    const patterns = {
      jwt: /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/,
      apiKey: /^[A-Za-z0-9]{20,}$/,
      base64: /^[A-Za-z0-9+/]+=*$/,
      uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
    };

    for (const [format, pattern] of Object.entries(
      patterns
    )) {
      if (pattern.test(value)) {
        return format;
      }
    }

    return 'unknown';
  }

  /**
   * デバッグ情報の安全な出力
   */
  static safeLog(level, message, sensitiveData = {}) {
    console.log(`::${level}::${message}`);

    if (
      Object.keys(sensitiveData).length > 0 &&
      process.env.ACTIONS_STEP_DEBUG === 'true'
    ) {
      const maskedData = {};

      Object.entries(sensitiveData).forEach(
        ([key, value]) => {
          if (typeof value === 'string') {
            maskedData[key] =
              this.maskSensitiveValue(value);
          } else {
            maskedData[key] = value;
          }
        }
      );

      console.log(
        `::debug::Masked data: ${JSON.stringify(
          maskedData,
          null,
          2
        )}`
      );
    }
  }
}

トラブルシューティング実例

実際のプロジェクトで発生した典型的な問題と、3 つの切り分け手法を活用した解決プロセスをご紹介します。

ケース 1: 依存関係の問題

Node.js プロジェクトで package-lock.json の更新後、テストが間欠的に失敗するようになった事例です。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant GH as GitHub Actions
    participant NPM as npm registry
    participant Test as テストランナー

    Dev ->> GH: package-lock.json 更新をプッシュ
    GH ->> NPM: npm ci 実行
    NPM -->> GH: 依存関係インストール
    GH ->> Test: テスト実行
    Test -->> GH: 間欠的失敗(タイムアウト)

    Note over GH: Step1: ログレベル調整
    GH ->> GH: ACTIONS_STEP_DEBUG=true
    GH ->> Test: 詳細ログ付きテスト実行
    Test -->> GH: 特定パッケージでタイムアウト判明

    Note over Dev: Step2: re-run による検証
    Dev ->> GH: 複数回 re-run 実行
    GH -->> Dev: 成功率 60%(間欠的問題確認)

    Note over Dev: Step3: debug secrets で環境確認
    Dev ->> GH: NODE_VERSION, NPM_CONFIG 確認
    GH -->> Dev: 依存関係の競合状態を発見

Step1: ログレベル調整による詳細確認

yaml# 依存関係問題の詳細調査
- name: Enhanced dependency debugging
  env:
    ACTIONS_STEP_DEBUG: true
    NPM_CONFIG_LOGLEVEL: verbose
  run: |
    echo "::group::Package installation analysis"

    # npm の詳細ログを有効化
    npm config set loglevel verbose

    # インストール前の状態確認
    echo "::notice::Pre-installation check"
    ls -la node_modules/ 2>/dev/null || echo "node_modules not found"

    # 依存関係の段階的インストール
    echo "::notice::Installing dependencies with timing"
    time npm ci --verbose

    # インストール後の検証
    echo "::notice::Post-installation verification"
    npm ls --depth=0

    echo "::endgroup::"

Step2: re-run による問題パターンの特定

bash# 複数回の re-run による分析スクリプト
#!/bin/bash

# GitHub CLI を使用して複数回実行
RUN_ID=$1
ATTEMPTS=5

echo "Analyzing failure pattern for run $RUN_ID"

for i in $(seq 1 $ATTEMPTS); do
  echo "Re-run attempt $i"
  gh run rerun $RUN_ID --failed

  # 実行完了まで待機
  while true; do
    STATUS=$(gh run view $RUN_ID --json status -q '.status')
    if [[ "$STATUS" == "completed" ]]; then
      break
    fi
    sleep 30
  done

  # 結果を記録
  CONCLUSION=$(gh run view $RUN_ID --json conclusion -q '.conclusion')
  echo "Attempt $i: $CONCLUSION"

  # 成功した場合は詳細ログを保存
  if [[ "$CONCLUSION" == "success" ]]; then
    gh run view $RUN_ID --log > "success-attempt-$i.log"
  else
    gh run view $RUN_ID --log > "failure-attempt-$i.log"
  fi
done

Step3: debug secrets による環境要因の調査

yaml- name: Deep environment analysis
  env:
    DEBUG_MODE: ${{ secrets.DEBUG_MODE }}
    NODE_VERSION: ${{ matrix.node-version }}
  run: |
    if [[ "$DEBUG_MODE" == "true" ]]; then
      echo "::group::Environment deep analysis"
      
      # Node.js 環境の詳細確認
      echo "Node.js version: $(node --version)"
      echo "npm version: $(npm --version)"
      echo "npm config:"
      npm config list
      
      # システムリソースの確認
      echo "::notice::System resources"
      echo "Memory: $(free -h | grep '^Mem:' | awk '{print $3 "/" $2}')"
      echo "Disk: $(df -h / | tail -1 | awk '{print $3 "/" $2 " (" $5 ")"}')"
      
      # ネットワーク状況の確認
      echo "::notice::Network connectivity"
      time curl -s -o /dev/null -w "npm registry: %{time_total}s\n" https://registry.npmjs.org/
      
      # 競合プロセスの確認
      echo "::notice::Running processes"
      ps aux | grep -E "(npm|node)" | grep -v grep
      
      echo "::endgroup::"
    fi

問題の解決策

分析の結果、特定のパッケージのインストール時にネットワークタイムアウトが発生していることが判明しました。

yaml# 解決策の実装
- name: Install dependencies with retry
  run: |
    # 複数回リトライする関数
    retry_npm_install() {
      local max_attempts=3
      local delay=10
      
      for attempt in $(seq 1 $max_attempts); do
        echo "npm install attempt $attempt/$max_attempts"
        
        if npm ci --cache ~/.npm --prefer-offline; then
          echo "npm install succeeded on attempt $attempt"
          return 0
        else
          echo "npm install failed on attempt $attempt"
          if [[ $attempt -lt $max_attempts ]]; then
            echo "Retrying in ${delay}s..."
            sleep $delay
            delay=$((delay * 2))  # 指数バックオフ
          fi
        fi
      done
      
      echo "npm install failed after $max_attempts attempts"
      return 1
    }

    # リトライロジックを実行
    retry_npm_install

ケース 2: 環境変数・秘密情報の問題

本番環境で API キーの有効期限が切れた際の対応事例です。外部サービスとの連携が突然失敗し、エラーメッセージからは原因が特定できませんでした。

Step1: 基本ログでの初期調査

yaml- name: API connection test
  env:
    API_ENDPOINT: ${{ secrets.API_ENDPOINT }}
    API_KEY: ${{ secrets.API_KEY }}
  run: |
    echo "::group::API Connection Test"

    # API エンドポイントの疎通確認
    if curl -f -s "$API_ENDPOINT/health" > /dev/null; then
      echo "::notice::API endpoint is reachable"
    else
      echo "::error::API endpoint is not reachable"
      exit 1
    fi

    # 認証テスト(詳細なエラー情報を取得)
    RESPONSE=$(curl -s -w "\n%{http_code}" \
      -H "Authorization: Bearer $API_KEY" \
      "$API_ENDPOINT/auth/test")

    HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
    BODY=$(echo "$RESPONSE" | sed '$d')

    echo "HTTP Status: $HTTP_CODE"

    if [[ "$HTTP_CODE" -eq 200 ]]; then
      echo "::notice::API authentication successful"
    elif [[ "$HTTP_CODE" -eq 401 ]]; then
      echo "::error::API authentication failed - invalid credentials"
    elif [[ "$HTTP_CODE" -eq 403 ]]; then
      echo "::error::API authentication failed - insufficient permissions"
    else
      echo "::error::Unexpected API response: $HTTP_CODE"
      echo "Response body: $BODY"
    fi

    echo "::endgroup::"

Step2: re-run による問題の再現性確認

typescript// re-run 結果の分析スクリプト
interface ApiTestResult {
  attempt: number;
  httpStatus: number;
  responseTime: number;
  errorMessage?: string;
}

class ApiFailureAnalyzer {
  static analyzeResults(results: ApiTestResult[]): {
    issueType: 'authentication' | 'network' | 'service';
    confidence: number;
    recommendation: string;
  } {
    const authErrors = results.filter(
      (r) => r.httpStatus === 401 || r.httpStatus === 403
    );
    const networkErrors = results.filter(
      (r) => r.httpStatus === 0 || r.responseTime > 30000
    );
    const serviceErrors = results.filter(
      (r) => r.httpStatus >= 500
    );

    if (authErrors.length === results.length) {
      return {
        issueType: 'authentication',
        confidence: 0.95,
        recommendation:
          'Check API key validity and permissions',
      };
    } else if (
      networkErrors.length >
      results.length * 0.8
    ) {
      return {
        issueType: 'network',
        confidence: 0.85,
        recommendation:
          'Investigate network connectivity and timeouts',
      };
    } else if (
      serviceErrors.length >
      results.length * 0.5
    ) {
      return {
        issueType: 'service',
        confidence: 0.75,
        recommendation:
          'Contact API service provider - possible outage',
      };
    }

    return {
      issueType: 'authentication',
      confidence: 0.6,
      recommendation:
        'Mixed results - investigate authentication and network',
    };
  }
}

Step3: debug secrets による認証情報の詳細調査

yaml- name: Secure authentication debugging
  env:
    API_KEY: ${{ secrets.API_KEY }}
    DEBUG_AUTH: ${{ secrets.DEBUG_AUTH }}
  run: |
    if [[ "$DEBUG_AUTH" == "true" ]]; then
      echo "::group::Secure Authentication Analysis"
      
      # API キーの基本検証
      if [[ -n "$API_KEY" ]]; then
        KEY_LENGTH=${#API_KEY}
        echo "::notice::API key present (length: $KEY_LENGTH)"
        
        # JWT トークンかどうかの確認
        if [[ "$API_KEY" =~ ^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$ ]]; then
          echo "::notice::API key appears to be JWT format"
          
          # JWT の有効期限確認(ヘッダーとペイロードのデコード)
          HEADER=$(echo "$API_KEY" | cut -d. -f1)
          PAYLOAD=$(echo "$API_KEY" | cut -d. -f2)
          
          # Base64 デコードして expiration 確認
          if command -v jq > /dev/null; then
            EXP=$(echo "$PAYLOAD" | base64 -d 2>/dev/null | jq -r '.exp // empty')
            if [[ -n "$EXP" ]]; then
              CURRENT_TIME=$(date +%s)
              if [[ "$EXP" -lt "$CURRENT_TIME" ]]; then
                echo "::error::JWT token has expired"
                echo "Expired at: $(date -d @$EXP)"
              else
                echo "::notice::JWT token is valid until: $(date -d @$EXP)"
              fi
            fi
          fi
        else
          echo "::notice::API key appears to be standard format"
          
          # 標準 API キーの簡単な形式チェック
          if [[ "$API_KEY" =~ ^[A-Za-z0-9]{32,}$ ]]; then
            echo "::notice::API key format appears valid"
          else
            echo "::warning::API key format may be non-standard"
          fi
        fi
        
        # キーの最初の数文字で識別(安全に)
        KEY_PREFIX=$(echo "$API_KEY" | cut -c1-4)
        echo "::notice::API key prefix: ${KEY_PREFIX}***"
        
      else
        echo "::error::API key not found or empty"
      fi
      
      echo "::endgroup::"
    fi

解決策の実装

yaml# API キー更新とテストの自動化
- name: API key validation and renewal
  env:
    OLD_API_KEY: ${{ secrets.API_KEY }}
    NEW_API_KEY: ${{ secrets.NEW_API_KEY }}
  run: |
    echo "::group::API Key Management"

    # 既存キーのテスト
    test_api_key() {
      local key=$1
      local label=$2
      
      echo "Testing $label..."
      RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
        -H "Authorization: Bearer $key" \
        "${{ secrets.API_ENDPOINT }}/auth/test")
      
      if [[ "$RESPONSE" -eq 200 ]]; then
        echo "::notice::$label is valid"
        return 0
      else
        echo "::warning::$label failed with HTTP $RESPONSE"
        return 1
      fi
    }

    # 古いキーと新しいキーをテスト
    if test_api_key "$OLD_API_KEY" "Current API key"; then
      echo "Current API key is working"
    elif test_api_key "$NEW_API_KEY" "New API key"; then
      echo "::warning::Current key failed, but new key works"
      echo "Action required: Update API_KEY secret to NEW_API_KEY value"
    else
      echo "::error::Both API keys failed - manual intervention required"
      exit 1
    fi

    echo "::endgroup::"

ケース 3: タイミング・競合状態の問題

マイクロサービス間の API 呼び出しで、負荷テスト時に間欠的にタイムアウトが発生する事例です。

mermaidgraph TD
    A[負荷テスト開始] --> B[複数サービス並行起動]
    B --> C[Service A]
    B --> D[Service B]
    B --> E[Service C]

    C --> F[データベース接続]
    D --> F
    E --> F

    F --> G{接続プール枯渇?}
    G -->|Yes| H[タイムアウト発生]
    G -->|No| I[正常処理]

    H --> J[re-run で成功パターン]
    I --> K[一貫した成功]

    J --> L[競合状態の特定]
    K --> M[正常フロー確認]

Step1: タイミング問題の可視化

yaml- name: Concurrent load testing with timing analysis
  run: |
    echo "::group::Concurrent Service Load Test"

    # 並行処理の開始時刻を記録
    START_TIME=$(date +%s.%N)
    echo "Load test started at: $START_TIME"

    # 複数サービスを並行起動
    start_service() {
      local service_name=$1
      local port=$2
      local start_time=$(date +%s.%N)
      
      echo "::notice::Starting $service_name on port $port"
      
      # サービス起動
      npm start --service=$service_name --port=$port &
      local pid=$!
      
      # 起動完了まで待機(最大30秒)
      local timeout=30
      local elapsed=0
      
      while ! curl -f -s "http://localhost:$port/health" > /dev/null; do
        sleep 0.5
        elapsed=$(echo "$elapsed + 0.5" | bc)
        
        if (( $(echo "$elapsed > $timeout" | bc -l) )); then
          echo "::error::$service_name startup timeout after ${timeout}s"
          kill $pid 2>/dev/null
          return 1
        fi
      done
      
      local end_time=$(date +%s.%N)
      local startup_duration=$(echo "$end_time - $start_time" | bc)
      
      echo "::notice::$service_name started in ${startup_duration}s (PID: $pid)"
      echo "$service_name:$pid:$port" >> services.txt
      
      return 0
    }

    # 3つのサービスを並行起動
    start_service "auth-service" 3001 &
    start_service "user-service" 3002 &
    start_service "api-service" 3003 &

    # 全ての起動完了を待機
    wait

    echo "::endgroup::"

Step2: 負荷パターンと失敗率の分析

javascript// 負荷テストと失敗パターンの分析
class LoadTestAnalyzer {
  static async runConcurrentTests(concurrency, duration) {
    const results = [];
    const startTime = Date.now();

    console.log(
      `Starting load test: ${concurrency} concurrent requests for ${duration}ms`
    );

    // 並行リクエストの実行
    const promises = Array.from(
      { length: concurrency },
      async (_, index) => {
        const requestResults = [];

        while (Date.now() - startTime < duration) {
          const requestStart = Date.now();

          try {
            const response = await fetch(
              'http://localhost:3003/api/test',
              {
                timeout: 5000,
              }
            );

            const requestEnd = Date.now();
            const duration = requestEnd - requestStart;

            requestResults.push({
              threadId: index,
              timestamp: requestStart,
              duration,
              status: response.status,
              success: response.ok,
            });

            // スループット調整
            await new Promise((resolve) =>
              setTimeout(resolve, 100)
            );
          } catch (error) {
            const requestEnd = Date.now();

            requestResults.push({
              threadId: index,
              timestamp: requestStart,
              duration: requestEnd - requestStart,
              status: 0,
              success: false,
              error: error.message,
            });
          }
        }

        return requestResults;
      }
    );

    // 全ての並行テストの完了を待機
    const allResults = await Promise.all(promises);
    return allResults.flat();
  }

  static analyzeResults(results) {
    const totalRequests = results.length;
    const successfulRequests = results.filter(
      (r) => r.success
    ).length;
    const failedRequests =
      totalRequests - successfulRequests;

    const successRate =
      (successfulRequests / totalRequests) * 100;
    const avgDuration =
      results.reduce((sum, r) => sum + r.duration, 0) /
      totalRequests;

    // タイムアウトエラーの分析
    const timeoutErrors = results.filter(
      (r) => r.error && r.error.includes('timeout')
    );
    const connectionErrors = results.filter(
      (r) => r.error && r.error.includes('ECONNREFUSED')
    );

    // 時系列での失敗パターン分析
    const timeSlots = this.groupByTimeSlots(results, 1000); // 1秒間隔
    const failurePattern = timeSlots.map((slot) => ({
      timestamp: slot.timestamp,
      failureRate: (slot.failures / slot.total) * 100,
    }));

    return {
      summary: {
        totalRequests,
        successfulRequests,
        failedRequests,
        successRate: successRate.toFixed(2),
        avgDuration: avgDuration.toFixed(2),
      },
      errorBreakdown: {
        timeouts: timeoutErrors.length,
        connectionRefused: connectionErrors.length,
        other:
          failedRequests -
          timeoutErrors.length -
          connectionErrors.length,
      },
      failurePattern,
      recommendation: this.generateRecommendation(
        successRate,
        timeoutErrors.length,
        failurePattern
      ),
    };
  }

  static generateRecommendation(
    successRate,
    timeouts,
    pattern
  ) {
    if (successRate < 50) {
      return 'Critical: System cannot handle current load - scale infrastructure';
    } else if (timeouts > pattern.length * 0.3) {
      return 'Timeout issues detected - increase timeout values or optimize backend';
    } else if (pattern.some((p) => p.failureRate > 20)) {
      return 'Intermittent failures detected - investigate resource contention';
    } else {
      return 'System performing adequately under current load';
    }
  }

  static groupByTimeSlots(results, intervalMs) {
    const slots = new Map();

    results.forEach((result) => {
      const slotKey =
        Math.floor(result.timestamp / intervalMs) *
        intervalMs;

      if (!slots.has(slotKey)) {
        slots.set(slotKey, {
          timestamp: slotKey,
          total: 0,
          failures: 0,
        });
      }

      const slot = slots.get(slotKey);
      slot.total++;
      if (!result.success) {
        slot.failures++;
      }
    });

    return Array.from(slots.values()).sort(
      (a, b) => a.timestamp - b.timestamp
    );
  }
}

Step3: リソース競合の詳細調査

yaml- name: Resource contention analysis
  env:
    ENABLE_PROFILING: ${{ secrets.ENABLE_PROFILING }}
  run: |
    if [[ "$ENABLE_PROFILING" == "true" ]]; then
      echo "::group::Resource Contention Analysis"
      
      # データベース接続プールの監視
      monitor_db_connections() {
        echo "Monitoring database connections..."
        
        while true; do
          # PostgreSQL の場合
          ACTIVE_CONNECTIONS=$(psql -h localhost -U testuser -d testdb -t -c \
            "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';" 2>/dev/null || echo "0")
          
          TOTAL_CONNECTIONS=$(psql -h localhost -U testuser -d testdb -t -c \
            "SELECT count(*) FROM pg_stat_activity;" 2>/dev/null || echo "0")
          
          MAX_CONNECTIONS=$(psql -h localhost -U testuser -d testdb -t -c \
            "SHOW max_connections;" 2>/dev/null | tr -d ' ' || echo "100")
          
          echo "DB Connections: $ACTIVE_CONNECTIONS active, $TOTAL_CONNECTIONS total, $MAX_CONNECTIONS max"
          
          # 接続プール使用率の警告
          USAGE_PERCENT=$(echo "scale=2; $TOTAL_CONNECTIONS * 100 / $MAX_CONNECTIONS" | bc)
          if (( $(echo "$USAGE_PERCENT > 80" | bc -l) )); then
            echo "::warning::High DB connection usage: ${USAGE_PERCENT}%"
          fi
          
          sleep 2
        done
      }
      
      # システムリソースの監視
      monitor_system_resources() {
        echo "Monitoring system resources..."
        
        while true; do
          # CPU使用率
          CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
          
          # メモリ使用率
          MEMORY_INFO=$(free | grep '^Mem:')
          TOTAL_MEM=$(echo $MEMORY_INFO | awk '{print $2}')
          USED_MEM=$(echo $MEMORY_INFO | awk '{print $3}')
          MEM_USAGE=$(echo "scale=2; $USED_MEM * 100 / $TOTAL_MEM" | bc)
          
          # ディスクI/O
          DISK_IO=$(iostat -d 1 1 | tail -n +4 | awk '{sum += $4} END {print sum}')
          
          echo "System: CPU ${CPU_USAGE}%, Memory ${MEM_USAGE}%, Disk I/O ${DISK_IO}"
          
          # リソース使用率の警告
          if (( $(echo "$CPU_USAGE > 80" | bc -l) )); then
            echo "::warning::High CPU usage: ${CPU_USAGE}%"
          fi
          
          if (( $(echo "$MEM_USAGE > 85" | bc -l) )); then
            echo "::warning::High memory usage: ${MEM_USAGE}%"
          fi
          
          sleep 2
        done
      }
      
      # バックグラウンドで監視開始
      monitor_db_connections > db_monitor.log &
      DB_MONITOR_PID=$!
      
      monitor_system_resources > system_monitor.log &
      SYS_MONITOR_PID=$!
      
      # 負荷テスト実行
      echo "Starting load test with monitoring..."
      npm run load-test
      
      # 監視停止
      kill $DB_MONITOR_PID $SYS_MONITOR_PID
      
      # 結果の分析
      echo "::notice::Database connection patterns:"
      tail -20 db_monitor.log
      
      echo "::notice::System resource patterns:"
      tail -20 system_monitor.log
      
      echo "::endgroup::"
    fi

解決策の実装

yaml# 競合状態への対策実装
- name: Implement concurrency safeguards
  run: |
    echo "::group::Concurrency Optimization"

    # 1. データベース接続プールの最適化
    cat > database-config.js << 'EOF'
    module.exports = {
      pool: {
        min: 2,
        max: 10,
        acquire: 30000,        // 接続取得の最大待機時間
        idle: 10000,           // アイドル接続の維持時間
        evict: 1000,           // 使われていない接続の削除間隔
        handleDisconnects: true // 接続断の自動ハンドリング
      },
      retry: {
        max: 3,                // 最大リトライ回数
        timeout: 5000,         // タイムアウト時間
        match: [              // リトライ対象のエラー
          /ETIMEDOUT/,
          /ECONNRESET/,
          /ENOTFOUND/,
          /ENETUNREACH/
        ]
      }
    };
    EOF

    # 2. サーキットブレーカーパターンの実装
    cat > circuit-breaker.js << 'EOF'
    class CircuitBreaker {
      constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 5;
        this.timeout = options.timeout || 60000;
        this.resetTimeout = options.resetTimeout || 30000;
        
        this.state = 'CLOSED';
        this.failureCount = 0;
        this.lastFailureTime = null;
      }
      
      async execute(operation) {
        if (this.state === 'OPEN') {
          if (Date.now() - this.lastFailureTime > this.resetTimeout) {
            this.state = 'HALF_OPEN';
          } else {
            throw new Error('Circuit breaker is OPEN');
          }
        }
        
        try {
          const result = await Promise.race([
            operation(),
            new Promise((_, reject) => 
              setTimeout(() => reject(new Error('Timeout')), this.timeout)
            )
          ]);
          
          this.onSuccess();
          return result;
        } catch (error) {
          this.onFailure();
          throw error;
        }
      }
      
      onSuccess() {
        this.failureCount = 0;
        this.state = 'CLOSED';
      }
      
      onFailure() {
        this.failureCount++;
        this.lastFailureTime = Date.now();
        
        if (this.failureCount >= this.failureThreshold) {
          this.state = 'OPEN';
        }
      }
    }
    EOF

    # 3. 接続プールの健全性チェック
    cat > pool-health-check.js << 'EOF'
    async function checkPoolHealth(pool) {
      const healthMetrics = {
        activeConnections: pool.pool.numUsed(),
        idleConnections: pool.pool.numFree(),
        totalConnections: pool.pool.numUsed() + pool.pool.numFree(),
        maxConnections: pool.pool.max,
        pendingAcquires: pool.pool.numPendingAcquires()
      };
      
      const usage = (healthMetrics.totalConnections / healthMetrics.maxConnections) * 100;
      
      if (usage > 80) {
        console.warn(`High pool usage: ${usage.toFixed(1)}%`);
      }
      
      if (healthMetrics.pendingAcquires > 5) {
        console.warn(`High pending acquires: ${healthMetrics.pendingAcquires}`);
      }
      
      return healthMetrics;
    }
    EOF

    echo "::notice::Concurrency safeguards implemented"
    echo "::endgroup::"

まとめ

GitHub Actions の突然の失敗に対する効果的な切り分け術として、ログレベル調整、re-run 機能、debug secrets の 3 つの手法をご紹介しました。

これらの手法を段階的に適用することで、以下のような成果を得られます。

手法主な効果解決できる問題の種類実施コスト
ログレベル調整問題の可視化と詳細把握環境設定、依存関係、コード不具合
re-run 機能問題の再現性と性質の判定間欠的障害、タイミング問題
debug secrets機密情報を含む深掘り調査認証エラー、外部連携問題

効果的な切り分けのポイント

  1. 段階的アプローチ: 簡単な方法から順番に試すことで、効率的に原因を絞り込める
  2. パターン認識: 失敗の種類を分類することで、適切な調査手法を選択できる
  3. セキュリティ配慮: debug secrets は必要最小限の範囲で、権限管理を徹底して活用する
  4. 自動化: 頻繁に発生する問題については、調査プロセスを自動化して対応時間を短縮する

これらの切り分け術を習得することで、GitHub Actions のトラブルシューティングが格段に効率化され、開発チームの生産性向上に大きく貢献するでしょう。突然の失敗に直面した際は、慌てずに本記事の手法を順番に適用し、根本原因の特定に取り組んでください。

関連リンク