feat: enhance GitHub request utilities with error handling

This commit is contained in:
Peter M. Elias
2024-12-28 03:25:31 -08:00
parent fb421b4837
commit ff2f2c5347

View File

@@ -1,46 +1,123 @@
import fetch from "node-fetch"; import { createGitHubError } from "./errors.js";
if (!process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { type RequestOptions = {
console.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set");
process.exit(1);
}
export const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
interface GitHubRequestOptions {
method?: string; method?: string;
body?: any; body?: unknown;
headers?: Record<string, string>;
};
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
return response.text();
} }
export async function githubRequest(url: string, options: GitHubRequestOptions = {}) { export async function githubRequest(
const response = await fetch(url, { url: string,
method: options.method || "GET", options: RequestOptions = {}
headers: { ): Promise<unknown> {
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, const headers = {
Accept: "application/vnd.github.v3+json", "Accept": "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server", "Content-Type": "application/json",
...(options.body ? { "Content-Type": "application/json" } : {}), ...options.headers,
}, };
...(options.body ? { body: JSON.stringify(options.body) } : {}),
});
if (!response.ok) { if (process.env.GITHUB_TOKEN) {
throw new Error(`GitHub API error: ${response.statusText}`); headers["Authorization"] = `Bearer ${process.env.GITHUB_TOKEN}`;
} }
return response.json(); const response = await fetch(url, {
method: options.method || "GET",
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
const responseBody = await parseResponseBody(response);
if (!response.ok) {
throw createGitHubError(response.status, responseBody);
}
return responseBody;
} }
export function buildUrl(baseUrl: string, params: Record<string, any> = {}) { export function validateBranchName(branch: string): string {
const url = new URL(baseUrl); const sanitized = branch.trim();
Object.entries(params).forEach(([key, value]) => { if (!sanitized) {
if (value !== undefined && value !== null) { throw new Error("Branch name cannot be empty");
if (Array.isArray(value)) { }
url.searchParams.append(key, value.join(",")); if (sanitized.includes("..")) {
} else { throw new Error("Branch name cannot contain '..'");
url.searchParams.append(key, value.toString()); }
} if (/[\s~^:?*[\\\]]/.test(sanitized)) {
throw new Error("Branch name contains invalid characters");
}
if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
throw new Error("Branch name cannot start or end with '/'");
}
if (sanitized.endsWith(".lock")) {
throw new Error("Branch name cannot end with '.lock'");
}
return sanitized;
}
export function validateRepositoryName(name: string): string {
const sanitized = name.trim().toLowerCase();
if (!sanitized) {
throw new Error("Repository name cannot be empty");
}
if (!/^[a-z0-9_.-]+$/.test(sanitized)) {
throw new Error(
"Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores"
);
}
if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
throw new Error("Repository name cannot start or end with a period");
}
return sanitized;
}
export function validateOwnerName(owner: string): string {
const sanitized = owner.trim().toLowerCase();
if (!sanitized) {
throw new Error("Owner name cannot be empty");
}
if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) {
throw new Error(
"Owner name must start with a letter or number and can contain up to 39 characters"
);
}
return sanitized;
}
export async function checkBranchExists(
owner: string,
repo: string,
branch: string
): Promise<boolean> {
try {
await githubRequest(
`https://api.github.com/repos/${owner}/${repo}/branches/${branch}`
);
return true;
} catch (error) {
if (error && typeof error === "object" && "status" in error && error.status === 404) {
return false;
} }
}); throw error;
return url.toString(); }
}
export async function checkUserExists(username: string): Promise<boolean> {
try {
await githubRequest(`https://api.github.com/users/${username}`);
return true;
} catch (error) {
if (error && typeof error === "object" && "status" in error && error.status === 404) {
return false;
}
throw error;
}
} }