1
- /* eslint-disable no-console, no-cond-assign */
1
+ /* eslint-disable no-console, no-cond-assign, ts/no-require-imports */
2
2
import type {
3
3
BuddyBotConfig ,
4
4
DashboardData ,
@@ -1140,6 +1140,7 @@ export class Buddy {
1140
1140
* This handles cases where:
1141
1141
* 1. respectLatest config changed from false to true, making dynamic version updates invalid
1142
1142
* 2. ignorePaths config changed to exclude paths that existing PRs contain updates for
1143
+ * 3. Dependency files (like composer.json) were removed from the project
1143
1144
*/
1144
1145
private shouldAutoClosePR ( existingPR : PullRequest , _newUpdates : PackageUpdate [ ] ) : boolean {
1145
1146
// Check for respectLatest config changes
@@ -1154,6 +1155,12 @@ export class Buddy {
1154
1155
return true
1155
1156
}
1156
1157
1158
+ // Check for removed dependency files
1159
+ const shouldCloseForRemovedFiles = this . shouldAutoCloseForRemovedFiles ( existingPR )
1160
+ if ( shouldCloseForRemovedFiles ) {
1161
+ return true
1162
+ }
1163
+
1157
1164
return false
1158
1165
}
1159
1166
@@ -1230,7 +1237,7 @@ export class Buddy {
1230
1237
}
1231
1238
1232
1239
// Check if any of the files in the PR are now in ignored paths
1233
- // eslint-disable-next-line ts/no-require-imports
1240
+
1234
1241
const { Glob } = require ( 'bun' )
1235
1242
1236
1243
const ignoredFiles = filePaths . filter ( ( filePath ) => {
@@ -1257,6 +1264,66 @@ export class Buddy {
1257
1264
return false
1258
1265
}
1259
1266
1267
+ /**
1268
+ * Check if a PR should be auto-closed due to removed dependency files
1269
+ * This handles cases where dependency files like composer.json, package.json, etc.
1270
+ * are completely removed from the project, making existing PRs obsolete
1271
+ */
1272
+ private shouldAutoCloseForRemovedFiles ( existingPR : PullRequest ) : boolean {
1273
+ try {
1274
+ // Extract file paths from the PR body
1275
+ const filePaths = this . extractFilePathsFromPRBody ( existingPR . body )
1276
+ if ( filePaths . length === 0 ) {
1277
+ return false
1278
+ }
1279
+
1280
+ // Check if any of the dependency files mentioned in the PR no longer exist
1281
+ const fs = require ( 'node:fs' )
1282
+ const path = require ( 'node:path' )
1283
+
1284
+ const removedFiles = filePaths . filter ( ( filePath ) => {
1285
+ const fullPath = path . join ( this . projectPath , filePath )
1286
+ return ! fs . existsSync ( fullPath )
1287
+ } )
1288
+
1289
+ if ( removedFiles . length > 0 ) {
1290
+ this . logger . info ( `PR #${ existingPR . number } references removed files: ${ removedFiles . join ( ', ' ) } ` )
1291
+
1292
+ // Special handling for composer files - if composer.json is removed, close all composer-related PRs
1293
+ const hasRemovedComposerJson = removedFiles . some ( file => file . endsWith ( 'composer.json' ) )
1294
+ if ( hasRemovedComposerJson ) {
1295
+ this . logger . info ( `composer.json was removed - PR #${ existingPR . number } should be auto-closed` )
1296
+ return true
1297
+ }
1298
+
1299
+ // For other dependency files, check if the PR is specifically about those files
1300
+ const prBodyLower = existingPR . body . toLowerCase ( )
1301
+ const isComposerPR = prBodyLower . includes ( 'composer' )
1302
+ || removedFiles . some ( file => file . includes ( 'composer' ) )
1303
+ const isPackageJsonPR = prBodyLower . includes ( 'package.json' )
1304
+ || removedFiles . some ( file => file . includes ( 'package.json' ) )
1305
+ const isDependencyFilePR = removedFiles . some ( file =>
1306
+ file . endsWith ( 'deps.yaml' )
1307
+ || file . endsWith ( 'deps.yml' )
1308
+ || file . endsWith ( 'dependencies.yaml' )
1309
+ || file . endsWith ( 'dependencies.yml' ) ,
1310
+ )
1311
+
1312
+ // Auto-close if the PR is specifically about the removed dependency management system
1313
+ if ( isComposerPR || isPackageJsonPR || isDependencyFilePR ) {
1314
+ this . logger . info ( `PR #${ existingPR . number } is about removed dependency system - should be auto-closed` )
1315
+ return true
1316
+ }
1317
+ }
1318
+
1319
+ return false
1320
+ }
1321
+ catch ( error ) {
1322
+ this . logger . debug ( `Failed to check for removed files in PR #${ existingPR . number } : ${ error } ` )
1323
+ return false
1324
+ }
1325
+ }
1326
+
1260
1327
/**
1261
1328
* Extract file paths from PR body
1262
1329
*/
@@ -1352,6 +1419,144 @@ export class Buddy {
1352
1419
return this . projectPath
1353
1420
}
1354
1421
1422
+ /**
1423
+ * Check for and auto-close obsolete PRs due to removed dependency files
1424
+ * This is called during the update-check workflow to proactively clean up PRs
1425
+ * when projects stop using certain dependency management systems (like Composer)
1426
+ */
1427
+ async checkAndCloseObsoletePRs ( gitProvider : GitHubProvider , dryRun : boolean = false ) : Promise < void > {
1428
+ try {
1429
+ this . logger . info ( '🔍 Scanning for obsolete PRs due to removed dependency files...' )
1430
+
1431
+ // Get all open PRs
1432
+ const openPRs = await gitProvider . getPullRequests ( 'open' )
1433
+
1434
+ // Filter to buddy-bot PRs and dependency-related PRs
1435
+ const dependencyPRs = openPRs . filter ( pr =>
1436
+ // Include buddy-bot PRs
1437
+ ( pr . head . startsWith ( 'buddy-bot/' ) || pr . author === 'github-actions[bot]' )
1438
+ // Also include other dependency update PRs that might be obsolete
1439
+ || pr . labels . includes ( 'dependencies' )
1440
+ || pr . labels . includes ( 'dependency' )
1441
+ || pr . title . toLowerCase ( ) . includes ( 'update' )
1442
+ || pr . title . toLowerCase ( ) . includes ( 'chore(deps)' )
1443
+ || pr . title . toLowerCase ( ) . includes ( 'composer' ) ,
1444
+ )
1445
+
1446
+ this . logger . info ( `Found ${ dependencyPRs . length } dependency-related PRs to check` )
1447
+
1448
+ let closedCount = 0
1449
+
1450
+ for ( const pr of dependencyPRs ) {
1451
+ try {
1452
+ // Check if this PR should be auto-closed due to removed files
1453
+ const shouldClose = this . shouldAutoCloseForRemovedFiles ( pr )
1454
+
1455
+ if ( shouldClose ) {
1456
+ this . logger . info ( `🔒 PR #${ pr . number } should be auto-closed: ${ pr . title } ` )
1457
+
1458
+ if ( dryRun ) {
1459
+ this . logger . info ( `🔍 [DRY RUN] Would auto-close PR #${ pr . number } ` )
1460
+ closedCount ++
1461
+ }
1462
+ else {
1463
+ try {
1464
+ // Close the PR with a helpful comment
1465
+ const closeReason = this . generateCloseReason ( pr )
1466
+
1467
+ // Add comment explaining why the PR is being closed
1468
+ try {
1469
+ await gitProvider . createComment ( pr . number , closeReason )
1470
+ }
1471
+ catch ( commentError ) {
1472
+ this . logger . warn ( `⚠️ Could not add close reason comment to PR #${ pr . number } :` , commentError )
1473
+ }
1474
+
1475
+ await gitProvider . closePullRequest ( pr . number )
1476
+
1477
+ // Try to delete the branch if it's a buddy-bot branch
1478
+ if ( pr . head . startsWith ( 'buddy-bot/' ) ) {
1479
+ try {
1480
+ await gitProvider . deleteBranch ( pr . head )
1481
+ this . logger . success ( `✅ Auto-closed PR #${ pr . number } and deleted branch ${ pr . head } ` )
1482
+ }
1483
+ catch ( branchError ) {
1484
+ this . logger . warn ( `⚠️ Auto-closed PR #${ pr . number } but failed to delete branch: ${ branchError } ` )
1485
+ }
1486
+ }
1487
+ else {
1488
+ this . logger . success ( `✅ Auto-closed PR #${ pr . number } ` )
1489
+ }
1490
+
1491
+ closedCount ++
1492
+ }
1493
+ catch ( closeError ) {
1494
+ this . logger . error ( `❌ Failed to auto-close PR #${ pr . number } :` , closeError )
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ catch ( error ) {
1500
+ this . logger . warn ( `⚠️ Error checking PR #${ pr . number } :` , error )
1501
+ }
1502
+ }
1503
+
1504
+ if ( closedCount > 0 ) {
1505
+ this . logger . success ( `✅ ${ dryRun ? 'Would auto-close' : 'Auto-closed' } ${ closedCount } obsolete PR(s)` )
1506
+ }
1507
+ else {
1508
+ this . logger . info ( '📋 No obsolete PRs found' )
1509
+ }
1510
+ }
1511
+ catch ( error ) {
1512
+ this . logger . error ( 'Failed to check for obsolete PRs:' , error )
1513
+ throw error
1514
+ }
1515
+ }
1516
+
1517
+ /**
1518
+ * Generate a helpful close reason comment for auto-closed PRs
1519
+ */
1520
+ private generateCloseReason ( pr : PullRequest ) : string {
1521
+ const filePaths = this . extractFilePathsFromPRBody ( pr . body )
1522
+ const removedFiles = filePaths . filter ( ( filePath ) => {
1523
+ const fs = require ( 'node:fs' )
1524
+ const path = require ( 'node:path' )
1525
+ const fullPath = path . join ( this . projectPath , filePath )
1526
+ return ! fs . existsSync ( fullPath )
1527
+ } )
1528
+
1529
+ const hasRemovedComposer = removedFiles . some ( file => file . includes ( 'composer' ) )
1530
+ const hasRemovedPackageJson = removedFiles . some ( file => file . includes ( 'package.json' ) )
1531
+ const hasRemovedDeps = removedFiles . some ( file =>
1532
+ file . endsWith ( 'deps.yaml' ) || file . endsWith ( 'deps.yml' )
1533
+ || file . endsWith ( 'dependencies.yaml' ) || file . endsWith ( 'dependencies.yml' ) ,
1534
+ )
1535
+
1536
+ let reason = '🤖 **Auto-closing obsolete PR**\n\n'
1537
+
1538
+ if ( hasRemovedComposer ) {
1539
+ reason += 'This PR was automatically closed because `composer.json` has been removed from the project, indicating that Composer is no longer used for dependency management.\n\n'
1540
+ }
1541
+ else if ( hasRemovedPackageJson ) {
1542
+ reason += 'This PR was automatically closed because `package.json` has been removed from the project, indicating that npm/yarn/pnpm is no longer used for dependency management.\n\n'
1543
+ }
1544
+ else if ( hasRemovedDeps ) {
1545
+ reason += 'This PR was automatically closed because the dependency files it references have been removed from the project.\n\n'
1546
+ }
1547
+ else {
1548
+ reason += 'This PR was automatically closed because the dependency files it references are no longer present in the project.\n\n'
1549
+ }
1550
+
1551
+ if ( removedFiles . length > 0 ) {
1552
+ reason += `**Removed files:**\n${ removedFiles . map ( file => `- \`${ file } \`` ) . join ( '\n' ) } \n\n`
1553
+ }
1554
+
1555
+ reason += 'If this was closed in error, please reopen the PR and update the dependency files accordingly.'
1556
+
1557
+ return reason
1558
+ }
1559
+
1355
1560
/**
1356
1561
* Create or update dependency dashboard
1357
1562
*/
0 commit comments