Skip to content

Commit 0c924f5

Browse files
committed
Improve claude --green error tracking and commit handling
Add error tracking to prevent infinite loops when same error repeats: - Track local check errors via hashError() to detect duplicates - Track CI errors by run ID to avoid re-checking same failures - Exit early when error unchanged after attempted fix Detect if commit is part of a PR: - Add checkIfCommitIsPartOfPR() to query gh for PR info - Display PR number and state when monitoring CI Fix commit messages and --no-verify handling: - Remove hardcoded generic commit messages - Use descriptive messages without AI attribution - Only use --no-verify when explicitly requested via flag - Respect CLAUDE.md commit standards
1 parent 0f07ce1 commit 0c924f5

File tree

1 file changed

+124
-15
lines changed

1 file changed

+124
-15
lines changed

scripts/claude.mjs

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,60 @@ async function ensureGitHubAuthenticated() {
499499
return false
500500
}
501501

502+
/**
503+
* Check if a commit SHA is part of a pull request.
504+
* @param {string} sha - The commit SHA to check
505+
* @param {string} owner - The repository owner
506+
* @param {string} repo - The repository name
507+
* @returns {Promise<{isPR: boolean, prNumber?: number, prTitle?: string}>}
508+
*/
509+
async function checkIfCommitIsPartOfPR(sha, owner, repo) {
510+
try {
511+
const result = await runCommandWithOutput('gh', [
512+
'pr',
513+
'list',
514+
'--repo',
515+
`${owner}/${repo}`,
516+
'--state',
517+
'all',
518+
'--search',
519+
sha,
520+
'--json',
521+
'number,title,state',
522+
'--limit',
523+
'1',
524+
])
525+
526+
if (result.exitCode === 0 && result.stdout) {
527+
const prs = JSON.parse(result.stdout)
528+
if (prs.length > 0) {
529+
const pr = prs[0]
530+
return {
531+
isPR: true,
532+
prNumber: pr.number,
533+
prTitle: pr.title,
534+
prState: pr.state,
535+
}
536+
}
537+
}
538+
} catch (e) {
539+
log.warn(`Failed to check if commit is part of PR: ${e.message}`)
540+
}
541+
542+
return { isPR: false }
543+
}
544+
545+
/**
546+
* Create a hash of error output for tracking duplicate errors.
547+
* @param {string} errorOutput - The error output to hash
548+
* @returns {string} A simple hash of the error
549+
*/
550+
function hashError(errorOutput) {
551+
// Simple hash: take first 200 chars of error, normalize whitespace
552+
const normalized = errorOutput.trim().slice(0, 200).replace(/\s+/g, ' ')
553+
return normalized
554+
}
555+
502556
/**
503557
* Model strategy for intelligent Pinky/Brain switching.
504558
* "Gee, Brain, what do you want to do tonight?"
@@ -2678,9 +2732,15 @@ async function runGreen(claudeCmd, options = {}) {
26782732
opts['max-auto-fixes'] || '10',
26792733
10,
26802734
)
2735+
const useNoVerify = opts['no-verify'] === true
26812736

26822737
printHeader('Green CI Pipeline')
26832738

2739+
// Track errors to avoid checking same error repeatedly
2740+
const seenErrors = new Set()
2741+
// Track CI errors by run ID
2742+
const ciErrorHistory = new Map()
2743+
26842744
// Step 1: Run local checks
26852745
const repoName = path.basename(rootPath)
26862746
log.step(`Running local checks in ${colors.cyan(repoName)}`)
@@ -2711,12 +2771,24 @@ async function runGreen(claudeCmd, options = {}) {
27112771

27122772
if (result.exitCode !== 0) {
27132773
log.failed(`${check.name} failed`)
2774+
2775+
// Track error to avoid repeated attempts on same error
2776+
const errorOutput =
2777+
result.stderr || result.stdout || 'No error output available'
2778+
const errorHash = hashError(errorOutput)
2779+
2780+
if (seenErrors.has(errorHash)) {
2781+
log.error(`Detected same error again for "${check.name}"`)
2782+
log.substep('Skipping auto-fix to avoid infinite loop')
2783+
log.substep('Error appears unchanged from previous attempt')
2784+
return false
2785+
}
2786+
2787+
seenErrors.add(errorHash)
27142788
autoFixAttempts++
27152789

27162790
// Decide whether to auto-fix or go interactive
27172791
const isAutoMode = autoFixAttempts <= MAX_AUTO_FIX_ATTEMPTS
2718-
const errorOutput =
2719-
result.stderr || result.stdout || 'No error output available'
27202792

27212793
if (isAutoMode) {
27222794
// Attempt automatic fix
@@ -2890,9 +2962,13 @@ Let's work through this together to get CI passing.`
28902962
// Stage all changes
28912963
await runCommand('git', ['add', '.'], { cwd: rootPath })
28922964

2893-
// Commit
2894-
const commitMessage = 'Fix CI issues and update tests'
2895-
await runCommand('git', ['commit', '-m', commitMessage, '--no-verify'], {
2965+
// Commit with descriptive message (no AI attribution per CLAUDE.md)
2966+
const commitMessage = 'Fix local checks and update tests'
2967+
const commitArgs = ['commit', '-m', commitMessage]
2968+
if (useNoVerify) {
2969+
commitArgs.push('--no-verify')
2970+
}
2971+
await runCommand('git', commitArgs, {
28962972
cwd: rootPath,
28972973
})
28982974

@@ -2969,6 +3045,17 @@ Let's work through this together to get CI passing.`
29693045
const [, owner, repoNameMatch] = repoMatch
29703046
const repo = repoNameMatch.replace('.git', '')
29713047

3048+
// Check if commit is part of a PR
3049+
const prInfo = await checkIfCommitIsPartOfPR(currentSha, owner, repo)
3050+
if (prInfo.isPR) {
3051+
log.info(
3052+
`Commit is part of PR #${prInfo.prNumber}: ${colors.cyan(prInfo.prTitle)}`,
3053+
)
3054+
log.substep(`PR state: ${prInfo.prState}`)
3055+
} else {
3056+
log.info('Commit is a direct push (not part of a PR)')
3057+
}
3058+
29723059
// Monitor workflow with retries
29733060
let retryCount = 0
29743061
let lastRunId = null
@@ -3118,6 +3205,29 @@ Let's work through this together to get CI passing.`
31183205
},
31193206
)
31203207

3208+
// Check if we've seen this CI error before
3209+
const ciErrorOutput = logsResult.stdout || 'No logs available'
3210+
const ciErrorHash = hashError(ciErrorOutput)
3211+
3212+
if (ciErrorHistory.has(lastRunId)) {
3213+
log.error(`Already attempted fix for run ${lastRunId}`)
3214+
log.substep('Skipping to avoid repeated attempts on same CI run')
3215+
retryCount++
3216+
continue
3217+
}
3218+
3219+
if (seenErrors.has(ciErrorHash)) {
3220+
log.error('Detected same CI error pattern as previous attempt')
3221+
log.substep('Error appears unchanged after push')
3222+
log.substep(
3223+
`View run at: https://github.com/${owner}/${repo}/actions/runs/${lastRunId}`,
3224+
)
3225+
return false
3226+
}
3227+
3228+
ciErrorHistory.set(lastRunId, ciErrorHash)
3229+
seenErrors.add(ciErrorHash)
3230+
31213231
// Analyze and fix with Claude
31223232
log.progress('Analyzing CI failure with Claude')
31233233
const fixPrompt = `You are automatically fixing CI failures. The CI workflow failed for commit ${currentSha} in ${owner}/${repo}.
@@ -3207,17 +3317,16 @@ Fix all CI failures now by making the necessary changes.`
32073317
if (fixStatusResult.stdout.trim()) {
32083318
log.progress('Committing CI fixes')
32093319
await runCommand('git', ['add', '.'], { cwd: rootPath })
3210-
await runCommand(
3211-
'git',
3212-
[
3213-
'commit',
3214-
'-m',
3215-
`Fix CI failures (attempt ${retryCount + 1})`,
3216-
'--no-verify',
3217-
],
3218-
{ cwd: rootPath },
3219-
)
3320+
3321+
// Commit with descriptive message (no AI attribution per CLAUDE.md)
3322+
const ciFixMessage = `Fix CI failures from run ${lastRunId}`
3323+
const ciCommitArgs = ['commit', '-m', ciFixMessage]
3324+
if (useNoVerify) {
3325+
ciCommitArgs.push('--no-verify')
3326+
}
3327+
await runCommand('git', ciCommitArgs, { cwd: rootPath })
32203328
await runCommand('git', ['push'], { cwd: rootPath })
3329+
log.done(`Pushed fix commit: ${ciFixMessage}`)
32213330

32223331
// Update SHA for next check
32233332
const newShaResult = await runCommandWithOutput(

0 commit comments

Comments
 (0)