Skip to content

Commit 88f690e

Browse files
committed
feat: auto close prs that have been ignored
chore: wip
1 parent 910502c commit 88f690e

File tree

7 files changed

+894
-38
lines changed

7 files changed

+894
-38
lines changed

bin/cli.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bun
2+
/* eslint-disable no-cond-assign */
23

34
import type { BuddyBotConfig } from '../src/types'
45
import fs from 'node:fs'
@@ -837,6 +838,53 @@ cli
837838
}
838839
})
839840

841+
// Helper function to extract file paths from PR body
842+
function extractFilePathsFromPRBody(prBody: string): string[] {
843+
const filePaths: string[] = []
844+
845+
// Look for file paths in the PR body table (File column)
846+
// Format: | [package](url) | version | **file** | status |
847+
const tableRowRegex = /\|\s*\[[^\]]+\]\([^)]*\)\s*\|[^|]*\|\s*\*\*([^*]+)\*\*\s*\|/g
848+
let match
849+
while ((match = tableRowRegex.exec(prBody)) !== null) {
850+
const filePath = match[1].trim()
851+
if (filePath && !filePaths.includes(filePath)) {
852+
filePaths.push(filePath)
853+
}
854+
}
855+
856+
// Also look for bold file paths without full table structure
857+
const boldFileRegex = /\*\*([^*]+\.(?:json|yaml|yml|lock))\*\*/g
858+
while ((match = boldFileRegex.exec(prBody)) !== null) {
859+
const filePath = match[1].trim()
860+
if (filePath && !filePaths.includes(filePath)) {
861+
filePaths.push(filePath)
862+
}
863+
}
864+
865+
// Also look for file paths in a simpler format
866+
// Format: | package | version | file | status |
867+
const simpleTableRowRegex = /\|[^|]+\|[^|]+\|([^|]+)\|[^|]*\|/g
868+
while ((match = simpleTableRowRegex.exec(prBody)) !== null) {
869+
const filePath = match[1].trim()
870+
// Only consider paths that look like file paths (contain / or end with common extensions)
871+
if (filePath && (filePath.includes('/') || /\.(?:json|yaml|yml|lock)$/.test(filePath)) && !filePaths.includes(filePath)) {
872+
filePaths.push(filePath)
873+
}
874+
}
875+
876+
// Look for file mentions in release notes or other sections
877+
const filePathRegex = /(?:^|\s)([\w-]+(?:\/[\w.-]+)*\/[\w.-]+\.(?:json|yaml|yml|lock))(?:\s|$)/gm
878+
while ((match = filePathRegex.exec(prBody)) !== null) {
879+
const filePath = match[1].trim()
880+
if (filePath && !filePaths.includes(filePath)) {
881+
filePaths.push(filePath)
882+
}
883+
}
884+
885+
return filePaths
886+
}
887+
840888
// Helper function to extract package updates from PR body
841889
async function extractPackageUpdatesFromPRBody(body: string): Promise<Array<{ name: string, currentVersion: string, newVersion: string }>> {
842890
const updates: Array<{ name: string, currentVersion: string, newVersion: string }> = []
@@ -847,7 +895,7 @@ async function extractPackageUpdatesFromPRBody(body: string): Promise<Array<{ na
847895
const tableRowRegex = /\|\s*\[([^\]]+)\][^|]*\|\s*\[?`\^?([^`]+)`\s*->\s*`\^?([^`]+)`\]?/g
848896

849897
let match
850-
// eslint-disable-next-line no-cond-assign
898+
851899
while ((match = tableRowRegex.exec(body)) !== null) {
852900
const [, packageName, currentVersion, newVersion] = match
853901
updates.push({
@@ -936,9 +984,38 @@ cli
936984

937985
try {
938986
// Add a comment explaining why the PR was closed
939-
const comment = `🤖 **Auto-closed by Buddy Bot**
987+
let comment = `🤖 **Auto-closed by Buddy Bot**
988+
989+
This PR was automatically closed due to configuration changes.`
990+
991+
// Check if it's a respectLatest issue
992+
const respectLatest = config.packages?.respectLatest ?? true
993+
const prBody = pr.body.toLowerCase()
994+
const dynamicIndicators = ['latest', '*', 'main', 'master', 'develop', 'dev']
995+
const hasDynamicVersions = dynamicIndicators.some(indicator => prBody.includes(indicator))
996+
997+
// Check if it's an ignorePaths issue
998+
const ignorePaths = config.packages?.ignorePaths || []
999+
const filePaths = extractFilePathsFromPRBody(pr.body)
1000+
// eslint-disable-next-line ts/no-require-imports
1001+
const { Glob } = require('bun')
1002+
const ignoredFiles = filePaths.filter((filePath) => {
1003+
const normalizedPath = filePath.replace(/^\.\//, '')
1004+
return ignorePaths.some((pattern) => {
1005+
try {
1006+
const glob = new Glob(pattern)
1007+
return glob.match(normalizedPath)
1008+
}
1009+
catch {
1010+
return false
1011+
}
1012+
})
1013+
})
9401014

941-
This PR was automatically closed because it contains updates for packages with dynamic version indicators (like \`*\`, \`latest\`, etc.) that are now filtered out by the \`respectLatest\` configuration.
1015+
if (respectLatest && hasDynamicVersions) {
1016+
comment += `
1017+
1018+
**Reason:** Contains updates for packages with dynamic version indicators (like \`*\`, \`latest\`, etc.) that are now filtered out by the \`respectLatest\` configuration.
9421019
9431020
**Affected packages:** ${packageNames.join(', ')}
9441021
@@ -949,6 +1026,31 @@ If you need to update these packages to specific versions, you can:
9491026
2. Or manually update the dependency files to use specific versions instead of dynamic indicators
9501027
9511028
This helps maintain the intended behavior of dynamic version indicators while preventing unwanted updates.`
1029+
}
1030+
else if (ignoredFiles.length > 0) {
1031+
comment += `
1032+
1033+
**Reason:** Contains updates for files that are now excluded by the \`ignorePaths\` configuration.
1034+
1035+
**Affected files:** ${ignoredFiles.join(', ')}
1036+
1037+
The \`ignorePaths\` setting now excludes these file paths from dependency updates. This PR was created before these paths were ignored.
1038+
1039+
If you need to include these files again, you can:
1040+
1. Remove or modify the relevant patterns in \`ignorePaths\` in your \`buddy-bot.config.ts\`
1041+
2. Or manually manage dependencies in these paths
1042+
1043+
Current ignore patterns: ${ignorePaths.join(', ')}`
1044+
}
1045+
else {
1046+
comment += `
1047+
1048+
**Reason:** Configuration changes have made this PR obsolete.
1049+
1050+
**Affected packages:** ${packageNames.join(', ')}
1051+
1052+
Please check your \`buddy-bot.config.ts\` configuration for recent changes to \`respectLatest\` or \`ignorePaths\` settings.`
1053+
}
9521054

9531055
await gitProvider.createComment(pr.number, comment)
9541056
await gitProvider.closePullRequest(pr.number)

src/buddy.ts

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* eslint-disable no-console */
1+
/* eslint-disable no-console, no-cond-assign */
22
import type {
33
BuddyBotConfig,
44
DashboardData,
@@ -920,7 +920,7 @@ export class Buddy {
920920
const existingUpdates = new Map<string, { from: string, to: string }>()
921921

922922
let match
923-
// eslint-disable-next-line no-cond-assign
923+
924924
while ((match = packageRegex.exec(existingPRBody)) !== null) {
925925
const [, packageName, fromVersion, toVersion] = match
926926
existingUpdates.set(packageName, { from: fromVersion, to: toVersion })
@@ -994,12 +994,32 @@ export class Buddy {
994994
return Array.from(labels)
995995
}
996996

997+
/**
998+
* Check if a PR should be auto-closed due to configuration changes
999+
* This handles cases where:
1000+
* 1. respectLatest config changed from false to true, making dynamic version updates invalid
1001+
* 2. ignorePaths config changed to exclude paths that existing PRs contain updates for
1002+
*/
1003+
private shouldAutoClosePR(existingPR: PullRequest, _newUpdates: PackageUpdate[]): boolean {
1004+
// Check for respectLatest config changes
1005+
const shouldCloseForRespectLatest = this.shouldAutoCloseForRespectLatest(existingPR)
1006+
if (shouldCloseForRespectLatest) {
1007+
return true
1008+
}
1009+
1010+
// Check for ignorePaths config changes
1011+
const shouldCloseForIgnorePaths = this.shouldAutoCloseForIgnorePaths(existingPR)
1012+
if (shouldCloseForIgnorePaths) {
1013+
return true
1014+
}
1015+
1016+
return false
1017+
}
1018+
9971019
/**
9981020
* Check if a PR should be auto-closed due to respectLatest config changes
999-
* This handles cases where old PRs were created with respectLatest: false
1000-
* but now the config is respectLatest: true, making those updates invalid
10011021
*/
1002-
private shouldAutoClosePR(existingPR: PullRequest, newUpdates: PackageUpdate[]): boolean {
1022+
private shouldAutoCloseForRespectLatest(existingPR: PullRequest): boolean {
10031023
const respectLatest = this.config.packages?.respectLatest ?? true
10041024

10051025
// Only auto-close if respectLatest is true (the new default behavior)
@@ -1021,20 +1041,11 @@ export class Buddy {
10211041
return false
10221042
}
10231043

1024-
// Check if the new updates don't include the same packages that were in the old PR
1025-
// This indicates the packages were filtered out due to respectLatest
1044+
// Check if any packages in the PR have dynamic versions
10261045
const oldPRPackages = this.extractPackagesFromPRBody(existingPR.body)
1027-
const newUpdatePackages = newUpdates.map(update => update.name)
10281046

1029-
// If old PR had packages that are not in new updates, and those packages had dynamic versions
1030-
const missingPackages = oldPRPackages.filter(pkg => !newUpdatePackages.includes(pkg))
1031-
1032-
if (missingPackages.length === 0) {
1033-
return false
1034-
}
1035-
1036-
// Check if the missing packages had dynamic versions in the old PR
1037-
const missingPackagesWithDynamicVersions = missingPackages.filter((pkg) => {
1047+
// Check if any of the packages had dynamic versions in the old PR
1048+
const packagesWithDynamicVersions = oldPRPackages.filter((pkg) => {
10381049
// Look for the package in the PR body table format: | [package](url) | version → newVersion |
10391050
const packagePattern = new RegExp(`\\|\\s*\\[${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\([^)]+\\)\\s*\\|\\s*([^|]+)\\s*\\|`, 'i')
10401051
const match = existingPR.body.match(packagePattern)
@@ -1059,7 +1070,99 @@ export class Buddy {
10591070
return dynamicIndicators.includes(currentVersion)
10601071
})
10611072

1062-
return missingPackagesWithDynamicVersions.length > 0
1073+
return packagesWithDynamicVersions.length > 0
1074+
}
1075+
1076+
/**
1077+
* Check if a PR should be auto-closed due to ignorePaths config changes
1078+
*/
1079+
private shouldAutoCloseForIgnorePaths(existingPR: PullRequest): boolean {
1080+
const ignorePaths = this.config.packages?.ignorePaths
1081+
if (!ignorePaths || ignorePaths.length === 0) {
1082+
return false
1083+
}
1084+
1085+
// Extract file paths from the PR body
1086+
const filePaths = this.extractFilePathsFromPRBody(existingPR.body)
1087+
if (filePaths.length === 0) {
1088+
return false
1089+
}
1090+
1091+
// Check if any of the files in the PR are now in ignored paths
1092+
// eslint-disable-next-line ts/no-require-imports
1093+
const { Glob } = require('bun')
1094+
1095+
const ignoredFiles = filePaths.filter((filePath) => {
1096+
// Normalize the file path (remove leading ./ if present)
1097+
const normalizedPath = filePath.replace(/^\.\//, '')
1098+
1099+
return ignorePaths.some((pattern) => {
1100+
try {
1101+
const glob = new Glob(pattern)
1102+
return glob.match(normalizedPath)
1103+
}
1104+
catch (error) {
1105+
this.logger.debug(`Failed to match path ${normalizedPath} against pattern ${pattern}: ${error}`)
1106+
return false
1107+
}
1108+
})
1109+
})
1110+
1111+
if (ignoredFiles.length > 0) {
1112+
this.logger.debug(`PR #${existingPR.number} contains files now in ignorePaths: ${ignoredFiles.join(', ')}`)
1113+
return true
1114+
}
1115+
1116+
return false
1117+
}
1118+
1119+
/**
1120+
* Extract file paths from PR body
1121+
*/
1122+
private extractFilePathsFromPRBody(prBody: string): string[] {
1123+
const filePaths: string[] = []
1124+
1125+
// Look for file paths in the PR body table (File column)
1126+
// Format: | [package](url) | version | **file** | status |
1127+
const tableRowRegex = /\|\s*\[[^\]]+\]\([^)]*\)\s*\|[^|]*\|\s*\*\*([^*]+)\*\*\s*\|/g
1128+
let match
1129+
while ((match = tableRowRegex.exec(prBody)) !== null) {
1130+
const filePath = match[1].trim()
1131+
if (filePath && !filePaths.includes(filePath)) {
1132+
filePaths.push(filePath)
1133+
}
1134+
}
1135+
1136+
// Also look for bold file paths without full table structure
1137+
const boldFileRegex = /\*\*([^*]+\.(?:json|yaml|yml|lock))\*\*/g
1138+
while ((match = boldFileRegex.exec(prBody)) !== null) {
1139+
const filePath = match[1].trim()
1140+
if (filePath && !filePaths.includes(filePath)) {
1141+
filePaths.push(filePath)
1142+
}
1143+
}
1144+
1145+
// Also look for file paths in a simpler format
1146+
// Format: | package | version | file | status |
1147+
const simpleTableRowRegex = /\|[^|]+\|[^|]+\|([^|]+)\|[^|]*\|/g
1148+
while ((match = simpleTableRowRegex.exec(prBody)) !== null) {
1149+
const filePath = match[1].trim()
1150+
// Only consider paths that look like file paths (contain / or end with common extensions)
1151+
if (filePath && (filePath.includes('/') || /\.(?:json|yaml|yml|lock)$/.test(filePath)) && !filePaths.includes(filePath)) {
1152+
filePaths.push(filePath)
1153+
}
1154+
}
1155+
1156+
// Look for file mentions in release notes or other sections
1157+
const filePathRegex = /(?:^|\s)([\w-]+(?:\/[\w.-]+)*\/[\w.-]+\.(?:json|yaml|yml|lock))(?:\s|$)/gm
1158+
while ((match = filePathRegex.exec(prBody)) !== null) {
1159+
const filePath = match[1].trim()
1160+
if (filePath && !filePaths.includes(filePath)) {
1161+
filePaths.push(filePath)
1162+
}
1163+
}
1164+
1165+
return filePaths
10631166
}
10641167

10651168
/**

0 commit comments

Comments
 (0)