1
1
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" ;
3
9
import {
4
10
GitHubIssueAssigneeSchema ,
5
11
GitHubRepositorySchema
6
12
} from "../common/types.js" ;
13
+ import {
14
+ GitHubError ,
15
+ GitHubValidationError ,
16
+ GitHubResourceNotFoundError ,
17
+ GitHubConflictError ,
18
+ } from "../common/errors.js" ;
7
19
8
20
const GITHUB_TITLE_MAX_LENGTH = 256 ;
9
21
const GITHUB_BODY_MAX_LENGTH = 65536 ;
10
22
11
23
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 ) ,
14
26
} ) ;
15
27
16
28
export const GitHubPullRequestStateSchema = z . enum ( [
@@ -34,7 +46,7 @@ export const GitHubDirectionSchema = z.enum([
34
46
35
47
export const GitHubPullRequestRefSchema = z . object ( {
36
48
label : z . string ( ) ,
37
- ref : z . string ( ) . min ( 1 ) ,
49
+ ref : z . string ( ) . min ( 1 ) . transform ( validateBranchName ) ,
38
50
sha : z . string ( ) . length ( 40 ) ,
39
51
user : GitHubIssueAssigneeSchema ,
40
52
repo : GitHubRepositorySchema ,
@@ -73,8 +85,8 @@ export const GitHubPullRequestSchema = z.object({
73
85
74
86
export const ListPullRequestsOptionsSchema = z . object ( {
75
87
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 ( ) ,
78
90
sort : GitHubPullRequestSortSchema . optional ( ) ,
79
91
direction : GitHubDirectionSchema . optional ( ) ,
80
92
per_page : z . number ( ) . min ( 1 ) . max ( 100 ) . optional ( ) ,
@@ -84,8 +96,8 @@ export const ListPullRequestsOptionsSchema = z.object({
84
96
export const CreatePullRequestOptionsSchema = z . object ( {
85
97
title : z . string ( ) . max ( GITHUB_TITLE_MAX_LENGTH ) ,
86
98
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 ) ,
89
101
maintainer_can_modify : z . boolean ( ) . optional ( ) ,
90
102
draft : z . boolean ( ) . optional ( ) ,
91
103
} ) ;
@@ -100,20 +112,86 @@ export type ListPullRequestsOptions = z.infer<typeof ListPullRequestsOptionsSche
100
112
export type GitHubPullRequest = z . infer < typeof GitHubPullRequestSchema > ;
101
113
export type GitHubPullRequestRef = z . infer < typeof GitHubPullRequestRefSchema > ;
102
114
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
+
103
164
export async function createPullRequest (
104
165
params : z . infer < typeof CreatePullRequestSchema >
105
166
) : Promise < GitHubPullRequest > {
106
167
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
- ) ;
115
168
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
+ }
117
195
}
118
196
119
197
export async function getPullRequest (
@@ -124,11 +202,25 @@ export async function getPullRequest(
124
202
pullNumber : z . number ( ) . positive ( ) ,
125
203
} ) . parse ( params ) ;
126
204
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
+ ) ;
130
209
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
+ }
132
224
}
133
225
134
226
export async function listPullRequests (
@@ -139,14 +231,28 @@ export async function listPullRequests(
139
231
...ListPullRequestsOptionsSchema . partial ( ) . shape ,
140
232
} ) . parse ( params ) ;
141
233
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
+ } ) ;
149
242
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
+ }
152
258
}
0 commit comments