Merge pull request #503 from modelcontextprotocol/justin/github-prs
[GitHub] Add PR listing, reading, reviewing, and merging tools
This commit is contained in:
commit
5e1350d095
3 changed files with 385 additions and 217 deletions
|
|
@ -188,6 +188,95 @@ MCP Server for the GitHub API, enabling file operations, repository management,
|
|||
- `issue_number` (number): Issue number to retrieve
|
||||
- Returns: Github Issue object & details
|
||||
|
||||
18. `get_pull_request`
|
||||
- Get details of a specific pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Pull request details including diff and review status
|
||||
|
||||
19. `list_pull_requests`
|
||||
- List and filter repository pull requests
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `state` (optional string): Filter by state ('open', 'closed', 'all')
|
||||
- `head` (optional string): Filter by head user/org and branch
|
||||
- `base` (optional string): Filter by base branch
|
||||
- `sort` (optional string): Sort by ('created', 'updated', 'popularity', 'long-running')
|
||||
- `direction` (optional string): Sort direction ('asc', 'desc')
|
||||
- `per_page` (optional number): Results per page (max 100)
|
||||
- `page` (optional number): Page number
|
||||
- Returns: Array of pull request details
|
||||
|
||||
20. `create_pull_request_review`
|
||||
- Create a review on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `body` (string): Review comment text
|
||||
- `event` (string): Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')
|
||||
- `commit_id` (optional string): SHA of commit to review
|
||||
- `comments` (optional array): Line-specific comments, each with:
|
||||
- `path` (string): File path
|
||||
- `position` (number): Line position in diff
|
||||
- `body` (string): Comment text
|
||||
- Returns: Created review details
|
||||
|
||||
21. `merge_pull_request`
|
||||
- Merge a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `commit_title` (optional string): Title for merge commit
|
||||
- `commit_message` (optional string): Extra detail for merge commit
|
||||
- `merge_method` (optional string): Merge method ('merge', 'squash', 'rebase')
|
||||
- Returns: Merge result details
|
||||
|
||||
22. `get_pull_request_files`
|
||||
- Get the list of files changed in a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of changed files with patch and status details
|
||||
|
||||
23. `get_pull_request_status`
|
||||
- Get the combined status of all status checks for a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Combined status check results and individual check details
|
||||
|
||||
24. `update_pull_request_branch`
|
||||
- Update a pull request branch with the latest changes from the base branch (equivalent to GitHub's "Update branch" button)
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref
|
||||
- Returns: Success message when branch is updated
|
||||
|
||||
25. `get_pull_request_comments`
|
||||
- Get the review comments on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of pull request review comments with details like the comment text, author, and location in the diff
|
||||
|
||||
26. `get_pull_request_reviews`
|
||||
- Get the reviews on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of pull request reviews with details like the review state (APPROVED, CHANGES_REQUESTED, etc.), reviewer, and review body
|
||||
|
||||
## Search Query Syntax
|
||||
|
||||
### Code Search
|
||||
|
|
|
|||
|
|
@ -204,6 +204,42 @@ export const GitHubSearchResponseSchema = z.object({
|
|||
items: z.array(GitHubRepositorySchema),
|
||||
});
|
||||
|
||||
// Pull request schemas
|
||||
export const GitHubPullRequestRefSchema = z.object({
|
||||
label: z.string(),
|
||||
ref: z.string(),
|
||||
sha: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
repo: GitHubRepositorySchema,
|
||||
});
|
||||
|
||||
export const GitHubPullRequestSchema = z.object({
|
||||
url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
html_url: z.string(),
|
||||
diff_url: z.string(),
|
||||
patch_url: z.string(),
|
||||
issue_url: z.string(),
|
||||
number: z.number(),
|
||||
state: z.string(),
|
||||
locked: z.boolean(),
|
||||
title: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
closed_at: z.string().nullable(),
|
||||
merged_at: z.string().nullable(),
|
||||
merge_commit_sha: z.string().nullable(),
|
||||
assignee: GitHubIssueAssigneeSchema.nullable(),
|
||||
assignees: z.array(GitHubIssueAssigneeSchema),
|
||||
requested_reviewers: z.array(GitHubIssueAssigneeSchema),
|
||||
labels: z.array(GitHubLabelSchema),
|
||||
head: GitHubPullRequestRefSchema,
|
||||
base: GitHubPullRequestRefSchema,
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
|
||||
export type GitHubRepository = z.infer<typeof GitHubRepositorySchema>;
|
||||
|
|
@ -218,4 +254,6 @@ export type GitHubIssueAssignee = z.infer<typeof GitHubIssueAssigneeSchema>;
|
|||
export type GitHubLabel = z.infer<typeof GitHubLabelSchema>;
|
||||
export type GitHubMilestone = z.infer<typeof GitHubMilestoneSchema>;
|
||||
export type GitHubIssue = z.infer<typeof GitHubIssueSchema>;
|
||||
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
|
||||
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
|
||||
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
|
||||
export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;
|
||||
|
|
@ -1,261 +1,302 @@
|
|||
import { z } from "zod";
|
||||
import { githubRequest } from "../common/utils.js";
|
||||
import {
|
||||
githubRequest,
|
||||
validateBranchName,
|
||||
validateOwnerName,
|
||||
validateRepositoryName,
|
||||
checkBranchExists,
|
||||
} from "../common/utils.js";
|
||||
import {
|
||||
GitHubIssueAssigneeSchema,
|
||||
GitHubRepositorySchema
|
||||
GitHubPullRequestSchema,
|
||||
GitHubIssueAssigneeSchema,
|
||||
GitHubRepositorySchema,
|
||||
} from "../common/types.js";
|
||||
import {
|
||||
GitHubError,
|
||||
GitHubValidationError,
|
||||
GitHubResourceNotFoundError,
|
||||
GitHubConflictError,
|
||||
} from "../common/errors.js";
|
||||
|
||||
const GITHUB_TITLE_MAX_LENGTH = 256;
|
||||
const GITHUB_BODY_MAX_LENGTH = 65536;
|
||||
|
||||
export const RepositoryParamsSchema = z.object({
|
||||
owner: z.string().min(1).transform(validateOwnerName),
|
||||
repo: z.string().min(1).transform(validateRepositoryName),
|
||||
// Schema definitions
|
||||
export const PullRequestFileSchema = z.object({
|
||||
sha: z.string(),
|
||||
filename: z.string(),
|
||||
status: z.enum(['added', 'removed', 'modified', 'renamed', 'copied', 'changed', 'unchanged']),
|
||||
additions: z.number(),
|
||||
deletions: z.number(),
|
||||
changes: z.number(),
|
||||
blob_url: z.string(),
|
||||
raw_url: z.string(),
|
||||
contents_url: z.string(),
|
||||
patch: z.string().optional()
|
||||
});
|
||||
|
||||
export const GitHubPullRequestStateSchema = z.enum([
|
||||
"open",
|
||||
"closed",
|
||||
"merged",
|
||||
"draft"
|
||||
]);
|
||||
|
||||
export const GitHubPullRequestSortSchema = z.enum([
|
||||
"created",
|
||||
"updated",
|
||||
"popularity",
|
||||
"long-running"
|
||||
]);
|
||||
|
||||
export const GitHubDirectionSchema = z.enum([
|
||||
"asc",
|
||||
"desc"
|
||||
]);
|
||||
|
||||
export const GitHubPullRequestRefSchema = z.object({
|
||||
label: z.string(),
|
||||
ref: z.string().min(1).transform(validateBranchName),
|
||||
sha: z.string().length(40),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
repo: GitHubRepositorySchema,
|
||||
export const StatusCheckSchema = z.object({
|
||||
url: z.string(),
|
||||
state: z.enum(['error', 'failure', 'pending', 'success']),
|
||||
description: z.string().nullable(),
|
||||
target_url: z.string().nullable(),
|
||||
context: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
|
||||
export const GitHubPullRequestSchema = z.object({
|
||||
url: z.string().url(),
|
||||
id: z.number().positive(),
|
||||
export const CombinedStatusSchema = z.object({
|
||||
state: z.enum(['error', 'failure', 'pending', 'success']),
|
||||
statuses: z.array(StatusCheckSchema),
|
||||
sha: z.string(),
|
||||
total_count: z.number()
|
||||
});
|
||||
|
||||
export const PullRequestCommentSchema = z.object({
|
||||
url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
html_url: z.string().url(),
|
||||
diff_url: z.string().url(),
|
||||
patch_url: z.string().url(),
|
||||
issue_url: z.string().url(),
|
||||
number: z.number().positive(),
|
||||
state: GitHubPullRequestStateSchema,
|
||||
locked: z.boolean(),
|
||||
title: z.string().max(GITHUB_TITLE_MAX_LENGTH),
|
||||
pull_request_review_id: z.number().nullable(),
|
||||
diff_hunk: z.string(),
|
||||
path: z.string().nullable(),
|
||||
position: z.number().nullable(),
|
||||
original_position: z.number().nullable(),
|
||||
commit_id: z.string(),
|
||||
original_commit_id: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string().max(GITHUB_BODY_MAX_LENGTH).nullable(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
closed_at: z.string().datetime().nullable(),
|
||||
merged_at: z.string().datetime().nullable(),
|
||||
merge_commit_sha: z.string().length(40).nullable(),
|
||||
assignee: GitHubIssueAssigneeSchema.nullable(),
|
||||
assignees: z.array(GitHubIssueAssigneeSchema),
|
||||
requested_reviewers: z.array(GitHubIssueAssigneeSchema),
|
||||
labels: z.array(z.object({
|
||||
name: z.string(),
|
||||
color: z.string().regex(/^[0-9a-fA-F]{6}$/),
|
||||
description: z.string().nullable(),
|
||||
})),
|
||||
head: GitHubPullRequestRefSchema,
|
||||
base: GitHubPullRequestRefSchema,
|
||||
body: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
html_url: z.string(),
|
||||
pull_request_url: z.string(),
|
||||
author_association: z.string(),
|
||||
_links: z.object({
|
||||
self: z.object({ href: z.string() }),
|
||||
html: z.object({ href: z.string() }),
|
||||
pull_request: z.object({ href: z.string() })
|
||||
})
|
||||
});
|
||||
|
||||
export const ListPullRequestsOptionsSchema = z.object({
|
||||
state: GitHubPullRequestStateSchema.optional(),
|
||||
head: z.string().transform(validateBranchName).optional(),
|
||||
base: z.string().transform(validateBranchName).optional(),
|
||||
sort: GitHubPullRequestSortSchema.optional(),
|
||||
direction: GitHubDirectionSchema.optional(),
|
||||
per_page: z.number().min(1).max(100).optional(),
|
||||
page: z.number().min(1).optional(),
|
||||
export const PullRequestReviewSchema = z.object({
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string().nullable(),
|
||||
state: z.enum(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING']),
|
||||
html_url: z.string(),
|
||||
pull_request_url: z.string(),
|
||||
commit_id: z.string(),
|
||||
submitted_at: z.string().nullable(),
|
||||
author_association: z.string()
|
||||
});
|
||||
|
||||
export const CreatePullRequestOptionsSchema = z.object({
|
||||
title: z.string().max(GITHUB_TITLE_MAX_LENGTH),
|
||||
body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional(),
|
||||
head: z.string().min(1).transform(validateBranchName),
|
||||
base: z.string().min(1).transform(validateBranchName),
|
||||
maintainer_can_modify: z.boolean().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
// Input schemas
|
||||
export const CreatePullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
title: z.string().describe("Pull request title"),
|
||||
body: z.string().optional().describe("Pull request body/description"),
|
||||
head: z.string().describe("The name of the branch where your changes are implemented"),
|
||||
base: z.string().describe("The name of the branch you want the changes pulled into"),
|
||||
draft: z.boolean().optional().describe("Whether to create the pull request as a draft"),
|
||||
maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request")
|
||||
});
|
||||
|
||||
export const CreatePullRequestSchema = RepositoryParamsSchema.extend({
|
||||
...CreatePullRequestOptionsSchema.shape,
|
||||
export const GetPullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
export type RepositoryParams = z.infer<typeof RepositoryParamsSchema>;
|
||||
export type CreatePullRequestOptions = z.infer<typeof CreatePullRequestOptionsSchema>;
|
||||
export type ListPullRequestsOptions = z.infer<typeof ListPullRequestsOptionsSchema>;
|
||||
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
|
||||
export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;
|
||||
export const ListPullRequestsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
state: z.enum(['open', 'closed', 'all']).optional().describe("State of the pull requests to return"),
|
||||
head: z.string().optional().describe("Filter by head user or head organization and branch name"),
|
||||
base: z.string().optional().describe("Filter by base branch name"),
|
||||
sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe("What to sort results by"),
|
||||
direction: z.enum(['asc', 'desc']).optional().describe("The direction of the sort"),
|
||||
per_page: z.number().optional().describe("Results per page (max 100)"),
|
||||
page: z.number().optional().describe("Page number of the results")
|
||||
});
|
||||
|
||||
async function validatePullRequestBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
head: string,
|
||||
base: string
|
||||
): Promise<void> {
|
||||
const [headExists, baseExists] = await Promise.all([
|
||||
checkBranchExists(owner, repo, head),
|
||||
checkBranchExists(owner, repo, base),
|
||||
]);
|
||||
export const CreatePullRequestReviewSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
commit_id: z.string().optional().describe("The SHA of the commit that needs a review"),
|
||||
body: z.string().describe("The body text of the review"),
|
||||
event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"),
|
||||
comments: z.array(z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
position: z.number().describe("The position in the diff where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
})).optional().describe("Comments to post as part of the review")
|
||||
});
|
||||
|
||||
if (!headExists) {
|
||||
throw new GitHubResourceNotFoundError(`Branch '${head}' not found`);
|
||||
}
|
||||
export const MergePullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
commit_title: z.string().optional().describe("Title for the automatic commit message"),
|
||||
commit_message: z.string().optional().describe("Extra detail to append to automatic commit message"),
|
||||
merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method to use")
|
||||
});
|
||||
|
||||
if (!baseExists) {
|
||||
throw new GitHubResourceNotFoundError(`Branch '${base}' not found`);
|
||||
}
|
||||
export const GetPullRequestFilesSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
if (head === base) {
|
||||
throw new GitHubValidationError(
|
||||
"Head and base branches cannot be the same",
|
||||
422,
|
||||
{ message: "Head and base branches must be different" }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GetPullRequestStatusSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
async function checkForExistingPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
head: string,
|
||||
base: string
|
||||
): Promise<void> {
|
||||
const existingPRs = await listPullRequests({
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
});
|
||||
export const UpdatePullRequestBranchSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref")
|
||||
});
|
||||
|
||||
// Check if any existing open PR has the exact same head and base combination
|
||||
const duplicatePR = existingPRs.find(pr =>
|
||||
pr.head.ref === head && pr.base.ref === base
|
||||
);
|
||||
export const GetPullRequestCommentsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
if (duplicatePR) {
|
||||
throw new GitHubConflictError(
|
||||
`A pull request already exists for ${head} into ${base}`
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GetPullRequestReviewsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
// Function implementations
|
||||
export async function createPullRequest(
|
||||
params: z.infer<typeof CreatePullRequestSchema>
|
||||
): Promise<GitHubPullRequest> {
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>> {
|
||||
const { owner, repo, ...options } = CreatePullRequestSchema.parse(params);
|
||||
|
||||
try {
|
||||
await validatePullRequestBranches(owner, repo, options.head, options.base);
|
||||
await checkForExistingPullRequest(owner, repo, options.head, options.base);
|
||||
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls`,
|
||||
{
|
||||
method: "POST",
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
} catch (error) {
|
||||
if (error instanceof GitHubError) {
|
||||
throw error;
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls`,
|
||||
{
|
||||
method: "POST",
|
||||
body: options,
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new GitHubValidationError(
|
||||
"Invalid pull request data",
|
||||
422,
|
||||
{ errors: error.errors }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequest(
|
||||
params: RepositoryParams & { pullNumber: number }
|
||||
): Promise<GitHubPullRequest> {
|
||||
const { owner, repo, pullNumber } = z.object({
|
||||
...RepositoryParamsSchema.shape,
|
||||
pullNumber: z.number().positive(),
|
||||
}).parse(params);
|
||||
|
||||
try {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
|
||||
);
|
||||
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
} catch (error) {
|
||||
if (error instanceof GitHubError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new GitHubValidationError(
|
||||
"Invalid pull request response data",
|
||||
422,
|
||||
{ errors: error.errors }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
|
||||
);
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function listPullRequests(
|
||||
params: RepositoryParams & Partial<ListPullRequestsOptions>
|
||||
): Promise<GitHubPullRequest[]> {
|
||||
const { owner, repo, ...options } = z.object({
|
||||
...RepositoryParamsSchema.shape,
|
||||
...ListPullRequestsOptionsSchema.partial().shape,
|
||||
}).parse(params);
|
||||
owner: string,
|
||||
repo: string,
|
||||
options: Omit<z.infer<typeof ListPullRequestsSchema>, 'owner' | 'repo'>
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>[]> {
|
||||
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
|
||||
|
||||
if (options.state) url.searchParams.append('state', options.state);
|
||||
if (options.head) url.searchParams.append('head', options.head);
|
||||
if (options.base) url.searchParams.append('base', options.base);
|
||||
if (options.sort) url.searchParams.append('sort', options.sort);
|
||||
if (options.direction) url.searchParams.append('direction', options.direction);
|
||||
if (options.per_page) url.searchParams.append('per_page', options.per_page.toString());
|
||||
if (options.page) url.searchParams.append('page', options.page.toString());
|
||||
|
||||
try {
|
||||
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
|
||||
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
const response = await githubRequest(url.toString());
|
||||
return z.array(GitHubPullRequestSchema).parse(response);
|
||||
}
|
||||
|
||||
const response = await githubRequest(url.toString());
|
||||
return z.array(GitHubPullRequestSchema).parse(response);
|
||||
} catch (error) {
|
||||
if (error instanceof GitHubError) {
|
||||
throw error;
|
||||
export async function createPullRequestReview(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
options: Omit<z.infer<typeof CreatePullRequestReviewSchema>, 'owner' | 'repo' | 'pull_number'>
|
||||
): Promise<z.infer<typeof PullRequestReviewSchema>> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: options,
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new GitHubValidationError(
|
||||
"Invalid pull request list response data",
|
||||
422,
|
||||
{ errors: error.errors }
|
||||
);
|
||||
);
|
||||
return PullRequestReviewSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function mergePullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
options: Omit<z.infer<typeof MergePullRequestSchema>, 'owner' | 'repo' | 'pull_number'>
|
||||
): Promise<any> {
|
||||
return githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/merge`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: options,
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPullRequestFiles(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestFileSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/files`
|
||||
);
|
||||
return z.array(PullRequestFileSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function updatePullRequestBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
expectedHeadSha?: string
|
||||
): Promise<void> {
|
||||
await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: expectedHeadSha ? { expected_head_sha: expectedHeadSha } : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPullRequestComments(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestCommentSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/comments`
|
||||
);
|
||||
return z.array(PullRequestCommentSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequestReviews(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestReviewSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`
|
||||
);
|
||||
return z.array(PullRequestReviewSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequestStatus(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof CombinedStatusSchema>> {
|
||||
// First get the PR to get the head SHA
|
||||
const pr = await getPullRequest(owner, repo, pullNumber);
|
||||
const sha = pr.head.sha;
|
||||
|
||||
// Then get the combined status for that SHA
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/commits/${sha}/status`
|
||||
);
|
||||
return CombinedStatusSchema.parse(response);
|
||||
}
|
||||
Loading…
Reference in a new issue