Skip to content

Commit d2bb56d

Browse files
feat(github): add pull request creation service
- Create GitHubPRService for PR creation - Add POST /api/github/pr endpoint - Auto-add AI Development Cockpit attribution - Commit multiple files to new branch - TDD with 1 passing test Part of Phase 2 - GitHub Integration COMPLETES GITHUB INTEGRATION!
1 parent 4056cb2 commit d2bb56d

3 files changed

Lines changed: 191 additions & 0 deletions

File tree

src/app/api/github/pr/route.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createClient } from '@/lib/supabase/server'
2+
import { GitHubPRService } from '@/services/github/pr.service'
3+
import { NextRequest, NextResponse } from 'next/server'
4+
import { cookies } from 'next/headers'
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const cookieStore = cookies()
9+
const supabase = createClient(cookieStore)
10+
11+
// Get current user session
12+
const { data: { session }, error: sessionError } = await supabase.auth.getSession()
13+
14+
if (sessionError || !session) {
15+
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
16+
}
17+
18+
const githubToken = session.provider_token
19+
if (!githubToken) {
20+
return NextResponse.json({ error: 'No GitHub token found' }, { status: 401 })
21+
}
22+
23+
const body = await request.json()
24+
const { owner, repo, branchName, baseBranch, title, prBody, files } = body
25+
26+
// Validation
27+
if (!owner || !repo || !branchName || !baseBranch || !title || !files) {
28+
return NextResponse.json(
29+
{ error: 'Missing required fields' },
30+
{ status: 400 }
31+
)
32+
}
33+
34+
const prService = new GitHubPRService(githubToken)
35+
const result = await prService.createPullRequest({
36+
owner,
37+
repo,
38+
branchName,
39+
baseBranch,
40+
title,
41+
body: prBody,
42+
files
43+
})
44+
45+
return NextResponse.json({
46+
success: true,
47+
...result
48+
})
49+
} catch (error) {
50+
console.error('PR creation error:', error)
51+
return NextResponse.json(
52+
{
53+
success: false,
54+
error: error instanceof Error ? error.message : 'PR creation failed'
55+
},
56+
{ status: 500 }
57+
)
58+
}
59+
}

src/services/github/pr.service.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Octokit } from '@octokit/rest'
2+
3+
export interface PRFile {
4+
path: string
5+
content: string
6+
}
7+
8+
export interface CreatePROptions {
9+
owner: string
10+
repo: string
11+
branchName: string
12+
baseBranch: string
13+
title: string
14+
body: string
15+
files: PRFile[]
16+
}
17+
18+
export interface PRResult {
19+
url: string
20+
number: number
21+
}
22+
23+
export class GitHubPRService {
24+
private octokit: Octokit
25+
26+
constructor(accessToken: string) {
27+
this.octokit = new Octokit({ auth: accessToken })
28+
}
29+
30+
async createPullRequest(options: CreatePROptions): Promise<PRResult> {
31+
const { owner, repo, branchName, baseBranch, title, body, files } = options
32+
33+
console.log(`🔄 Creating PR: ${owner}/${repo} (${branchName}${baseBranch})`)
34+
35+
// Step 1: Get base branch SHA
36+
const { data: baseBranchData } = await this.octokit.repos.getBranch({
37+
owner,
38+
repo,
39+
branch: baseBranch
40+
})
41+
42+
const baseSha = baseBranchData.commit.sha
43+
44+
// Step 2: Create new branch
45+
await this.octokit.git.createRef({
46+
owner,
47+
repo,
48+
ref: `refs/heads/${branchName}`,
49+
sha: baseSha
50+
})
51+
52+
console.log(`✅ Created branch: ${branchName}`)
53+
54+
// Step 3: Commit files to new branch
55+
for (const file of files) {
56+
await this.octokit.repos.createOrUpdateFileContents({
57+
owner,
58+
repo,
59+
path: file.path,
60+
message: `Add ${file.path}`,
61+
content: Buffer.from(file.content).toString('base64'),
62+
branch: branchName
63+
})
64+
}
65+
66+
console.log(`✅ Committed ${files.length} files`)
67+
68+
// Step 4: Create pull request
69+
const { data: pr } = await this.octokit.pulls.create({
70+
owner,
71+
repo,
72+
title,
73+
body: `${body}\n\n---\n🤖 Generated with [AI Development Cockpit](https://github.com/ScientiaCapital/ai-development-cockpit)`,
74+
head: branchName,
75+
base: baseBranch
76+
})
77+
78+
console.log(`✅ Created PR #${pr.number}: ${pr.html_url}`)
79+
80+
return {
81+
url: pr.html_url,
82+
number: pr.number
83+
}
84+
}
85+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { GitHubPRService } from '@/services/github/pr.service'
2+
3+
// Mock Octokit
4+
jest.mock('@octokit/rest', () => ({
5+
Octokit: jest.fn().mockImplementation(() => ({
6+
repos: {
7+
createOrUpdateFileContents: jest.fn().mockResolvedValue({ data: {} }),
8+
getBranch: jest.fn().mockResolvedValue({ data: { commit: { sha: 'abc123' } } })
9+
},
10+
git: {
11+
createRef: jest.fn().mockResolvedValue({ data: {} })
12+
},
13+
pulls: {
14+
create: jest.fn().mockResolvedValue({
15+
data: {
16+
html_url: 'https://github.com/owner/repo/pull/1',
17+
number: 1
18+
}
19+
})
20+
}
21+
}))
22+
}))
23+
24+
describe('GitHubPRService', () => {
25+
let service: GitHubPRService
26+
27+
beforeEach(() => {
28+
service = new GitHubPRService('fake-token')
29+
})
30+
31+
it('should create a pull request', async () => {
32+
const result = await service.createPullRequest({
33+
owner: 'testowner',
34+
repo: 'testrepo',
35+
branchName: 'feature/ai-generated',
36+
baseBranch: 'main',
37+
title: 'AI Generated Feature',
38+
body: 'This PR contains AI-generated code',
39+
files: [
40+
{ path: 'src/test.ts', content: 'console.log("test")' }
41+
]
42+
})
43+
44+
expect(result.url).toBe('https://github.com/owner/repo/pull/1')
45+
expect(result.number).toBe(1)
46+
})
47+
})

0 commit comments

Comments
 (0)