Skip to content

Commit dbf88b9

Browse files
committed
chore: improve cleanup
chore: wip
1 parent b55bd3c commit dbf88b9

File tree

2 files changed

+276
-289
lines changed

2 files changed

+276
-289
lines changed

bin/cli.ts

Lines changed: 96 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
validateRepositoryAccess,
3131
validateWorkflowGeneration,
3232
} from '../src/setup'
33-
import { checkForAutoClose, extractPackageNamesFromPRBody } from '../src/utils/helpers'
3433
import { Logger } from '../src/utils/logger'
3534

3635
const cli = new CAC('buddy-bot')
@@ -944,280 +943,131 @@ cli
944943
hasWorkflowPermissions,
945944
)
946945

947-
// Get buddy-bot PRs using GitHub CLI to avoid API rate limits
948-
let buddyPRs: any[] = []
949-
try {
950-
// Try GitHub CLI first (much faster and doesn't count against API limits)
951-
const prOutput = await gitProvider.runCommand('gh', [
952-
'pr',
953-
'list',
954-
'--state',
955-
'open',
956-
'--json',
957-
'number,title,body,headRefName,author,url,createdAt,updatedAt',
958-
])
959-
const allPRs = JSON.parse(prOutput)
960-
961-
buddyPRs = allPRs.filter((pr: any) =>
962-
pr.headRefName?.startsWith('buddy-bot/')
963-
|| pr.author?.login === 'github-actions[bot]'
964-
|| pr.author?.login?.includes('buddy'),
965-
).map((pr: any) => ({
966-
number: pr.number,
967-
title: pr.title,
968-
body: pr.body || '',
969-
head: pr.headRefName,
970-
author: pr.author?.login || 'unknown',
971-
url: pr.url,
972-
createdAt: new Date(pr.createdAt),
973-
updatedAt: new Date(pr.updatedAt),
974-
}))
975-
976-
console.log(`🔍 Found ${buddyPRs.length} buddy-bot PRs using GitHub CLI (no API calls)`)
977-
}
978-
catch (cliError) {
979-
console.warn('⚠️ GitHub CLI failed, falling back to API:', cliError)
980-
// Fallback to API method
981-
const prs = await gitProvider.getPullRequests('open')
982-
buddyPRs = prs.filter(pr =>
983-
pr.head.startsWith('buddy-bot/')
984-
|| pr.author === 'github-actions[bot]'
985-
|| pr.author.includes('buddy'),
986-
)
987-
}
988-
989-
if (buddyPRs.length === 0) {
990-
logger.info('📋 No buddy-bot PRs found')
991-
return
992-
}
993-
994-
logger.info(`📋 Found ${buddyPRs.length} buddy-bot PR(s)`)
946+
// Step 1: Check for rebase checkboxes using HTTP (no API auth needed)
947+
logger.info('🔍 Checking for PRs with rebase checkbox enabled...')
995948

996949
let rebasedCount = 0
997-
let closedCount = 0
998-
999-
for (const pr of buddyPRs) {
1000-
// First, check if this PR should be auto-closed due to respectLatest config changes
1001-
const shouldAutoClose = await checkForAutoClose(pr, config, logger)
1002-
if (shouldAutoClose) {
1003-
if (options.dryRun) {
1004-
logger.info(`🔍 [DRY RUN] Would auto-close PR #${pr.number} (contains dynamic versions that are now filtered)`)
1005-
1006-
// Extract package names for logging
1007-
const packageNames = extractPackageNamesFromPRBody(pr.body)
1008-
logger.info(`📋 PR contains packages: ${packageNames.join(', ')}`)
1009-
closedCount++
1010-
continue
1011-
}
1012-
else {
1013-
logger.info(`🔒 Auto-closing PR #${pr.number} (contains dynamic versions that are now filtered by respectLatest config)`)
1014-
1015-
// Extract package names for logging
1016-
const packageNames = extractPackageNamesFromPRBody(pr.body)
1017-
logger.info(`📋 PR contains packages: ${packageNames.join(', ')}`)
1018-
1019-
try {
1020-
// Add a comment explaining why the PR was closed
1021-
let comment = `🤖 **Auto-closed by Buddy Bot**
1022-
1023-
This PR was automatically closed due to configuration changes.`
1024-
1025-
// Check if it's a respectLatest issue
1026-
const respectLatest = config.packages?.respectLatest ?? true
1027-
const prBody = pr.body.toLowerCase()
1028-
const dynamicIndicators = ['latest', '*', 'main', 'master', 'develop', 'dev']
1029-
const hasDynamicVersions = dynamicIndicators.some(indicator => prBody.includes(indicator))
1030-
1031-
// Check if it's an ignorePaths issue
1032-
const ignorePaths = config.packages?.ignorePaths || []
1033-
const filePaths = extractFilePathsFromPRBody(pr.body)
1034-
// eslint-disable-next-line ts/no-require-imports
1035-
const { Glob } = require('bun')
1036-
const ignoredFiles = filePaths.filter((filePath) => {
1037-
const normalizedPath = filePath.replace(/^\.\//, '')
1038-
return ignorePaths.some((pattern) => {
1039-
try {
1040-
const glob = new Glob(pattern)
1041-
return glob.match(normalizedPath)
1042-
}
1043-
catch {
1044-
return false
1045-
}
1046-
})
1047-
})
1048-
1049-
if (respectLatest && hasDynamicVersions) {
1050-
comment += `
1051-
1052-
**Reason:** Contains updates for packages with dynamic version indicators (like \`*\`, \`latest\`, etc.) that are now filtered out by the \`respectLatest\` configuration.
950+
let checkedPRs = 0
1053951

1054-
**Affected packages:** ${packageNames.join(', ')}
1055-
1056-
The \`respectLatest\` setting (default: \`true\`) prevents updates to packages that use dynamic version indicators, as these are typically meant to always use the latest version and shouldn't be pinned to specific versions.
1057-
1058-
If you need to update these packages to specific versions, you can:
1059-
1. Set \`respectLatest: false\` in your \`buddy-bot.config.ts\`
1060-
2. Or manually update the dependency files to use specific versions instead of dynamic indicators
1061-
1062-
This helps maintain the intended behavior of dynamic version indicators while preventing unwanted updates.`
1063-
}
1064-
else if (ignoredFiles.length > 0) {
1065-
comment += `
1066-
1067-
**Reason:** Contains updates for files that are now excluded by the \`ignorePaths\` configuration.
1068-
1069-
**Affected files:** ${ignoredFiles.join(', ')}
1070-
1071-
The \`ignorePaths\` setting now excludes these file paths from dependency updates. This PR was created before these paths were ignored.
1072-
1073-
If you need to include these files again, you can:
1074-
1. Remove or modify the relevant patterns in \`ignorePaths\` in your \`buddy-bot.config.ts\`
1075-
2. Or manually manage dependencies in these paths
1076-
1077-
Current ignore patterns: ${ignorePaths.join(', ')}`
1078-
}
1079-
else {
1080-
comment += `
1081-
1082-
**Reason:** Configuration changes have made this PR obsolete.
1083-
1084-
**Affected packages:** ${packageNames.join(', ')}
1085-
1086-
Please check your \`buddy-bot.config.ts\` configuration for recent changes to \`respectLatest\` or \`ignorePaths\` settings.`
952+
try {
953+
// Get PR numbers from git refs
954+
const prRefsOutput = await gitProvider.runCommand('git', ['ls-remote', 'origin', 'refs/pull/*/head'])
955+
const prNumbers: number[] = []
956+
957+
for (const line of prRefsOutput.split('\n')) {
958+
if (line.trim()) {
959+
const parts = line.trim().split('\t')
960+
if (parts.length === 2) {
961+
const ref = parts[1] // refs/pull/123/head
962+
const prMatch = ref.match(/refs\/pull\/(\d+)\/head/)
963+
if (prMatch) {
964+
prNumbers.push(Number.parseInt(prMatch[1]))
1087965
}
1088-
1089-
await gitProvider.createComment(pr.number, comment)
1090-
await gitProvider.closePullRequest(pr.number)
1091-
logger.success(`✅ Successfully closed PR #${pr.number} (contains dynamic versions that are now filtered by respectLatest configuration)`)
1092-
closedCount++
1093-
continue
1094-
}
1095-
catch (error) {
1096-
logger.error(`❌ Failed to close PR #${pr.number}:`, error)
1097966
}
1098967
}
1099968
}
1100969

1101-
// Check if rebase checkbox is checked
1102-
const isRebaseChecked = checkRebaseCheckbox(pr.body)
1103-
1104-
if (isRebaseChecked) {
1105-
logger.info(`🔄 PR #${pr.number} has rebase checkbox checked: ${pr.title}`)
970+
logger.info(`📋 Found ${prNumbers.length} PRs to check for rebase requests`)
1106971

1107-
if (options.dryRun) {
1108-
logger.info('🔍 [DRY RUN] Would rebase this PR')
1109-
rebasedCount++
1110-
}
1111-
else {
1112-
logger.info(`🔄 Rebasing PR #${pr.number}...`)
972+
// Check each PR for rebase checkbox (in small batches)
973+
const batchSize = 3 // Smaller batches for PR content fetching
974+
for (let i = 0; i < prNumbers.length && i < 20; i += batchSize) { // Limit to first 20 PRs
975+
const batch = prNumbers.slice(i, i + batchSize)
1113976

977+
for (const prNumber of batch) {
1114978
try {
1115-
// Extract package updates from PR body
1116-
const packageUpdates = await extractPackageUpdatesFromPRBody(pr.body)
1117-
1118-
if (packageUpdates.length === 0) {
1119-
logger.warn(`⚠️ Could not extract package updates from PR #${pr.number}, skipping`)
1120-
continue
1121-
}
1122-
1123-
// Update the existing PR with latest updates (true rebase)
1124-
const buddy = new Buddy({
1125-
...config,
1126-
verbose: options.verbose ?? config.verbose,
979+
// Fetch PR page HTML to check for rebase checkbox
980+
const url = `https://github.com/${config.repository.owner}/${config.repository.name}/pull/${prNumber}`
981+
const response = await fetch(url, {
982+
headers: { 'User-Agent': 'buddy-bot/1.0' },
1127983
})
1128984

1129-
const scanResult = await buddy.scanForUpdates()
1130-
if (scanResult.updates.length === 0) {
1131-
logger.info('✅ All dependencies are now up to date!')
1132-
continue
1133-
}
985+
if (response.ok) {
986+
const html = await response.text()
987+
checkedPRs++
1134988

1135-
// Find the matching update group - must match exactly
1136-
const group = scanResult.groups.find(g =>
1137-
g.updates.length === packageUpdates.length
1138-
&& g.updates.every(u => packageUpdates.some(pu => pu.name === u.name))
1139-
&& packageUpdates.every(pu => g.updates.some(u => u.name === pu.name)),
1140-
)
1141-
1142-
if (!group) {
1143-
logger.warn(`⚠️ Could not find matching update group for PR #${pr.number}. This likely means the package grouping has changed.`)
1144-
logger.info(`📋 PR packages: ${packageUpdates.map(p => p.name).join(', ')}`)
1145-
logger.info(`📋 Available groups: ${scanResult.groups.map(g => `${g.name} (${g.updates.length} packages)`).join(', ')}`)
1146-
logger.info(`💡 Skipping rebase - close this PR manually and let buddy-bot create new ones with correct grouping`)
1147-
continue
1148-
}
989+
// Check if PR is open and has rebase checkbox checked
990+
const isOpen = html.includes('State--open') && !html.includes('State--closed') && !html.includes('State--merged')
991+
const hasRebaseChecked = checkRebaseCheckbox(html)
1149992

1150-
// Generate new file changes (package.json, dependency files, GitHub Actions)
1151-
const packageJsonUpdates = await buddy.generateAllFileUpdates(group.updates)
993+
if (isOpen && hasRebaseChecked) {
994+
logger.info(`🔄 PR #${prNumber} has rebase checkbox checked`)
1152995

1153-
// Update the branch with new commits
1154-
await gitProvider.commitChanges(pr.head, group.title, packageJsonUpdates)
1155-
logger.info(`✅ Updated branch ${pr.head} with latest changes`)
1156-
1157-
// Generate new PR content
1158-
const { PullRequestGenerator } = await import('../src/pr/pr-generator')
1159-
const prGenerator = new PullRequestGenerator({ verbose: options.verbose })
1160-
const newBody = await prGenerator.generateBody(group)
1161-
1162-
// Update the PR with new title/body (and uncheck the rebase box)
1163-
const updatedBody = newBody.replace(
1164-
/- \[x\] <!-- rebase-check -->/g,
1165-
'- [ ] <!-- rebase-check -->',
1166-
)
1167-
1168-
await gitProvider.updatePullRequest(pr.number, {
1169-
title: group.title,
1170-
body: updatedBody,
1171-
})
996+
if (options.dryRun) {
997+
logger.info(`🔍 [DRY RUN] Would rebase PR #${prNumber}`)
998+
rebasedCount++
999+
}
1000+
else {
1001+
logger.info(`🔄 Rebasing PR #${prNumber} via rebase command...`)
1002+
1003+
try {
1004+
// Use the existing rebase command logic
1005+
const { spawn } = await import('node:child_process')
1006+
const rebaseProcess = spawn('bunx', ['buddy-bot', 'rebase', prNumber.toString()], {
1007+
stdio: 'inherit',
1008+
cwd: process.cwd(),
1009+
})
1010+
1011+
await new Promise((resolve, reject) => {
1012+
rebaseProcess.on('close', (code) => {
1013+
if (code === 0) {
1014+
rebasedCount++
1015+
resolve(code)
1016+
}
1017+
else {
1018+
reject(new Error(`Rebase failed with code ${code}`))
1019+
}
1020+
})
1021+
})
1022+
1023+
logger.success(`✅ Successfully rebased PR #${prNumber}`)
1024+
}
1025+
catch (rebaseError) {
1026+
logger.error(`❌ Failed to rebase PR #${prNumber}:`, rebaseError)
1027+
}
1028+
}
1029+
}
1030+
}
11721031

1173-
logger.success(`🔄 Successfully rebased PR #${pr.number} in place!`)
1174-
rebasedCount++
1032+
// Small delay between requests
1033+
await new Promise(resolve => setTimeout(resolve, 200))
11751034
}
11761035
catch (error) {
1177-
logger.error(`❌ Failed to rebase PR #${pr.number}:`, error)
1036+
logger.warn(`⚠️ Could not check PR #${prNumber}:`, error)
11781037
}
11791038
}
1039+
1040+
// Delay between batches
1041+
if (i + batchSize < Math.min(prNumbers.length, 20)) {
1042+
await new Promise(resolve => setTimeout(resolve, 1000))
1043+
}
11801044
}
1181-
else {
1182-
logger.info(`📋 PR #${pr.number}: No rebase requested`)
1045+
1046+
if (rebasedCount > 0) {
1047+
logger.success(`✅ ${options.dryRun ? 'Would rebase' : 'Successfully rebased'} ${rebasedCount} PR(s)`)
1048+
}
1049+
else if (checkedPRs > 0) {
1050+
logger.info('📋 No PRs have rebase checkbox enabled')
11831051
}
11841052
}
1185-
1186-
if (closedCount > 0) {
1187-
logger.success(`🔒 ${options.dryRun ? 'Would close' : 'Successfully closed'} ${closedCount} PR(s) with dynamic versions`)
1053+
catch (error) {
1054+
logger.warn('⚠️ Could not check for rebase requests:', error)
11881055
}
11891056

1190-
if (rebasedCount > 0) {
1191-
logger.success(`✅ ${options.dryRun ? 'Would rebase' : 'Successfully rebased'} ${rebasedCount} PR(s)`)
1192-
}
1057+
// Step 2: Run branch cleanup (uses local git commands, no API calls)
1058+
logger.info('\n🧹 Running branch cleanup...')
1059+
const result = await gitProvider.cleanupStaleBranches(2, !!options.dryRun)
11931060

1194-
if (closedCount === 0 && rebasedCount === 0) {
1195-
logger.info('✅ No PRs need attention')
1061+
if (options.dryRun) {
1062+
logger.info(`🔍 [DRY RUN] Would delete ${result.deleted.length} stale branches`)
1063+
}
1064+
else {
1065+
logger.success(`🎉 Cleanup complete: ${result.deleted.length} branches deleted, ${result.failed.length} failed`)
11961066
}
11971067

1198-
// Automatic cleanup of stale branches after processing PRs
1199-
if (!options.dryRun) {
1200-
logger.info('\n🧹 Checking for stale branches to clean up...')
1201-
try {
1202-
const cleanupResult = await gitProvider.cleanupStaleBranches(7, false) // Clean branches older than 7 days
1203-
1204-
if (cleanupResult.deleted.length > 0) {
1205-
logger.success(`🧹 Automatically cleaned up ${cleanupResult.deleted.length} stale branch(es)`)
1206-
if (options.verbose) {
1207-
cleanupResult.deleted.forEach(branch => logger.info(` ✅ Deleted: ${branch}`))
1208-
}
1209-
}
1210-
1211-
if (cleanupResult.failed.length > 0) {
1212-
logger.warn(`⚠️ Failed to clean up ${cleanupResult.failed.length} branch(es)`)
1213-
if (options.verbose) {
1214-
cleanupResult.failed.forEach(branch => logger.warn(` ❌ Failed: ${branch}`))
1215-
}
1216-
}
1217-
}
1218-
catch (cleanupError) {
1219-
logger.warn('⚠️ Branch cleanup failed:', cleanupError)
1220-
}
1068+
// Summary
1069+
if (rebasedCount > 0 || result.deleted.length > 0) {
1070+
logger.success(`\n🎉 Update-check complete: ${rebasedCount} PR(s) rebased, ${result.deleted.length} branches cleaned`)
12211071
}
12221072
}
12231073
catch (error) {

0 commit comments

Comments
 (0)