github-pr-automation
Version:
MCP server and CLI for automated GitHub PR management, review resolution, and workflow optimization
251 lines • 9.67 kB
JavaScript
import { Octokit } from "@octokit/rest";
/**
* GitHub API client wrapper with authentication and error handling.
*
* This class provides a high-level interface to the GitHub API with built-in
* authentication, error handling, and token validation. It wraps the Octokit
* client and adds application-specific functionality.
*
* ## Features
*
* - **Authentication**: Automatic token validation and scope checking
* - **Error Handling**: Normalized GitHub API errors with context
* - **Token Validation**: Support for both classic PATs and fine-grained tokens
* - **Rate Limiting**: Built-in rate limit monitoring
* - **Testing Support**: Dependency injection for testing
*
* ## Authentication
*
* The client requires a GitHub Personal Access Token (PAT) with appropriate
* scopes. It supports both:
* - **Classic PATs**: With `repo` scope for full repository access
* - **Fine-grained tokens**: With repository-specific permissions
*
* ## Error Handling
*
* All API calls are wrapped with error handling that:
* - Normalizes GitHub API errors into consistent format
* - Provides context about the failed operation
* - Throws structured errors with operation details
*
* @example
* ```typescript
* // Initialize with environment token
* const client = new GitHubClient();
*
* // Initialize with explicit token
* const client = new GitHubClient('ghp_your_token_here');
*
* // Initialize for testing
* const client = new GitHubClient(undefined, mockOctokit);
* ```
*/
export class GitHubClient {
octokit;
/**
* Initialize GitHub client with authentication token or inject Octokit instance.
*
* The constructor handles authentication setup and validates that a token
* is available. It supports both explicit tokens and environment variables,
* as well as dependency injection for testing.
*
* ## Token Resolution Order
*
* 1. **Explicit Token**: If provided as parameter
* 2. **Environment Variable**: `GITHUB_TOKEN` from process.env
* 3. **Error**: If no token found, throws descriptive error
*
* ## Testing Support
*
* When `octokitInstance` is provided, the client skips token validation
* and uses the provided instance directly. This enables easy mocking
* in unit tests.
*
* @param token - GitHub Personal Access Token (optional, falls back to GITHUB_TOKEN env var)
* @param octokitInstance - Pre-configured Octokit instance for testing (optional)
* @throws Error if no token is found and no octokitInstance is provided
*/
constructor(token, octokitInstance) {
// Use injected Octokit instance for testing
if (octokitInstance) {
this.octokit = octokitInstance;
}
else {
// Resolve token from parameter or environment
const githubToken = token || process.env.GITHUB_TOKEN;
// Validate that token is available
if (!githubToken) {
throw new Error("GitHub token not found. Set GITHUB_TOKEN environment variable.");
}
// Initialize Octokit with authentication
this.octokit = new Octokit({
auth: githubToken,
});
}
}
/**
* Validate GitHub token and check permissions.
*
* This method performs a lightweight authentication check by calling the
* GitHub API to verify the token is valid and has appropriate permissions.
* It supports both classic Personal Access Tokens and fine-grained tokens.
*
* ## Token Types Supported
*
* ### Classic Personal Access Tokens (PATs)
* - Include `x-oauth-scopes` header with comma-separated scopes
* - Require `repo` or `public_repo` scope for repository access
* - Full repository access when properly scoped
*
* ### Fine-grained Personal Access Tokens
* - Do not include `x-oauth-scopes` header
* - Have repository-specific permissions
* - More granular access control
*
* ## Validation Process
*
* 1. **API Call**: Call `GET /user` to verify token validity
* 2. **Scope Parsing**: Extract scopes from response headers
* 3. **Permission Check**: Verify `repo` scope for classic tokens
* 4. **Fine-grained Handling**: Skip scope check for fine-grained tokens
*
* @returns Promise resolving to validation result with user info and scopes
*
* @example
* ```typescript
* const validation = await client.validateToken();
* if (validation.valid) {
* console.log(`Authenticated as: ${validation.user}`);
* console.log(`Scopes: ${validation.scopes.join(', ')}`);
* } else {
* console.error(`Token invalid: ${validation.error}`);
* }
* ```
*/
async validateToken() {
try {
// Call GitHub API to verify token validity and get user info
const response = await this.octokit.users.getAuthenticated();
const user = response.data;
// Parse scopes from response headers (may be undefined for fine-grained tokens)
const scopeHeader = response.headers?.["x-oauth-scopes"] ?? "";
const scopes = scopeHeader
? String(scopeHeader)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
// Check for required repository access scope
const hasRepo = scopes.includes("repo") || scopes.includes("public_repo");
// Only enforce classic-scope check when header is present
// Fine-grained tokens don't have x-oauth-scopes header
if (scopes.length > 0 && !hasRepo) {
return {
valid: false,
error: 'Token missing required "repo" scope',
};
}
// Return successful validation with user info
return {
valid: true,
user: user.login,
scopes: scopes,
};
}
catch (error) {
// Handle any authentication errors
return {
valid: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
* Get pull request details by identifier.
*
* Fetches comprehensive pull request information from the GitHub API,
* including metadata, diff stats, and status information. All errors
* are normalized and include context about the failed operation.
*
* @param pr - PR identifier containing owner, repo, and number
* @returns Promise resolving to complete PR data from GitHub API
* @throws Error with normalized GitHub API errors and operation context
*
* @example
* ```typescript
* const pr = await client.getPullRequest({ owner: 'microsoft', repo: 'vscode', number: 12345 });
* console.log(`PR Title: ${pr.title}`);
* console.log(`State: ${pr.state}`);
* ```
*/
async getPullRequest(pr) {
try {
// Fetch PR data from GitHub API
const { data } = await this.octokit.pulls.get({
owner: pr.owner,
repo: pr.repo,
pull_number: pr.number,
});
return data;
}
catch (error) {
// Normalize GitHub API errors with operation context
const { handleGitHubError } = await import("./errors.js");
const toolError = handleGitHubError(error, `GET /repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`);
throw new Error(JSON.stringify(toolError));
}
}
/**
* Get current GitHub API rate limit status.
*
* Retrieves the current rate limit information for the authenticated token,
* including remaining requests, reset time, and limit details. This is useful
* for monitoring API usage and implementing rate limit-aware logic.
*
* @returns Promise resolving to rate limit information
* @throws Error with normalized GitHub API errors
*
* @example
* ```typescript
* const rateLimit = await client.getRateLimit();
* console.log(`Remaining requests: ${rateLimit.remaining}/${rateLimit.limit}`);
* console.log(`Resets at: ${new Date(rateLimit.reset * 1000)}`);
* ```
*/
async getRateLimit() {
try {
// Fetch rate limit information from GitHub API
const { data } = await this.octokit.rateLimit.get();
return data;
}
catch (error) {
// Normalize GitHub API errors with operation context
const { handleGitHubError } = await import("./errors.js");
const toolError = handleGitHubError(error, "GET /rate_limit");
throw new Error(JSON.stringify(toolError));
}
}
/**
* Get the underlying Octokit instance for direct API access.
*
* This method provides access to the raw Octokit client for advanced
* operations that aren't covered by the high-level methods. Use this
* when you need direct access to GitHub API endpoints.
*
* @returns Octokit instance for advanced GitHub API operations
*
* @example
* ```typescript
* const octokit = client.getOctokit();
* const { data: issues } = await octokit.issues.listForRepo({
* owner: 'microsoft',
* repo: 'vscode',
* state: 'open'
* });
* ```
*/
getOctokit() {
return this.octokit;
}
}
//# sourceMappingURL=client.js.map