Skip to content

Commit 6012d18

Browse files
committed
chore: several dashboard improvements
chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip
1 parent 0c7b00a commit 6012d18

File tree

6 files changed

+275
-86
lines changed

6 files changed

+275
-86
lines changed

src/buddy.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -760,15 +760,11 @@ export class Buddy {
760760
assignees: dashboardConfig.assignees,
761761
})
762762

763-
// Pin the issue if configured (even if it's an existing issue)
763+
// Note: GitHub REST API does not support pinning issues programmatically
764+
// Users need to pin the dashboard issue manually through the GitHub UI
764765
if (dashboardConfig.pin) {
765-
try {
766-
await gitProvider.pinIssue(issue.number)
767-
this.logger.info(`📌 Pinned existing dashboard issue #${issue.number}`)
768-
}
769-
catch (error) {
770-
this.logger.warn(`Failed to pin dashboard issue: ${error}`)
771-
}
766+
this.logger.info(`💡 To pin dashboard issue #${issue.number}, please do so manually in the GitHub UI`)
767+
this.logger.info(`📌 Go to: ${issue.url} and click "Pin issue" in the right sidebar`)
772768
}
773769
}
774770
else {
@@ -782,15 +778,11 @@ export class Buddy {
782778
assignees: dashboardConfig.assignees,
783779
})
784780

785-
// Pin the issue if configured
781+
// Note: GitHub REST API does not support pinning issues programmatically
782+
// Users need to pin the dashboard issue manually through the GitHub UI
786783
if (dashboardConfig.pin) {
787-
try {
788-
await gitProvider.pinIssue(issue.number)
789-
this.logger.info(`📌 Pinned new dashboard issue #${issue.number}`)
790-
}
791-
catch (error) {
792-
this.logger.warn(`Failed to pin dashboard issue: ${error}`)
793-
}
784+
this.logger.info(`💡 To pin dashboard issue #${issue.number}, please do so manually in the GitHub UI`)
785+
this.logger.info(`📌 Go to: ${issue.url} and click "Pin issue" in the right sidebar`)
794786
}
795787
}
796788

@@ -814,9 +806,15 @@ export class Buddy {
814806

815807
// Filter PRs to only include dependency updates (likely created by buddy-bot)
816808
const dependencyPRs = openPRs.filter(pr =>
817-
pr.labels.includes('dependencies')
818-
|| pr.title.toLowerCase().includes('update')
819-
|| pr.title.toLowerCase().includes('chore(deps)'),
809+
// Exclude Renovate PRs
810+
!pr.author.toLowerCase().includes('renovate')
811+
&& !pr.head.includes('renovate/')
812+
&& !pr.title.toLowerCase().includes('renovate')
813+
&& (
814+
pr.labels.includes('dependencies')
815+
|| pr.title.toLowerCase().includes('update')
816+
|| pr.title.toLowerCase().includes('chore(deps)')
817+
),
820818
)
821819

822820
// Categorize package files

src/dashboard/dashboard-generator.ts

Lines changed: 199 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */
12
import type { DashboardData, PackageFile, PullRequest } from '../types'
23

34
export class DashboardGenerator {
@@ -42,10 +43,10 @@ export class DashboardGenerator {
4243
/**
4344
* Generate the default header section
4445
*/
45-
private generateDefaultHeader(data: DashboardData): string {
46-
const portalUrl = `https://developer.mend.io/github/${data.repository.owner}/${data.repository.name}`
46+
private generateDefaultHeader(_data: DashboardData): string {
47+
// const portalUrl = `https://developer.mend.io/github/${data.repository.owner}/${data.repository.name}`
4748

48-
return `This issue lists Buddy Bot updates and detected dependencies. Read the [Dependency Dashboard](https://buddy-bot.sh/features/dependency-dashboard) docs to learn more.<br/>[View this repository on the Mend.io Web Portal](${portalUrl}).
49+
return `This issue lists Buddy Bot updates and detected dependencies. Read the [Dependency Dashboard](https://buddy-bot.sh/features/dependency-dashboard) docs to learn more.
4950
5051
`
5152
}
@@ -72,6 +73,7 @@ The following updates have all been created. To force a retry/rebase of any, cli
7273

7374
section += ` - [ ] <!-- rebase-branch=${rebaseBranch} -->[${pr.title}](${relativeUrl})`
7475

76+
// Show clean package names like Renovate does (without version info)
7577
if (packageInfo.length > 0) {
7678
section += ` (\`${packageInfo.join('`, `')}\`)`
7779
}
@@ -247,58 +249,219 @@ The following updates have all been created. To force a retry/rebase of any, cli
247249
}
248250

249251
/**
250-
* Extract package names from PR title or body
252+
* Extract package names from PR title or body (like Renovate does)
251253
*/
252254
private extractPackageInfo(pr: PullRequest): string[] {
253255
const packages: string[] = []
254256

255-
// Try to extract from title patterns like "update dependency react to v18"
256-
const titleMatch = pr.title.match(/update.*?dependency\s+(\w+)/i)
257-
if (titleMatch) {
258-
packages.push(titleMatch[1])
257+
// Add logging for debugging the real issue
258+
const isTargetPR = pr.title.includes('update all non-major dependencies')
259+
if (isTargetPR) {
260+
console.log(`\n🔍 DEBUG: Extracting from PR #${pr.number}`)
261+
console.log(`📝 Title: "${pr.title}"`)
262+
console.log(`📄 Body preview: "${pr.body.substring(0, 200)}..."\n`)
259263
}
260264

261-
// Extract from the enhanced PR body format
262-
// Look for table entries like: | [package-name](url) | version change | badges |
263-
const tableMatches = pr.body.match(/\|\s*\[([^\]]+)\]/g)
264-
if (tableMatches) {
265-
for (const match of tableMatches) {
266-
const packageMatch = match.match(/\|\s*\[([^\]]+)\]/)
267-
if (packageMatch) {
268-
const packageName = packageMatch[1]
269-
// Skip if it's a URL, badge, or contains special characters that indicate it's not a package name
270-
if (!packageName.includes('://')
271-
&& !packageName.includes('Compare Source')
272-
&& !packageName.includes('badge')
273-
&& !packageName.includes('!')
274-
&& !packageName.startsWith('[![')
275-
&& !packages.includes(packageName)) {
276-
packages.push(packageName)
265+
// Pattern 1: Extract from common dependency update titles
266+
// Examples: "chore(deps): update dependency react to v18"
267+
// "chore(deps): update all non-major dependencies"
268+
// "update @types/node to v20"
269+
const titlePatterns = [
270+
/update.*?dependency\s+(\S+)/i,
271+
/update\s+(\S+)\s+to\s+v?\d+/i,
272+
/bump\s+(\S+)\s+from/i,
273+
]
274+
275+
for (const pattern of titlePatterns) {
276+
const match = pr.title.match(pattern)
277+
if (match && match[1] && !packages.includes(match[1])) {
278+
packages.push(match[1])
279+
if (isTargetPR)
280+
console.log(`✅ Title match: "${match[1]}"`)
281+
}
282+
}
283+
284+
// Pattern 2: Extract from table format in PR body - handle all table sections
285+
// Look for different table types: npm Dependencies, Launchpad/pkgx Dependencies, GitHub Actions
286+
287+
// Split the body into sections and process each table
288+
const tableSections = [
289+
// npm Dependencies table
290+
{ name: 'npm', pattern: /### npm Dependencies[\s\S]*?(?=###|\n\n---|z)/i },
291+
// Launchpad/pkgx Dependencies table
292+
{ name: 'pkgx', pattern: /### Launchpad\/pkgx Dependencies[\s\S]*?(?=###|\n\n---|z)/i },
293+
// GitHub Actions table
294+
{ name: 'actions', pattern: /### GitHub Actions[\s\S]*?(?=###|\n\n---|z)/i },
295+
]
296+
297+
for (const section of tableSections) {
298+
const sectionMatch = pr.body.match(section.pattern)
299+
if (sectionMatch) {
300+
const sectionContent = sectionMatch[0]
301+
if (isTargetPR)
302+
console.log(`📊 Found ${section.name} section`)
303+
304+
// Extract package names from this section's table
305+
const tableRowMatches = sectionContent.match(/\|\s*\[([^\]]+)\]\([^)]+\)\s*\|/g)
306+
if (tableRowMatches) {
307+
if (isTargetPR) {
308+
console.log(`📊 ${section.name} table matches: ${tableRowMatches.length}`)
309+
console.log(`📋 Raw ${section.name} matches:`, JSON.stringify(tableRowMatches))
310+
}
311+
312+
for (const match of tableRowMatches) {
313+
const packageMatch = match.match(/\|\s*\[([^\]]+)\]/)
314+
if (packageMatch && packageMatch[1]) {
315+
const packageName = packageMatch[1].trim()
316+
317+
if (isTargetPR) {
318+
console.log(`🔍 Checking ${section.name}: "${packageName}"`)
319+
}
320+
321+
// Check if this looks like a version string - if so, try to extract package name from URL
322+
if (packageName.includes('`') && packageName.includes('->')) {
323+
// This is a version string like "`1.2.17` -> `1.2.19`"
324+
// Try to extract the package name from the URL
325+
const urlMatch = match.match(/\]\(([^)]+)\)/)
326+
if (urlMatch && urlMatch[1]) {
327+
const url = urlMatch[1]
328+
if (isTargetPR) {
329+
console.log(`🔗 Extracting from URL: "${url}"`)
330+
}
331+
332+
// Extract package name from Renovate diff URLs like:
333+
// https://renovatebot.com/diffs/npm/%40types%2Fbun/1.2.17/1.2.19
334+
// https://renovatebot.com/diffs/npm/cac/6.7.13/6.7.14
335+
const diffUrlMatch = url.match(/\/diffs\/npm\/([^/]+)\//)
336+
if (diffUrlMatch && diffUrlMatch[1]) {
337+
// Decode URL encoding like %40types%2Fbun -> @types/bun
338+
const extractedPackage = decodeURIComponent(diffUrlMatch[1])
339+
if (isTargetPR) {
340+
console.log(`📦 Extracted from URL: "${extractedPackage}"`)
341+
}
342+
343+
if (extractedPackage && extractedPackage.length > 1 && !packages.includes(extractedPackage)) {
344+
packages.push(extractedPackage)
345+
if (isTargetPR)
346+
console.log(`✅ ${section.name} (from URL): "${extractedPackage}"`)
347+
}
348+
}
349+
else if (isTargetPR) {
350+
console.log(`❌ Could not extract package from URL: "${url}"`)
351+
}
352+
}
353+
continue // Skip the normal processing for version strings
354+
}
355+
356+
// Normal processing for direct package names
357+
// Skip if it's a URL, badge, or contains special characters
358+
// CRITICAL: Skip version strings like "`1.2.17` -> `1.2.19`"
359+
if (!packageName.includes('://')
360+
&& !packageName.includes('Compare Source')
361+
&& !packageName.includes('badge')
362+
&& !packageName.includes('!')
363+
&& !packageName.startsWith('[![')
364+
&& !packageName.includes('`') // Skip anything with backticks (version strings)
365+
&& !packageName.includes('->') // Skip version arrows
366+
&& !packageName.includes(' -> ') // Skip spaced version arrows
367+
&& !packageName.match(/^\d+\.\d+/) // Skip version numbers
368+
&& !packageName.includes(' ') // Package names shouldn't have spaces
369+
&& packageName.length > 0
370+
&& !packages.includes(packageName)) {
371+
packages.push(packageName)
372+
if (isTargetPR)
373+
console.log(`✅ ${section.name}: "${packageName}"`)
374+
}
375+
else if (isTargetPR) {
376+
console.log(`❌ ${section.name} skipped: "${packageName}" (likely version string)`)
377+
}
378+
}
379+
}
380+
}
381+
else if (isTargetPR) {
382+
console.log(`❌ No table matches in ${section.name} section`)
383+
// Let's also try a simpler approach - look for table rows with package names
384+
const simpleRowMatches = sectionContent.match(/^\|\s*\[([^\]]+)\]/gm)
385+
if (simpleRowMatches) {
386+
console.log(`🔄 Trying simpler pattern, found ${simpleRowMatches.length} rows:`, JSON.stringify(simpleRowMatches))
277387
}
278388
}
279389
}
390+
else if (isTargetPR) {
391+
console.log(`❌ No ${section.name} section found`)
392+
}
280393
}
281394

282-
// Fallback: try to extract from simple backtick patterns
283-
if (packages.length === 0) {
395+
// Pattern 3: Extract from PR body - look for package names in backticks (avoid table content)
396+
// This handles cases where the title doesn't contain specific package names
397+
// but the body lists them like: `@types/bun`, `cac`, `ts-pkgx`
398+
if (packages.length < 3) { // Allow backtick extraction to supplement table extraction
284399
const bodyMatches = pr.body.match(/`([^`]+)`/g)
285400
if (bodyMatches) {
401+
if (isTargetPR) {
402+
console.log(`🔍 Backtick matches (${bodyMatches.length}):`)
403+
console.log(JSON.stringify(bodyMatches.slice(0, 10))) // Show first 10
404+
}
405+
286406
for (const match of bodyMatches) {
287-
const content = match.replace(/`/g, '')
288-
// Only extract if it looks like a package name (no version arrows, URLs, or special chars)
289-
if (!content.includes('->')
290-
&& !content.includes('://')
291-
&& !content.includes(' ')
292-
&& !content.includes('Compare Source')
293-
&& content.length > 0
294-
&& !packages.includes(content)) {
295-
packages.push(content)
407+
let packageName = match.replace(/`/g, '').trim()
408+
409+
// Skip anything that looks like version information
410+
if (packageName.includes('->')
411+
|| packageName.includes(' -> ')
412+
|| packageName.includes('` -> `')
413+
|| packageName.match(/^\d+\.\d+/) // Starts with version number
414+
|| packageName.match(/^v\d+/) // Version tags
415+
|| packageName.match(/[\d.]+\s*->\s*[\d.]+/) // Version arrows
416+
|| packageName.match(/^[\d.]+$/) // Pure version numbers like "1.2.17"
417+
|| packageName.match(/^\d+\.\d+\.\d+/) // Semver patterns
418+
|| packageName.match(/^\d+\.\d+\.\d+\./) // Longer version patterns
419+
|| packageName.match(/^\^?\d+\.\d+/) // Version ranges like "^1.2.3"
420+
|| packageName.match(/^~\d+\.\d+/) // Tilde version ranges
421+
|| packageName.includes('://') // URLs with protocol
422+
|| packageName.includes('Compare Source')
423+
|| packageName.includes('badge')
424+
|| packageName.includes(' ')) { // Package names shouldn't have spaces
425+
if (isTargetPR)
426+
console.log(`❌ Skipped: "${packageName}"`)
427+
continue
428+
}
429+
430+
// Clean up the package name - take first part only
431+
packageName = packageName.split(',')[0].trim()
432+
433+
// Only include if it looks like a valid package/dependency name
434+
if (packageName
435+
&& packageName.length > 1
436+
&& !packages.includes(packageName)
437+
&& (
438+
// Must match one of these patterns for valid package names:
439+
packageName.startsWith('@') // Scoped packages like @types/node
440+
|| packageName.includes('/') // GitHub actions like actions/checkout
441+
|| packageName.match(/^[a-z][a-z0-9.-]*$/i) // Simple package names like lodash, ts-pkgx, bun.com
442+
)) {
443+
packages.push(packageName)
444+
if (isTargetPR)
445+
console.log(`✅ Added: "${packageName}"`)
446+
}
447+
else if (isTargetPR) {
448+
console.log(`❌ Failed validation: "${packageName}"`)
296449
}
297450
}
298451
}
452+
else if (isTargetPR) {
453+
console.log(`❌ No backtick matches found`)
454+
}
455+
}
456+
else if (isTargetPR) {
457+
console.log(`⏭️ Skipping backticks (have ${packages.length} packages)`)
299458
}
300459

301-
return packages.slice(0, 5) // Limit to first 5 packages to keep it clean
460+
// NO LIMIT - return all packages like Renovate does
461+
if (isTargetPR) {
462+
console.log(`\n📋 FINAL RESULT: ${JSON.stringify(packages)} (${packages.length} packages)\n`)
463+
}
464+
return packages
302465
}
303466

304467
/**

src/git/github-provider.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -658,29 +658,17 @@ export class GitHubProvider implements GitProvider {
658658
}
659659
}
660660

661-
async pinIssue(issueNumber: number): Promise<void> {
662-
try {
663-
// GitHub's pin/unpin issue API requires special headers and is relatively new
664-
await this.apiRequest(`PUT /repos/${this.owner}/${this.repo}/issues/${issueNumber}/pin`, undefined)
665-
666-
console.log(`✅ Pinned issue #${issueNumber}`)
667-
}
668-
catch (error) {
669-
console.warn(`⚠️ Failed to pin issue #${issueNumber}:`, error)
670-
// Don't throw error for pinning failures as it's not critical
671-
}
672-
}
673-
674661
async unpinIssue(issueNumber: number): Promise<void> {
675662
try {
676-
// GitHub's pin/unpin issue API requires special headers and is relatively new
677663
await this.apiRequest(`DELETE /repos/${this.owner}/${this.repo}/issues/${issueNumber}/pin`, undefined)
678-
679-
console.log(`✅ Unpinned issue #${issueNumber}`)
680664
}
681-
catch (error) {
682-
console.warn(`⚠️ Failed to unpin issue #${issueNumber}:`, error)
683-
// Don't throw error for unpinning failures as it's not critical
665+
catch (error: any) {
666+
console.log(`⚠️ Failed to unpin issue #${issueNumber}:`, error)
667+
// Don't throw error for pinning failures as it's not critical
684668
}
685669
}
670+
671+
// Note: GitHub REST API does not support pinning issues programmatically
672+
// Pinning can only be done manually through the GitHub web interface
673+
// See: https://docs.github.com/en/issues/tracking-your-work-with-issues/administering-issues/pinning-an-issue-to-your-repository
686674
}

src/git/gitlab-provider.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,8 @@ export class GitLabProvider implements GitProvider {
9797
console.log(`Would close issue #${issueNumber}`)
9898
}
9999

100-
async pinIssue(issueNumber: number): Promise<void> {
101-
// TODO: Implement GitLab API call to pin issue
102-
console.log(`Would pin issue #${issueNumber}`)
103-
}
104-
105-
async unpinIssue(issueNumber: number): Promise<void> {
106-
// TODO: Implement GitLab API call to unpin issue
107-
console.log(`Would unpin issue #${issueNumber}`)
100+
async unpinIssue(_issueNumber: number): Promise<void> {
101+
// GitLab doesn't have issue pinning functionality
102+
console.log(`ℹ️ GitLab does not support issue pinning`)
108103
}
109104
}

0 commit comments

Comments
 (0)