Skip to content

Commit 6b4e170

Browse files
committed
chore: pr improvements
chore: wip
1 parent e9d10d8 commit 6b4e170

File tree

6 files changed

+461
-17
lines changed

6 files changed

+461
-17
lines changed

bin/cli.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -688,18 +688,20 @@ cli
688688
process.exit(1)
689689
}
690690

691-
// Get GitHub token from environment
692-
const token = process.env.GITHUB_TOKEN
691+
// Get GitHub token from environment (prefer BUDDY_BOT_TOKEN for full permissions)
692+
const token = process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN
693693
if (!token) {
694-
logger.error('❌ GITHUB_TOKEN environment variable required for PR operations')
694+
logger.error('❌ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for PR operations')
695695
process.exit(1)
696696
}
697697

698698
const { GitHubProvider } = await import('../src/git/github-provider')
699+
const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN
699700
const gitProvider = new GitHubProvider(
700701
token,
701702
config.repository.owner,
702703
config.repository.name,
704+
hasWorkflowPermissions,
703705
)
704706

705707
const prNum = Number.parseInt(prNumber)
@@ -864,18 +866,20 @@ cli
864866
process.exit(1)
865867
}
866868

867-
// Get GitHub token from environment
868-
const token = process.env.GITHUB_TOKEN
869+
// Get GitHub token from environment (prefer BUDDY_BOT_TOKEN for full permissions)
870+
const token = process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN
869871
if (!token) {
870-
logger.error('❌ GITHUB_TOKEN environment variable required for PR operations')
872+
logger.error('❌ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for PR operations')
871873
process.exit(1)
872874
}
873875

874876
const { GitHubProvider } = await import('../src/git/github-provider')
877+
const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN
875878
const gitProvider = new GitHubProvider(
876879
token,
877880
config.repository.owner,
878881
config.repository.name,
882+
hasWorkflowPermissions,
879883
)
880884

881885
// Get all open PRs

src/buddy.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,22 @@ export class Buddy {
117117
return
118118
}
119119

120-
// Get GitHub token from environment
121-
const token = process.env.GITHUB_TOKEN
120+
// Get GitHub token from environment (prefer BUDDY_BOT_TOKEN for full permissions)
121+
const token = process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN
122122
if (!token) {
123-
this.logger.error('❌ GITHUB_TOKEN environment variable required for PR creation')
123+
this.logger.error('❌ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for PR creation')
124124
return
125125
}
126126

127+
// Determine if we have workflow permissions (BUDDY_BOT_TOKEN has full permissions)
128+
const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN
129+
127130
// Initialize GitHub provider
128131
const gitProvider = new GitHubProvider(
129132
token,
130133
this.config.repository.owner,
131134
this.config.repository.name,
135+
hasWorkflowPermissions,
132136
)
133137

134138
// Initialize PR generator with config
@@ -877,10 +881,13 @@ export class Buddy {
877881
}
878882

879883
// Initialize git provider
884+
const token = this.config.repository.token || process.env.BUDDY_BOT_TOKEN || process.env.GITHUB_TOKEN || ''
885+
const hasWorkflowPermissions = !!process.env.BUDDY_BOT_TOKEN
880886
const gitProvider = new GitHubProvider(
881-
this.config.repository.token || process.env.GITHUB_TOKEN || '',
887+
token,
882888
this.config.repository.owner,
883889
this.config.repository.name,
890+
hasWorkflowPermissions,
884891
)
885892

886893
// Collect dashboard data

src/git/github-provider.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class GitHubProvider implements GitProvider {
1111
private readonly token: string,
1212
private readonly owner: string,
1313
private readonly repo: string,
14+
private readonly hasWorkflowPermissions: boolean = false,
1415
) {}
1516

1617
async createBranch(branchName: string, baseBranch: string): Promise<void> {
@@ -46,14 +47,14 @@ export class GitHubProvider implements GitProvider {
4647

4748
private async commitChangesWithGit(branchName: string, message: string, files: FileChange[]): Promise<void> {
4849
try {
49-
// Filter out workflow files since they require special permissions
50+
// Handle workflow files based on token permissions
5051
const workflowFiles = files.filter(f => f.path.includes('.github/workflows/'))
5152
const nonWorkflowFiles = files.filter(f => !f.path.includes('.github/workflows/'))
5253

53-
if (workflowFiles.length > 0) {
54+
if (workflowFiles.length > 0 && !this.hasWorkflowPermissions) {
5455
console.warn(`⚠️ Detected ${workflowFiles.length} workflow file(s). These require elevated permissions.`)
5556
console.warn(`⚠️ Workflow files: ${workflowFiles.map(f => f.path).join(', ')}`)
56-
console.warn(`ℹ️ Workflow files will be skipped in this commit. Consider using a GitHub App with workflow permissions for workflow updates.`)
57+
console.warn(`ℹ️ Workflow files will be skipped in this commit. You need to set BUDDY_BOT_TOKEN (with workflow permissions) in the repository settings.`)
5758

5859
// If we have non-workflow files, commit just those
5960
if (nonWorkflowFiles.length > 0) {
@@ -62,10 +63,13 @@ export class GitHubProvider implements GitProvider {
6263
}
6364
else {
6465
console.warn(`⚠️ All files are workflow files. No files will be committed in this PR.`)
65-
console.warn(`💡 To update workflow files, consider using a GitHub App with appropriate permissions.`)
66+
console.warn(`💡 To update workflow files, you need to set BUDDY_BOT_TOKEN (with workflow permissions) in the repository settings.`)
6667
return // Exit early if no non-workflow files to commit
6768
}
6869
}
70+
else if (workflowFiles.length > 0) {
71+
console.log(`✅ Including ${workflowFiles.length} workflow file(s) with elevated permissions`)
72+
}
6973

7074
// Configure Git identity if not already set
7175
// try {
@@ -150,11 +154,11 @@ export class GitHubProvider implements GitProvider {
150154

151155
private async commitChangesWithAPI(branchName: string, message: string, files: FileChange[]): Promise<void> {
152156
try {
153-
// Filter out workflow files since they require special permissions
157+
// Handle workflow files based on token permissions
154158
const workflowFiles = files.filter(f => f.path.includes('.github/workflows/'))
155159
const nonWorkflowFiles = files.filter(f => !f.path.includes('.github/workflows/'))
156160

157-
if (workflowFiles.length > 0) {
161+
if (workflowFiles.length > 0 && !this.hasWorkflowPermissions) {
158162
console.warn(`⚠️ Detected ${workflowFiles.length} workflow file(s). These require elevated permissions.`)
159163
console.warn(`⚠️ Workflow files: ${workflowFiles.map(f => f.path).join(', ')}`)
160164
console.warn(`ℹ️ Workflow files will be skipped in this commit. Consider using a GitHub App with workflow permissions for workflow updates.`)
@@ -170,6 +174,9 @@ export class GitHubProvider implements GitProvider {
170174
return // Exit early if no non-workflow files to commit
171175
}
172176
}
177+
else if (workflowFiles.length > 0) {
178+
console.log(`✅ Including ${workflowFiles.length} workflow file(s) with elevated permissions`)
179+
}
173180

174181
// Get current branch SHA
175182
const branchRef = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/git/ref/heads/${branchName}`)

src/pr/pr-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ReleaseNotesFetcher } from '../services/release-notes-fetcher'
55
export class PullRequestGenerator {
66
private releaseNotesFetcher = new ReleaseNotesFetcher()
77

8-
constructor(private readonly config?: BuddyBotConfig) {}
8+
constructor(private readonly config?: BuddyBotConfig | undefined) {}
99

1010
/**
1111
* Generate pull requests for update groups

test/pr-body-major-updates.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import type { UpdateGroup } from '../src/types'
2+
import { describe, expect, it } from 'bun:test'
3+
import { PullRequestGenerator } from '../src/pr/pr-generator'
4+
5+
describe('PR Body Generation for Major Updates', () => {
6+
const generator = new PullRequestGenerator()
7+
8+
describe('single major update issue reproduction', () => {
9+
it('should generate complete PR body for major stripe update (original issue)', async () => {
10+
// This test reproduces the exact scenario from the user's issue
11+
const majorStripeUpdate: UpdateGroup = {
12+
name: 'Major Update - stripe',
13+
updateType: 'major',
14+
title: 'chore(deps): update dependency stripe to 18.4.0',
15+
body: '',
16+
updates: [{
17+
name: 'stripe',
18+
currentVersion: '17.7.0',
19+
newVersion: '18.4.0',
20+
updateType: 'major',
21+
dependencyType: 'dependencies',
22+
file: 'package.json',
23+
metadata: undefined,
24+
}],
25+
}
26+
27+
const body = await generator.generateBody(majorStripeUpdate)
28+
29+
// Should NOT be like the broken version:
30+
// | Type | Count |
31+
// |------|-------|
32+
// | **Total** | **1** |
33+
34+
// Should BE like Renovate's format:
35+
// | Type | Count |
36+
// |------|-------|
37+
// | 📦 NPM Packages | 1 |
38+
// | **Total** | **1** |
39+
40+
expect(body).toContain('## 📦 Package Updates Summary')
41+
expect(body).toContain('| 📦 NPM Packages | 1 |')
42+
expect(body).toContain('| **Total** | **1** |')
43+
44+
// Should include the detailed package table (missing in original issue)
45+
expect(body).toContain('## 📦 npm Dependencies')
46+
expect(body).toContain('*1 package will be updated*')
47+
expect(body).toContain('| Package | Change | Age | Adoption | Passing | Confidence |')
48+
49+
// Should include package details
50+
expect(body).toContain('stripe')
51+
expect(body).toContain('17.7.0')
52+
expect(body).toContain('18.4.0')
53+
54+
// Should include badges and links like Renovate
55+
expect(body).toContain('renovatebot.com/diffs/npm/stripe')
56+
expect(body).toContain('developer.mend.io/api/mc/badges')
57+
58+
// Should have proper structure (not just empty Release Notes)
59+
expect(body).toContain('### Release Notes')
60+
expect(body).toContain('### Configuration')
61+
62+
// Verify it's not the broken minimal version
63+
expect(body).not.toMatch(/^This PR contains the following updates:\s*## 📦 Package Updates Summary\s*\| Type \| Count \|\s*\|------\|-------\|\s*\| \*\*Total\*\* \| \*\*1\*\* \|\s*---\s*### Release Notes\s*---/)
64+
})
65+
66+
it('should match Renovate-style format for major updates', async () => {
67+
const majorStripeUpdate: UpdateGroup = {
68+
name: 'Major Update - stripe',
69+
updateType: 'major',
70+
title: 'chore(deps): update dependency stripe to 18.4.0',
71+
body: '',
72+
updates: [{
73+
name: 'stripe',
74+
currentVersion: '17.7.0',
75+
newVersion: '18.4.0',
76+
updateType: 'major',
77+
dependencyType: 'dependencies',
78+
file: 'package.json',
79+
metadata: undefined,
80+
}],
81+
}
82+
83+
const body = await generator.generateBody(majorStripeUpdate)
84+
85+
// The format should be similar to Renovate's table:
86+
// | Package | Change | Age | Adoption | Passing | Confidence |
87+
// |---|---|---|---|---|---|
88+
// | [stripe](https://redirect.github.com/stripe/stripe-node) | [`^17.7.0` -> `^18.4.0`](https://renovatebot.com/diffs/npm/stripe/17.7.0/18.4.0) | badges... |
89+
90+
// Check for Renovate-style package table
91+
const tableRegex = /\| Package \| Change \| Age \| Adoption \| Passing \| Confidence \|[\s\S]*?\| \[stripe\][^|]*\| [^|]*17\.7\.0[^|]*18\.4\.0[^|]*\|/
92+
expect(body).toMatch(tableRegex)
93+
94+
// Should have package link
95+
expect(body).toMatch(/\[stripe\]\([^)]*github\.com[^)]*\)/)
96+
97+
// Should have diff link
98+
expect(body).toMatch(/\[.*17\.7\.0.*18\.4\.0.*\]\([^)]*renovatebot\.com[^)]*\)/)
99+
100+
// Should have badges
101+
expect(body).toMatch(/!\[age\]\([^)]*developer\.mend\.io[^)]*\)/)
102+
expect(body).toMatch(/!\[adoption\]\([^)]*developer\.mend\.io[^)]*\)/)
103+
expect(body).toMatch(/!\[passing\]\([^)]*developer\.mend\.io[^)]*\)/)
104+
expect(body).toMatch(/!\[confidence\]\([^)]*developer\.mend\.io[^)]*\)/)
105+
})
106+
107+
it('should work for different major update types', async () => {
108+
// Test composer major update
109+
const majorComposerUpdate: UpdateGroup = {
110+
name: 'Major Update - laravel/framework',
111+
updateType: 'major',
112+
title: 'chore(deps): update dependency laravel/framework to v11.0.0',
113+
body: '',
114+
updates: [{
115+
name: 'laravel/framework',
116+
currentVersion: '10.48.0',
117+
newVersion: '11.0.0',
118+
updateType: 'major',
119+
dependencyType: 'require',
120+
file: 'composer.json',
121+
metadata: undefined,
122+
}],
123+
}
124+
125+
const composerBody = await generator.generateBody(majorComposerUpdate)
126+
127+
expect(composerBody).toContain('| 🎼 Composer Packages | 1 |')
128+
expect(composerBody).toContain('## 🎼 PHP/Composer Dependencies')
129+
expect(composerBody).toContain('*1 package will be updated*')
130+
expect(composerBody).toContain('laravel/framework')
131+
132+
// Test GitHub Action major update
133+
const majorActionUpdate: UpdateGroup = {
134+
name: 'Major Update - actions/checkout',
135+
updateType: 'major',
136+
title: 'chore(deps): update dependency actions/checkout to v5',
137+
body: '',
138+
updates: [{
139+
name: 'actions/checkout',
140+
currentVersion: 'v4',
141+
newVersion: 'v5',
142+
updateType: 'major',
143+
dependencyType: 'github-actions',
144+
file: '.github/workflows/ci.yml',
145+
metadata: undefined,
146+
}],
147+
}
148+
149+
const actionBody = await generator.generateBody(majorActionUpdate)
150+
151+
expect(actionBody).toContain('| 🚀 GitHub Actions | 1 |')
152+
expect(actionBody).toContain('## 🚀 GitHub Actions')
153+
expect(actionBody).toContain('*1 action will be updated*')
154+
expect(actionBody).toContain('actions/checkout')
155+
})
156+
})
157+
})

0 commit comments

Comments
 (0)