Skip to content

Commit b22782d

Browse files
committed
chore: improve auto closing of obsolete prs
1 parent f82d180 commit b22782d

File tree

3 files changed

+380
-3
lines changed

3 files changed

+380
-3
lines changed

bin/cli.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,18 @@ cli
10051005
logger.warn('⚠️ Could not check for rebase requests:', error)
10061006
}
10071007

1008-
// Step 2: Run branch cleanup (uses local git commands, no API calls)
1008+
// Step 2: Check for obsolete PRs (composer files removed, etc.)
1009+
logger.info('\n🔍 Checking for obsolete PRs due to removed dependency files...')
1010+
try {
1011+
const { Buddy } = await import('../src/buddy')
1012+
const buddy = new Buddy(config)
1013+
await buddy.checkAndCloseObsoletePRs(gitProvider, !!options.dryRun)
1014+
}
1015+
catch (error) {
1016+
logger.warn('⚠️ Could not check for obsolete PRs:', error)
1017+
}
1018+
1019+
// Step 3: Run branch cleanup (uses local git commands, no API calls)
10091020
logger.info('\n🧹 Running branch cleanup...')
10101021
const result = await gitProvider.cleanupStaleBranches(2, !!options.dryRun)
10111022

src/buddy.ts

Lines changed: 207 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* eslint-disable no-console, no-cond-assign */
1+
/* eslint-disable no-console, no-cond-assign, ts/no-require-imports */
22
import type {
33
BuddyBotConfig,
44
DashboardData,
@@ -1140,6 +1140,7 @@ export class Buddy {
11401140
* This handles cases where:
11411141
* 1. respectLatest config changed from false to true, making dynamic version updates invalid
11421142
* 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
11431144
*/
11441145
private shouldAutoClosePR(existingPR: PullRequest, _newUpdates: PackageUpdate[]): boolean {
11451146
// Check for respectLatest config changes
@@ -1154,6 +1155,12 @@ export class Buddy {
11541155
return true
11551156
}
11561157

1158+
// Check for removed dependency files
1159+
const shouldCloseForRemovedFiles = this.shouldAutoCloseForRemovedFiles(existingPR)
1160+
if (shouldCloseForRemovedFiles) {
1161+
return true
1162+
}
1163+
11571164
return false
11581165
}
11591166

@@ -1230,7 +1237,7 @@ export class Buddy {
12301237
}
12311238

12321239
// Check if any of the files in the PR are now in ignored paths
1233-
// eslint-disable-next-line ts/no-require-imports
1240+
12341241
const { Glob } = require('bun')
12351242

12361243
const ignoredFiles = filePaths.filter((filePath) => {
@@ -1257,6 +1264,66 @@ export class Buddy {
12571264
return false
12581265
}
12591266

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+
12601327
/**
12611328
* Extract file paths from PR body
12621329
*/
@@ -1352,6 +1419,144 @@ export class Buddy {
13521419
return this.projectPath
13531420
}
13541421

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+
13551560
/**
13561561
* Create or update dependency dashboard
13571562
*/

0 commit comments

Comments
 (0)