๐ŸŽ‰ DevOps Interview Prep Bundle is live โ€” 1000+ Q&A across 20 topicsGet it โ†’
All Articles

Build an AI Pipeline Failure Analyzer with GitHub Actions and Claude API

Build an AI tool that automatically analyzes GitHub Actions failures, fetches logs, identifies root causes with Claude API, and posts a fix directly as a PR comment โ€” cutting MTTR for CI failures by 70%.

Shubham5 min read
Share:Tweet

When a CI pipeline fails, developers spend 10-20 minutes reading logs and diagnosing the cause. This tool automates that: Claude reads the failure, identifies the root cause, and posts the fix as a comment on the PR โ€” all within 60 seconds of the failure.

Architecture

GitHub Actions failure
       โ†“
Failure Analyzer workflow triggers
       โ†“
Fetch failed job logs via GitHub API
       โ†“
Send logs to Claude API with context
       โ†“
Claude identifies root cause + fix
       โ†“
Post analysis as PR comment

Step 1: The Analyzer Workflow

Create .github/workflows/analyze-failure.yml:

yaml
name: Analyze CI Failure
 
on:
  workflow_run:
    workflows: ["CI", "Build and Deploy"]   # Trigger when these fail
    types: [completed]
 
jobs:
  analyze:
    if: ${{ github.event.workflow_run.conclusion == 'failure' }}
    runs-on: ubuntu-latest
    permissions:
      actions: read
      pull-requests: write
 
    steps:
      - name: Analyze failure with Claude
        uses: actions/github-script@v7
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const { analyzeFailure } = require('./scripts/analyze-failure.js');
            await analyzeFailure({ github, context });

Step 2: The Analyzer Script

Create scripts/analyze-failure.js:

javascript
const https = require('https');
 
async function callClaude(prompt) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({
      model: 'claude-sonnet-5',
      max_tokens: 2000,
      messages: [{ role: 'user', content: prompt }]
    });
 
    const options = {
      hostname: 'api.anthropic.com',
      path: '/v1/messages',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': process.env.ANTHROPIC_API_KEY,
        'anthropic-version': '2023-06-01',
        'Content-Length': Buffer.byteLength(body)
      }
    };
 
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        const parsed = JSON.parse(data);
        resolve(parsed.content[0].text);
      });
    });
 
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}
 
 
async function getFailedJobLogs(github, owner, repo, runId) {
  // Get list of jobs
  const jobs = await github.rest.actions.listJobsForWorkflowRun({
    owner, repo, run_id: runId
  });
 
  const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
 
  const logsPerJob = [];
  for (const job of failedJobs.slice(0, 3)) {  // Max 3 jobs
    try {
      const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({
        owner, repo, job_id: job.id
      });
 
      // Extract last 200 lines โ€” where errors usually are
      const logText = typeof logs.data === 'string'
        ? logs.data.split('\n').slice(-200).join('\n')
        : String(logs.data).slice(-8000);
 
      logsPerJob.push({
        jobName: job.name,
        conclusion: job.conclusion,
        logs: logText
      });
    } catch (e) {
      logsPerJob.push({ jobName: job.name, logs: `Could not fetch logs: ${e.message}` });
    }
  }
 
  return logsPerJob;
}
 
 
async function analyzeFailure({ github, context }) {
  const run = context.payload.workflow_run;
  const owner = context.repo.owner;
  const repo = context.repo.repo;
 
  console.log(`Analyzing failure for run ${run.id}: ${run.name}`);
 
  // Get failed job logs
  const failedJobLogs = await getFailedJobLogs(github, owner, repo, run.id);
 
  if (failedJobLogs.length === 0) {
    console.log('No failed jobs found');
    return;
  }
 
  // Build prompt for Claude
  const logsFormatted = failedJobLogs.map(job =>
    `## Job: ${job.jobName}\n\`\`\`\n${job.logs}\n\`\`\``
  ).join('\n\n');
 
  const prompt = `You are a CI/CD expert analyzing a GitHub Actions pipeline failure.
 
## Workflow Information
- Workflow: ${run.name}
- Branch: ${run.head_branch}
- Commit: ${run.head_sha.slice(0, 8)}
- Trigger: ${run.event}
 
## Failed Job Logs
${logsFormatted}
 
Analyze this failure and provide:
1. **Root Cause** (1-2 sentences): What exactly caused the failure
2. **Category**: test-failure | build-error | dependency-issue | config-error | flaky-test | infra-issue | permission-error
3. **Fix** (specific steps or code to resolve it)
4. **Confidence**: high | medium | low
5. **Estimated Fix Time**: e.g. "5 minutes", "30 minutes", "1-2 hours"
 
Format your response as a GitHub PR comment using markdown. Start with an emoji that reflects severity (๐Ÿ”ด critical, ๐ŸŸก flaky/infra, ๐ŸŸข easy fix).`;
 
  const analysis = await callClaude(prompt);
 
  // Find the PR associated with this workflow run
  const prs = await github.rest.pulls.list({
    owner, repo,
    head: `${owner}:${run.head_branch}`,
    state: 'open'
  });
 
  const comment = `## ๐Ÿค– AI Failure Analysis
 
**Workflow:** ${run.name} | **Job(s):** ${failedJobLogs.map(j => j.jobName).join(', ')}
 
${analysis}
 
---
*Analyzed by Claude AI ยท [View failed run](${run.html_url})*`;
 
  if (prs.data.length > 0) {
    // Post to the PR
    const prNumber = prs.data[0].number;
    await github.rest.issues.createComment({
      owner, repo,
      issue_number: prNumber,
      body: comment
    });
    console.log(`Posted analysis to PR #${prNumber}`);
  } else {
    // Post to commit if no PR
    await github.rest.repos.createCommitComment({
      owner, repo,
      commit_sha: run.head_sha,
      body: comment
    });
    console.log(`Posted analysis to commit ${run.head_sha.slice(0, 8)}`);
  }
}
 
module.exports = { analyzeFailure };

What Claude Analyzes

Claude sees the raw log output and categorizes failures accurately:

Test failures:

๐Ÿ”ด Root Cause: Unit test `TestUserAuth` failed because the mock Redis client 
was not returning expected data for expired sessions. Line 47 of auth_test.go 
asserts `result.Valid == false` but mock returns `Valid == true`.

Fix: Update mock in `auth_test.go` line 23:
mockRedis.On("Get", expiredKey).Return("", redis.Nil)

Dependency issues:

๐ŸŸก Root Cause: npm install failed with ECONNRESET โ€” likely a transient 
registry connectivity issue. This is probably a flaky infra failure.

Fix: Re-run the workflow. If it keeps failing, add `--legacy-peer-deps` 
flag or pin npm registry to a mirror.

Config errors:

๐Ÿ”ด Root Cause: AWS credentials not found. The `AWS_ROLE_ARN` secret is 
missing from this repository's Actions secrets.

Fix: Go to Settings โ†’ Secrets โ†’ Actions โ†’ New secret:
Name: AWS_ROLE_ARN
Value: arn:aws:iam::123456789:role/github-actions

Example PR Comment Output

markdown
## ๐Ÿค– AI Failure Analysis
 
**Workflow:** CI | **Job(s):** test, build
 
๐Ÿ”ด **Root Cause**: The Docker build failed because the base image 
`python:3.11-slim` changed and the new version removed `libpq-dev`. 
Your requirements.txt includes `psycopg2==2.9.9` which requires PostgreSQL 
client libraries at build time.
 
**Category**: dependency-issue | **Confidence**: high | **Estimated Fix Time**: 15 minutes
 
**Fix**:
Option 1 โ€” Install build dependency explicitly:
```dockerfile
RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/*

Option 2 โ€” Use psycopg2-binary instead (no build deps required):

psycopg2-binary==2.9.9

Option 2 is recommended for containers.


## Extend It

Add Slack notification when Claude detects a critical failure:

```javascript
// In analyze-failure.js, after posting the PR comment:
if (analysis.includes('CRITICAL') || analysis.includes('๐Ÿ”ด')) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `CI failure detected on ${run.head_branch}: ${run.html_url}\n${analysis.slice(0, 300)}...`
    })
  });
}

Teams using this report cutting MTTR from 15-20 minutes to under 5 minutes for common failure categories.


More AI + CI/CD tools? Read our Build AI PR description generator and AI-powered GitHub Actions reviewer.

๐Ÿ”ง

Today I Fixed

Short real fixes from production โ€” posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam โ€” just practical engineering content.

Related Articles

Comments