Skip to content

Commit 10bd24d

Browse files
committed
feat: enhance pull request operations with validation and error handling
- Add branch existence validation - Add duplicate PR check - Add comprehensive error handling - Improve type safety with zod transforms - Add input sanitization
1 parent ff2f2c5 commit 10bd24d

File tree

1 file changed

+136
-30
lines changed

1 file changed

+136
-30
lines changed

src/github/operations/pulls.ts

Lines changed: 136 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import { z } from "zod";
2-
import { githubRequest } from "../common/utils.js";
2+
import {
3+
githubRequest,
4+
validateBranchName,
5+
validateOwnerName,
6+
validateRepositoryName,
7+
checkBranchExists,
8+
} from "../common/utils.js";
39
import {
410
GitHubIssueAssigneeSchema,
511
GitHubRepositorySchema
612
} from "../common/types.js";
13+
import {
14+
GitHubError,
15+
GitHubValidationError,
16+
GitHubResourceNotFoundError,
17+
GitHubConflictError,
18+
} from "../common/errors.js";
719

820
const GITHUB_TITLE_MAX_LENGTH = 256;
921
const GITHUB_BODY_MAX_LENGTH = 65536;
1022

1123
export const RepositoryParamsSchema = z.object({
12-
owner: z.string().min(1),
13-
repo: z.string().min(1),
24+
owner: z.string().min(1).transform(validateOwnerName),
25+
repo: z.string().min(1).transform(validateRepositoryName),
1426
});
1527

1628
export const GitHubPullRequestStateSchema = z.enum([
@@ -34,7 +46,7 @@ export const GitHubDirectionSchema = z.enum([
3446

3547
export const GitHubPullRequestRefSchema = z.object({
3648
label: z.string(),
37-
ref: z.string().min(1),
49+
ref: z.string().min(1).transform(validateBranchName),
3850
sha: z.string().length(40),
3951
user: GitHubIssueAssigneeSchema,
4052
repo: GitHubRepositorySchema,
@@ -73,8 +85,8 @@ export const GitHubPullRequestSchema = z.object({
7385

7486
export const ListPullRequestsOptionsSchema = z.object({
7587
state: GitHubPullRequestStateSchema.optional(),
76-
head: z.string().optional(),
77-
base: z.string().optional(),
88+
head: z.string().transform(validateBranchName).optional(),
89+
base: z.string().transform(validateBranchName).optional(),
7890
sort: GitHubPullRequestSortSchema.optional(),
7991
direction: GitHubDirectionSchema.optional(),
8092
per_page: z.number().min(1).max(100).optional(),
@@ -84,8 +96,8 @@ export const ListPullRequestsOptionsSchema = z.object({
8496
export const CreatePullRequestOptionsSchema = z.object({
8597
title: z.string().max(GITHUB_TITLE_MAX_LENGTH),
8698
body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional(),
87-
head: z.string().min(1),
88-
base: z.string().min(1),
99+
head: z.string().min(1).transform(validateBranchName),
100+
base: z.string().min(1).transform(validateBranchName),
89101
maintainer_can_modify: z.boolean().optional(),
90102
draft: z.boolean().optional(),
91103
});
@@ -100,20 +112,86 @@ export type ListPullRequestsOptions = z.infer<typeof ListPullRequestsOptionsSche
100112
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
101113
export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;
102114

115+
async function validatePullRequestBranches(
116+
owner: string,
117+
repo: string,
118+
head: string,
119+
base: string
120+
): Promise<void> {
121+
const [headExists, baseExists] = await Promise.all([
122+
checkBranchExists(owner, repo, head),
123+
checkBranchExists(owner, repo, base),
124+
]);
125+
126+
if (!headExists) {
127+
throw new GitHubResourceNotFoundError(`Branch '${head}' not found`);
128+
}
129+
130+
if (!baseExists) {
131+
throw new GitHubResourceNotFoundError(`Branch '${base}' not found`);
132+
}
133+
134+
if (head === base) {
135+
throw new GitHubValidationError(
136+
"Head and base branches cannot be the same",
137+
422,
138+
{ message: "Head and base branches must be different" }
139+
);
140+
}
141+
}
142+
143+
async function checkForExistingPullRequest(
144+
owner: string,
145+
repo: string,
146+
head: string,
147+
base: string
148+
): Promise<void> {
149+
const existingPRs = await listPullRequests({
150+
owner,
151+
repo,
152+
head,
153+
base,
154+
state: "open",
155+
});
156+
157+
if (existingPRs.length > 0) {
158+
throw new GitHubConflictError(
159+
`A pull request already exists for ${head} into ${base}`
160+
);
161+
}
162+
}
163+
103164
export async function createPullRequest(
104165
params: z.infer<typeof CreatePullRequestSchema>
105166
): Promise<GitHubPullRequest> {
106167
const { owner, repo, ...options } = CreatePullRequestSchema.parse(params);
107-
108-
const response = await githubRequest(
109-
`https://api.github.com/repos/${owner}/${repo}/pulls`,
110-
{
111-
method: "POST",
112-
body: options,
113-
}
114-
);
115168

116-
return GitHubPullRequestSchema.parse(response);
169+
try {
170+
await validatePullRequestBranches(owner, repo, options.head, options.base);
171+
await checkForExistingPullRequest(owner, repo, options.head, options.base);
172+
173+
const response = await githubRequest(
174+
`https://api.github.com/repos/${owner}/${repo}/pulls`,
175+
{
176+
method: "POST",
177+
body: options,
178+
}
179+
);
180+
181+
return GitHubPullRequestSchema.parse(response);
182+
} catch (error) {
183+
if (error instanceof GitHubError) {
184+
throw error;
185+
}
186+
if (error instanceof z.ZodError) {
187+
throw new GitHubValidationError(
188+
"Invalid pull request data",
189+
422,
190+
{ errors: error.errors }
191+
);
192+
}
193+
throw error;
194+
}
117195
}
118196

119197
export async function getPullRequest(
@@ -124,11 +202,25 @@ export async function getPullRequest(
124202
pullNumber: z.number().positive(),
125203
}).parse(params);
126204

127-
const response = await githubRequest(
128-
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
129-
);
205+
try {
206+
const response = await githubRequest(
207+
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
208+
);
130209

131-
return GitHubPullRequestSchema.parse(response);
210+
return GitHubPullRequestSchema.parse(response);
211+
} catch (error) {
212+
if (error instanceof GitHubError) {
213+
throw error;
214+
}
215+
if (error instanceof z.ZodError) {
216+
throw new GitHubValidationError(
217+
"Invalid pull request response data",
218+
422,
219+
{ errors: error.errors }
220+
);
221+
}
222+
throw error;
223+
}
132224
}
133225

134226
export async function listPullRequests(
@@ -139,14 +231,28 @@ export async function listPullRequests(
139231
...ListPullRequestsOptionsSchema.partial().shape,
140232
}).parse(params);
141233

142-
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
143-
144-
Object.entries(options).forEach(([key, value]) => {
145-
if (value !== undefined) {
146-
url.searchParams.append(key, value.toString());
147-
}
148-
});
234+
try {
235+
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
236+
237+
Object.entries(options).forEach(([key, value]) => {
238+
if (value !== undefined) {
239+
url.searchParams.append(key, value.toString());
240+
}
241+
});
149242

150-
const response = await githubRequest(url.toString());
151-
return z.array(GitHubPullRequestSchema).parse(response);
243+
const response = await githubRequest(url.toString());
244+
return z.array(GitHubPullRequestSchema).parse(response);
245+
} catch (error) {
246+
if (error instanceof GitHubError) {
247+
throw error;
248+
}
249+
if (error instanceof z.ZodError) {
250+
throw new GitHubValidationError(
251+
"Invalid pull request list response data",
252+
422,
253+
{ errors: error.errors }
254+
);
255+
}
256+
throw error;
257+
}
152258
}

0 commit comments

Comments
 (0)