@@ -30,7 +30,6 @@ import {
30
30
validateRepositoryAccess ,
31
31
validateWorkflowGeneration ,
32
32
} from '../src/setup'
33
- import { checkForAutoClose , extractPackageNamesFromPRBody } from '../src/utils/helpers'
34
33
import { Logger } from '../src/utils/logger'
35
34
36
35
const cli = new CAC ( 'buddy-bot' )
@@ -944,280 +943,131 @@ cli
944
943
hasWorkflowPermissions ,
945
944
)
946
945
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...' )
995
948
996
949
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
1053
951
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 ] ) )
1087
965
}
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 )
1097
966
}
1098
967
}
1099
968
}
1100
969
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` )
1106
971
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 )
1113
976
977
+ for ( const prNumber of batch ) {
1114
978
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' } ,
1127
983
} )
1128
984
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 ++
1134
988
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 )
1149
992
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` )
1152
995
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
+ }
1172
1031
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 ) )
1175
1034
}
1176
1035
catch ( error ) {
1177
- logger . error ( `❌ Failed to rebase PR #${ pr . number } :`, error )
1036
+ logger . warn ( `⚠️ Could not check PR #${ prNumber } :`, error )
1178
1037
}
1179
1038
}
1039
+
1040
+ // Delay between batches
1041
+ if ( i + batchSize < Math . min ( prNumbers . length , 20 ) ) {
1042
+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) )
1043
+ }
1180
1044
}
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' )
1183
1051
}
1184
1052
}
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 )
1188
1055
}
1189
1056
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 )
1193
1060
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` )
1196
1066
}
1197
1067
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` )
1221
1071
}
1222
1072
}
1223
1073
catch ( error ) {
0 commit comments