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 | 機密情報を含む深掘り調査 | 認証エラー、外部連携問題 | 中 |
効果的な切り分けのポイント
- 段階的アプローチ: 簡単な方法から順番に試すことで、効率的に原因を絞り込める
- パターン認識: 失敗の種類を分類することで、適切な調査手法を選択できる
- セキュリティ配慮: debug secrets は必要最小限の範囲で、権限管理を徹底して活用する
- 自動化: 頻繁に発生する問題については、調査プロセスを自動化して対応時間を短縮する
これらの切り分け術を習得することで、GitHub Actions のトラブルシューティングが格段に効率化され、開発チームの生産性向上に大きく貢献するでしょう。突然の失敗に直面した際は、慌てずに本記事の手法を順番に適用し、根本原因の特定に取り組んでください。
関連リンク
- article
GitHub Actions が突然失敗するときの切り分け術:ログレベル・re-run・debug secrets
- article
GitHub Actions の実行順序を完全図解:イベント → フィルタ → ジョブ → ステップの流れ
- article
GitHub Actions × Node.js:テストとデプロイを自動化する
- article
GitHub Actions で CI/CD パイプラインを構築する方法
- article
Dify × GitHub Actions で DevOps 自動化
- article
GitHub Actions と Jenkins の違いを徹底比較
- article
【比較検証】Convex vs Firebase vs Supabase:リアルタイム性・整合性・学習コストの最適解
- article
【徹底比較】Preact vs React 2025:バンドル・FPS・メモリ・DX を総合評価
- article
GPT-5-Codex vs Claude Code / Cursor 徹底比較:得意領域・精度・開発速度の違いを検証
- article
Astro × Cloudflare Workers/Pages:エッジ配信で超高速なサイトを構築
- article
【2025 年版】Playwright vs Cypress vs Selenium 徹底比較:速度・安定性・学習コストの最適解
- article
Apollo を最短導入:Vite/Next.js/Remix での初期配線テンプレ集
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来