Skip to content

Commit 1217beb

Browse files
committed
Detect and fix CI failures early with batched push
Track failed jobs during workflow execution and fix each immediately with local commits. Push all fix commits together when workflow completes to minimize CI runs while maintaining clean git history.
1 parent 3c5367d commit 1217beb

File tree

1 file changed

+218
-2
lines changed

1 file changed

+218
-2
lines changed

scripts/claude.mjs

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3060,8 +3060,15 @@ Let's work through this together to get CI passing.`
30603060
let retryCount = 0
30613061
let lastRunId = null
30623062
let pushTime = Date.now()
3063+
// Track which jobs we've already fixed (jobName -> true)
3064+
let fixedJobs = new Map()
3065+
// Track if we've made any commits during this workflow run
3066+
let hasPendingCommits = false
30633067

30643068
while (retryCount < maxRetries) {
3069+
// Reset tracking for each new CI run
3070+
fixedJobs = new Map()
3071+
hasPendingCommits = false
30653072
log.progress(`Checking CI status (attempt ${retryCount + 1}/${maxRetries})`)
30663073

30673074
// Wait a bit for CI to start
@@ -3186,6 +3193,28 @@ Let's work through this together to get CI passing.`
31863193
}
31873194
log.failed(`CI workflow failed with conclusion: ${run.conclusion}`)
31883195

3196+
// If we have pending commits from fixing jobs during execution, push them now
3197+
if (hasPendingCommits) {
3198+
log.progress('Pushing all fix commits')
3199+
await runCommand('git', ['push'], { cwd: rootPath })
3200+
log.done(`Pushed ${fixedJobs.size} fix commit(s)`)
3201+
3202+
// Update SHA and push time for next check
3203+
const newShaResult = await runCommandWithOutput(
3204+
'git',
3205+
['rev-parse', 'HEAD'],
3206+
{
3207+
cwd: rootPath,
3208+
},
3209+
)
3210+
currentSha = newShaResult.stdout.trim()
3211+
pushTime = Date.now()
3212+
3213+
retryCount++
3214+
continue
3215+
}
3216+
3217+
// No fixes were made during execution, handle as traditional completed workflow
31893218
if (retryCount < maxRetries - 1) {
31903219
// Fetch failure logs
31913220
log.progress('Fetching failure logs')
@@ -3367,8 +3396,195 @@ Fix all CI failures now by making the necessary changes.`
33673396
return false
33683397
}
33693398
} else {
3370-
// Workflow still running, wait and check again
3371-
log.substep('Workflow still running, waiting 30 seconds...')
3399+
// Workflow still running - check for failed jobs and fix them immediately
3400+
log.substep('Workflow still running, checking for failed jobs...')
3401+
3402+
// Fetch jobs for this workflow run
3403+
const jobsResult = await runCommandWithOutput(
3404+
'gh',
3405+
[
3406+
'run',
3407+
'view',
3408+
lastRunId.toString(),
3409+
'--repo',
3410+
`${owner}/${repo}`,
3411+
'--json',
3412+
'jobs',
3413+
],
3414+
{
3415+
cwd: rootPath,
3416+
},
3417+
)
3418+
3419+
if (jobsResult.exitCode === 0 && jobsResult.stdout) {
3420+
try {
3421+
const runData = JSON.parse(jobsResult.stdout)
3422+
const jobs = runData.jobs || []
3423+
3424+
// Check for any failed or cancelled jobs
3425+
const failedJobs = jobs.filter(
3426+
job => job.conclusion === 'failure' || job.conclusion === 'cancelled'
3427+
)
3428+
3429+
// Find new failures we haven't fixed yet
3430+
const newFailures = failedJobs.filter(job => !fixedJobs.has(job.name))
3431+
3432+
if (newFailures.length > 0) {
3433+
log.failed(`Detected ${newFailures.length} new failed job(s)`)
3434+
3435+
// Fix each failed job immediately
3436+
for (const job of newFailures) {
3437+
log.substep(`❌ ${job.name}: ${job.conclusion}`)
3438+
3439+
// Fetch logs for this specific failed job
3440+
log.progress(`Fetching logs for ${job.name}`)
3441+
const logsResult = await runCommandWithOutput(
3442+
'gh',
3443+
[
3444+
'run',
3445+
'view',
3446+
lastRunId.toString(),
3447+
'--repo',
3448+
`${owner}/${repo}`,
3449+
'--log-failed',
3450+
],
3451+
{
3452+
cwd: rootPath,
3453+
},
3454+
)
3455+
console.log('')
3456+
3457+
// Analyze and fix with Claude
3458+
log.progress(`Analyzing failure in ${job.name}`)
3459+
const fixPrompt = `You are automatically fixing CI failures. The job "${job.name}" failed in workflow run ${lastRunId} for commit ${currentSha} in ${owner}/${repo}.
3460+
3461+
Job: ${job.name}
3462+
Status: ${job.conclusion}
3463+
3464+
Failure logs:
3465+
${logsResult.stdout || 'No logs available'}
3466+
3467+
Your task:
3468+
1. Analyze these CI logs for the "${job.name}" job
3469+
2. Identify the root cause of the failure
3470+
3. Apply fixes directly to resolve the issue
3471+
3472+
Focus on:
3473+
- Test failures: Update snapshots, fix test logic, or correct test data
3474+
- Lint errors: Fix code style and formatting issues
3475+
- Type checking: Fix type errors and missing type annotations
3476+
- Build problems: Fix import errors, missing pinned dependencies, or syntax issues
3477+
3478+
IMPORTANT:
3479+
- Be direct and apply fixes immediately
3480+
- Don't ask for clarification or permission
3481+
- Make all necessary file changes to fix this specific failure
3482+
- Focus ONLY on fixing the "${job.name}" job
3483+
3484+
Fix the failure now by making the necessary changes.`
3485+
3486+
// Run Claude non-interactively to apply fixes
3487+
log.substep(`Applying fix for ${job.name}...`)
3488+
3489+
const fixStartTime = Date.now()
3490+
const fixTimeout = 180_000
3491+
3492+
const progressInterval = setInterval(() => {
3493+
const elapsed = Date.now() - fixStartTime
3494+
if (elapsed > fixTimeout) {
3495+
log.warn('Claude fix timeout, proceeding...')
3496+
clearInterval(progressInterval)
3497+
} else {
3498+
log.progress(
3499+
`Claude fixing ${job.name}... (${Math.round(elapsed / 1000)}s)`,
3500+
)
3501+
}
3502+
}, 10_000)
3503+
3504+
try {
3505+
const printArgs = ['--print', ...prepareClaudeArgs([], opts)]
3506+
const result = await runCommandWithOutput(claudeCmd, printArgs, {
3507+
cwd: rootPath,
3508+
input: fixPrompt,
3509+
stdio: ['pipe', 'pipe', 'pipe'],
3510+
})
3511+
if (result.exitCode !== 0) {
3512+
log.warn(`Claude fix exited with code ${result.exitCode}`)
3513+
if (result.stderr) {
3514+
log.warn(`Claude stderr: ${result.stderr.slice(0, 500)}`)
3515+
}
3516+
}
3517+
} catch (error) {
3518+
log.warn(`Claude fix error: ${error.message}`)
3519+
} finally {
3520+
clearInterval(progressInterval)
3521+
log.done(`Fix attempt for ${job.name} completed`)
3522+
}
3523+
3524+
// Give Claude's changes a moment to complete
3525+
await new Promise(resolve => setTimeout(resolve, 2000))
3526+
3527+
// Run local checks
3528+
log.progress('Running local checks after fix')
3529+
console.log('')
3530+
for (const check of localChecks) {
3531+
await runCommandWithOutput(check.cmd, check.args, {
3532+
cwd: rootPath,
3533+
stdio: 'inherit',
3534+
})
3535+
}
3536+
3537+
// Check if there are changes to commit
3538+
const fixStatusResult = await runCommandWithOutput(
3539+
'git',
3540+
['status', '--porcelain'],
3541+
{
3542+
cwd: rootPath,
3543+
},
3544+
)
3545+
3546+
if (fixStatusResult.stdout.trim()) {
3547+
log.progress(`Committing fix for ${job.name}`)
3548+
3549+
const changedFiles = fixStatusResult.stdout
3550+
.trim()
3551+
.split('\n')
3552+
.map(line => line.substring(3))
3553+
.join(', ')
3554+
log.substep(`Changed files: ${changedFiles}`)
3555+
3556+
await runCommand('git', ['add', '.'], { cwd: rootPath })
3557+
3558+
// Commit with descriptive message (no AI attribution per CLAUDE.md)
3559+
const ciFixMessage = `Fix CI failure in ${job.name} (run ${lastRunId})`
3560+
const ciCommitArgs = ['commit', '-m', ciFixMessage]
3561+
if (useNoVerify) {
3562+
ciCommitArgs.push('--no-verify')
3563+
}
3564+
await runCommand('git', ciCommitArgs, { cwd: rootPath })
3565+
log.done(`Committed fix for ${job.name}`)
3566+
3567+
hasPendingCommits = true
3568+
} else {
3569+
log.substep(`No changes to commit for ${job.name}`)
3570+
}
3571+
3572+
// Mark this job as fixed
3573+
fixedJobs.set(job.name, true)
3574+
}
3575+
}
3576+
3577+
// Show current status
3578+
if (fixedJobs.size > 0) {
3579+
log.substep(`Fixed ${fixedJobs.size} job(s) so far (commits pending push)`)
3580+
}
3581+
} catch (e) {
3582+
log.warn(`Failed to parse job data: ${e.message}`)
3583+
}
3584+
}
3585+
3586+
// Wait and check again
3587+
log.substep('Waiting 30 seconds before next check...')
33723588
await new Promise(resolve => setTimeout(resolve, 30_000))
33733589
}
33743590
}

0 commit comments

Comments
 (0)