@@ -30,7 +30,6 @@ import {
3030 validateRepositoryAccess ,
3131 validateWorkflowGeneration ,
3232} from '../src/setup'
33- import { checkForAutoClose , extractPackageNamesFromPRBody } from '../src/utils/helpers'
3433import { Logger } from '../src/utils/logger'
3534
3635const 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 ( / r e f s \/ p u l l \/ ( \d + ) \/ h e a d / )
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 \] < ! - - r e b a s e - c h e c k - - > / 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