UNPKG

@issue-linker/core

Version:
254 lines 8.63 kB
// Infrastructure layer - GitHub API operations import { retry } from "@octokit/plugin-retry"; import { throttling } from "@octokit/plugin-throttling"; import { RequestError } from "@octokit/request-error"; import { Octokit } from "octokit"; import { GitHubError } from "../domain/errors.js"; import { getGitHubApiUrl, getGitHubToken } from "./env-accessor.js"; // Create custom Octokit with retry and throttling plugins const MyOctokit = Octokit.plugin(retry, throttling); /** * Create an Octokit instance with proper configuration * @param token - Optional GitHub token * @param hostname - Optional GitHub hostname for Enterprise * @returns Configured Octokit instance */ function createOctokit(token, hostname) { const auth = token ?? getGitHubToken(); // Build API URL from hostname if provided let baseUrl; if (hostname) { const cleanHostname = hostname .replace(/^https?:\/\//, "") .replace(/\/+$/, ""); baseUrl = cleanHostname === "github.com" ? "https://api.github.com" : `https://${cleanHostname}/api/v3`; } else { baseUrl = getGitHubApiUrl(); } return new MyOctokit({ auth, userAgent: "issue-linker", baseUrl, request: { // Aggressive timeout for fast feedback during development // Adjust this value if you need more reliability vs speed timeout: 1000, // 1 second timeout (tunable) }, retry: { // Disable all retries for 404 and rate limits doNotRetry: ["404", "429"], // No retries for maximum speed retries: 0, // 0 = no retries (tunable: increase for reliability) }, throttle: { onRateLimit: (_retryAfter, _options, _octokit, _retryCount) => { // No retries on rate limit for speed return false; // false = don't retry (tunable: return true for retry) }, onSecondaryRateLimit: (_retryAfter, _options, _octokit) => { // Don't retry on abuse detection return false; }, }, }); } /** * Get an issue from GitHub * @param owner - Repository owner * @param repo - Repository name * @param issueNumber - Issue number * @param token - Optional GitHub token * @param hostname - Optional GitHub hostname for Enterprise * @returns Result object containing issue or error information */ export async function getIssue(owner, repo, issueNumber, token, hostname) { const octokit = createOctokit(token, hostname); try { const { data } = await octokit.rest.issues.get({ owner, repo, issue_number: issueNumber, headers: { Accept: "application/vnd.github+json", }, }); // Convert GitHub issue to our Issue type const issue = { number: data.number, state: (data.state?.toLowerCase() ?? "open"), title: data.title, }; // Add optional body if it exists if (data.body) { issue.body = data.body; } return { found: true, issue, }; } catch (error) { // Handle different error cases appropriately if (error instanceof RequestError) { if (error.status === 404) { // Issue doesn't exist - this is a normal case return { found: false, error: { type: "not-found", message: `Issue #${issueNumber} not found`, }, }; } if (error.status === 401) { return { found: false, error: { type: "unauthorized", message: "Unauthorized access to GitHub API", }, }; } // For other API errors return { found: false, error: { type: "api-error", message: error.message, }, }; } // Network or unexpected errors return { found: false, error: { type: "network-error", message: error instanceof Error ? error.message : String(error), }, }; } } /** * Fetch commits from a pull request * @param owner - Repository owner * @param repo - Repository name * @param prNumber - Pull request number * @param token - Optional GitHub token * @param hostname - Optional GitHub hostname for Enterprise * @returns Array of pull request commits * @throws GitHubError for API errors */ export async function fetchPullRequestCommits(owner, repo, prNumber, token, hostname) { const octokit = createOctokit(token, hostname); try { const { data } = await octokit.rest.pulls.listCommits({ owner, repo, pull_number: prNumber, headers: { Accept: "application/vnd.github+json", }, }); // Transform Octokit response to domain type return data.map((commit) => ({ sha: commit.sha, message: commit.commit.message, author: { name: commit.commit.author?.name ?? "Unknown", email: commit.commit.author?.email ?? "unknown@example.com", }, })); } catch (error) { // Handle API errors if (error instanceof RequestError) { throw new GitHubError(`Failed to fetch commits for PR #${prNumber}: ${error.message}`, error.status); } // Re-throw unexpected errors throw error; } } /** * Create a comment on an issue (internal use only) * @param owner - Repository owner * @param repo - Repository name * @param issueNumber - Issue number * @param body - Comment body * @param token - Optional GitHub token * @param hostname - Optional GitHub hostname for Enterprise * @returns Comment ID on success * @internal */ export async function createIssueComment(owner, repo, issueNumber, body, token, hostname) { const octokit = createOctokit(token, hostname); try { const { data } = await octokit.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body, headers: { Accept: "application/vnd.github+json", }, }); return data.id; } catch (error) { // Handle API errors if (error instanceof RequestError) { if (error.status === 404) { throw new GitHubError(`Issue #${issueNumber} not found`, error.status); } if (error.status === 403 || error.status === 401) { throw new GitHubError("Insufficient permissions to comment on issues", error.status); } throw new GitHubError(`Failed to create comment on issue #${issueNumber}: ${error.message}`, error.status); } // Re-throw unexpected errors throw error; } } /** * List comments on an issue (internal use only) * @param owner - Repository owner * @param repo - Repository name * @param issueNumber - Issue number * @param token - Optional GitHub token * @param hostname - Optional GitHub hostname for Enterprise * @returns Array of comment bodies * @internal */ export async function listIssueComments(owner, repo, issueNumber, token, hostname) { const octokit = createOctokit(token, hostname); try { const { data } = await octokit.rest.issues.listComments({ owner, repo, issue_number: issueNumber, headers: { Accept: "application/vnd.github+json", }, }); return data.map((comment) => ({ id: comment.id, body: comment.body ?? "", })); } catch (error) { // Handle API errors if (error instanceof RequestError) { if (error.status === 404) { // Issue doesn't exist - return empty array return []; } throw new GitHubError(`Failed to list comments for issue #${issueNumber}: ${error.message}`, error.status); } // Re-throw unexpected errors throw error; } } //# sourceMappingURL=github-client.js.map