Skip to content

Commit 0957e9a

Browse files
committed
Add --green flag for CI automation and test monitoring
1 parent 826b027 commit 0957e9a

File tree

1 file changed

+275
-1
lines changed

1 file changed

+275
-1
lines changed

scripts/claude.mjs

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,265 @@ Be specific and actionable.`
15111511
return true
15121512
}
15131513

1514+
/**
1515+
* Run all checks, push, and monitor CI until green.
1516+
*/
1517+
async function runGreen(claudeCmd, options = {}) {
1518+
const opts = { __proto__: null, ...options }
1519+
const maxRetries = parseInt(opts['max-retries'] || '3', 10)
1520+
const isDryRun = opts['dry-run']
1521+
1522+
printHeader('Green CI Pipeline')
1523+
1524+
// Step 1: Run local checks
1525+
log.step('Running local checks')
1526+
const localChecks = [
1527+
{ name: 'Install dependencies', cmd: 'pnpm', args: ['install'] },
1528+
{ name: 'Fix code style', cmd: 'pnpm', args: ['run', 'fix'] },
1529+
{ name: 'Run checks', cmd: 'pnpm', args: ['run', 'check'] },
1530+
{ name: 'Run coverage', cmd: 'pnpm', args: ['run', 'coverage'] },
1531+
{ name: 'Run tests', cmd: 'pnpm', args: ['run', 'test', '--', '--update'] }
1532+
]
1533+
1534+
for (const check of localChecks) {
1535+
log.progress(check.name)
1536+
1537+
if (isDryRun) {
1538+
log.done(`[DRY RUN] Would run: ${check.cmd} ${check.args.join(' ')}`)
1539+
continue
1540+
}
1541+
1542+
const result = await runCommandWithOutput(check.cmd, check.args, {
1543+
cwd: rootPath,
1544+
stdio: 'inherit'
1545+
})
1546+
1547+
if (result.exitCode !== 0) {
1548+
log.failed(`${check.name} failed`)
1549+
1550+
// Attempt to fix with Claude
1551+
log.progress('Attempting auto-fix with Claude')
1552+
const fixPrompt = `The command "${check.cmd} ${check.args.join(' ')}" failed in the ${path.basename(rootPath)} project.
1553+
1554+
Please analyze the error and provide a fix. The error output was:
1555+
${result.stderr || result.stdout}
1556+
1557+
Provide specific file edits or commands to fix this issue.`
1558+
1559+
await runCommand(claudeCmd, prepareClaudeArgs([], opts), {
1560+
input: fixPrompt,
1561+
stdio: 'inherit',
1562+
cwd: rootPath
1563+
})
1564+
1565+
// Retry the check
1566+
log.progress(`Retrying ${check.name}`)
1567+
const retryResult = await runCommandWithOutput(check.cmd, check.args, {
1568+
cwd: rootPath
1569+
})
1570+
1571+
if (retryResult.exitCode !== 0) {
1572+
log.error(`Failed to fix ${check.name} automatically`)
1573+
return false
1574+
}
1575+
}
1576+
1577+
log.done(`${check.name} passed`)
1578+
}
1579+
1580+
// Step 2: Commit and push changes
1581+
log.step('Committing and pushing changes')
1582+
1583+
// Check for changes
1584+
const statusResult = await runCommandWithOutput('git', ['status', '--porcelain'], {
1585+
cwd: rootPath
1586+
})
1587+
1588+
if (statusResult.stdout.trim()) {
1589+
log.progress('Changes detected, committing')
1590+
1591+
if (isDryRun) {
1592+
log.done('[DRY RUN] Would commit and push changes')
1593+
} else {
1594+
// Stage all changes
1595+
await runCommand('git', ['add', '.'], { cwd: rootPath })
1596+
1597+
// Commit
1598+
const commitMessage = 'Fix CI issues and update tests'
1599+
await runCommand('git', ['commit', '-m', commitMessage, '--no-verify'], { cwd: rootPath })
1600+
1601+
// Push
1602+
await runCommand('git', ['push'], { cwd: rootPath })
1603+
log.done('Changes pushed to remote')
1604+
}
1605+
} else {
1606+
log.info('No changes to commit')
1607+
}
1608+
1609+
// Step 3: Monitor CI workflow
1610+
log.step('Monitoring CI workflow')
1611+
1612+
if (isDryRun) {
1613+
log.done('[DRY RUN] Would monitor CI workflow')
1614+
printFooter('Green CI Pipeline (dry run) complete!')
1615+
return true
1616+
}
1617+
1618+
// Get current commit SHA
1619+
const shaResult = await runCommandWithOutput('git', ['rev-parse', 'HEAD'], {
1620+
cwd: rootPath
1621+
})
1622+
const currentSha = shaResult.stdout.trim()
1623+
1624+
// Get repo info
1625+
const remoteResult = await runCommandWithOutput('git', ['remote', 'get-url', 'origin'], {
1626+
cwd: rootPath
1627+
})
1628+
const remoteUrl = remoteResult.stdout.trim()
1629+
const repoMatch = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/)
1630+
1631+
if (!repoMatch) {
1632+
log.error('Could not determine GitHub repository from remote URL')
1633+
return false
1634+
}
1635+
1636+
const [, owner, repoName] = repoMatch
1637+
const repo = repoName.replace('.git', '')
1638+
1639+
// Monitor workflow with retries
1640+
let retryCount = 0
1641+
let lastRunId = null
1642+
1643+
while (retryCount < maxRetries) {
1644+
log.progress(`Checking CI status (attempt ${retryCount + 1}/${maxRetries})`)
1645+
1646+
// Wait a bit for CI to start
1647+
if (retryCount === 0) {
1648+
log.substep('Waiting 10 seconds for CI to start...')
1649+
await new Promise(resolve => setTimeout(resolve, 10000))
1650+
}
1651+
1652+
// Check workflow runs using gh CLI
1653+
const runsResult = await runCommandWithOutput('gh', [
1654+
'run', 'list',
1655+
'--repo', `${owner}/${repo}`,
1656+
'--commit', currentSha,
1657+
'--limit', '1',
1658+
'--json', 'databaseId,status,conclusion,name'
1659+
], {
1660+
cwd: rootPath
1661+
})
1662+
1663+
if (runsResult.exitCode !== 0) {
1664+
log.failed('Failed to fetch workflow runs')
1665+
return false
1666+
}
1667+
1668+
let runs
1669+
try {
1670+
runs = JSON.parse(runsResult.stdout || '[]')
1671+
} catch {
1672+
log.failed('Failed to parse workflow runs')
1673+
return false
1674+
}
1675+
1676+
if (runs.length === 0) {
1677+
log.substep('No workflow runs found yet, waiting...')
1678+
await new Promise(resolve => setTimeout(resolve, 30000))
1679+
continue
1680+
}
1681+
1682+
const run = runs[0]
1683+
lastRunId = run.databaseId
1684+
1685+
log.substep(`Workflow "${run.name}" status: ${run.status}`)
1686+
1687+
if (run.status === 'completed') {
1688+
if (run.conclusion === 'success') {
1689+
log.done('CI workflow passed! 🎉')
1690+
printFooter('Green CI Pipeline complete!')
1691+
return true
1692+
} else {
1693+
log.failed(`CI workflow failed with conclusion: ${run.conclusion}`)
1694+
1695+
if (retryCount < maxRetries - 1) {
1696+
// Fetch failure logs
1697+
log.progress('Fetching failure logs')
1698+
1699+
const logsResult = await runCommandWithOutput('gh', [
1700+
'run', 'view', lastRunId.toString(),
1701+
'--repo', `${owner}/${repo}`,
1702+
'--log-failed'
1703+
], {
1704+
cwd: rootPath
1705+
})
1706+
1707+
// Analyze and fix with Claude
1708+
log.progress('Analyzing CI failure with Claude')
1709+
const fixPrompt = `The CI workflow failed for commit ${currentSha} in ${owner}/${repo}.
1710+
1711+
Failure logs:
1712+
${logsResult.stdout || 'No logs available'}
1713+
1714+
Please analyze these CI logs and provide specific fixes for the failures. Focus on:
1715+
1. Test failures
1716+
2. Lint errors
1717+
3. Type checking issues
1718+
4. Build problems
1719+
1720+
Provide exact file changes needed to fix these issues.`
1721+
1722+
await runCommand(claudeCmd, prepareClaudeArgs([], opts), {
1723+
input: fixPrompt,
1724+
stdio: 'inherit',
1725+
cwd: rootPath
1726+
})
1727+
1728+
// Run local checks again
1729+
log.progress('Running local checks after fixes')
1730+
for (const check of localChecks) {
1731+
await runCommandWithOutput(check.cmd, check.args, {
1732+
cwd: rootPath,
1733+
stdio: 'inherit'
1734+
})
1735+
}
1736+
1737+
// Commit and push fixes
1738+
const fixStatusResult = await runCommandWithOutput('git', ['status', '--porcelain'], {
1739+
cwd: rootPath
1740+
})
1741+
1742+
if (fixStatusResult.stdout.trim()) {
1743+
log.progress('Committing CI fixes')
1744+
await runCommand('git', ['add', '.'], { cwd: rootPath })
1745+
await runCommand('git', ['commit', '-m', `Fix CI failures (attempt ${retryCount + 1})`, '--no-verify'], { cwd: rootPath })
1746+
await runCommand('git', ['push'], { cwd: rootPath })
1747+
1748+
// Update SHA for next check
1749+
const newShaResult = await runCommandWithOutput('git', ['rev-parse', 'HEAD'], {
1750+
cwd: rootPath
1751+
})
1752+
currentSha = newShaResult.stdout.trim()
1753+
}
1754+
1755+
retryCount++
1756+
} else {
1757+
log.error(`CI still failing after ${maxRetries} attempts`)
1758+
log.substep(`View run at: https://github.com/${owner}/${repo}/actions/runs/${lastRunId}`)
1759+
return false
1760+
}
1761+
}
1762+
} else {
1763+
// Workflow still running, wait and check again
1764+
log.substep('Workflow still running, waiting 30 seconds...')
1765+
await new Promise(resolve => setTimeout(resolve, 30000))
1766+
}
1767+
}
1768+
1769+
log.error(`Exceeded maximum retries (${maxRetries})`)
1770+
return false
1771+
}
1772+
15141773
/**
15151774
* Show available Claude operations.
15161775
*/
@@ -1519,6 +1778,7 @@ function showOperations() {
15191778
console.log(' --sync Synchronize CLAUDE.md files across projects')
15201779
console.log(' --commit Create commits with Claude assistance')
15211780
console.log(' --push Create commits and push to remote')
1781+
console.log(' --green Ensure all tests pass, push, monitor CI until green')
15221782

15231783
console.log('\nCode quality:')
15241784
console.log(' --review Review staged changes before committing')
@@ -1562,6 +1822,10 @@ async function main() {
15621822
type: 'boolean',
15631823
default: false,
15641824
},
1825+
green: {
1826+
type: 'boolean',
1827+
default: false,
1828+
},
15651829
// Code quality.
15661830
review: {
15671831
type: 'boolean',
@@ -1645,6 +1909,10 @@ async function main() {
16451909
type: 'boolean',
16461910
default: false,
16471911
},
1912+
'max-retries': {
1913+
type: 'string',
1914+
default: '3',
1915+
},
16481916
},
16491917
allowPositionals: true,
16501918
strict: false,
@@ -1654,7 +1922,7 @@ async function main() {
16541922
const hasOperation = values.sync || values.fix || values.commit || values.push ||
16551923
values.review || values.refactor || values.optimize || values.clean ||
16561924
values.audit || values.test || values.docs || values.explain ||
1657-
values.debug || values.deps || values.migrate
1925+
values.debug || values.deps || values.migrate || values.green
16581926

16591927
// Show help if requested or no operation specified.
16601928
if (values.help || !hasOperation) {
@@ -1670,9 +1938,13 @@ async function main() {
16701938
console.log(' --no-cross-repo Operate on current project only')
16711939
console.log(' --seq Run sequentially (default: parallel)')
16721940
console.log(' --no-darkwing Disable "Let\'s get dangerous!" mode')
1941+
console.log(' --max-retries N Max CI fix attempts (--green, default: 3)')
16731942
console.log('\nExamples:')
16741943
console.log(' pnpm claude --review # Review staged changes')
16751944
console.log(' pnpm claude --fix # Scan for issues')
1945+
console.log(' pnpm claude --green # Ensure CI passes')
1946+
console.log(' pnpm claude --green --dry-run # Test green without real CI')
1947+
console.log(' pnpm claude --green --max-retries 5 # More fix attempts')
16761948
console.log(' pnpm claude --test lib/utils.js # Generate tests for a file')
16771949
console.log(' pnpm claude --explain path.join # Explain a concept')
16781950
console.log(' pnpm claude --refactor src/index.js # Suggest refactoring')
@@ -1710,6 +1982,8 @@ async function main() {
17101982
} else if (values.push) {
17111983
// --push combines commit and push.
17121984
success = await runClaudeCommit(claudeCmd, { ...options, push: true })
1985+
} else if (values.green) {
1986+
success = await runGreen(claudeCmd, options)
17131987
}
17141988
// Code quality operations.
17151989
else if (values.review) {

0 commit comments

Comments
 (0)