import { Octokit } from '@octokit/rest'; import { throttling } from '@octokit/plugin-throttling'; import { retry } from '@octokit/plugin-retry'; import logger from '../utils/logger.js'; const OctokitWithPlugins = Octokit.plugin(throttling, retry); class GitHubService { constructor(config) { this.octokit = new OctokitWithPlugins({ auth: config.github.token, request: { timeout: 15000, }, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { logger.warn(`Rate limit hit for ${options.method} ${options.url}`); if (retryCount < 2) return true; return false; }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { logger.warn(`Secondary rate limit hit for ${options.method} ${options.url}`); if (retryCount < 2) return true; return false; }, }, retry: { doNotRetry: [400, 401, 403, 404, 422], retries: 2, }, }); this.owner = config.github.owner; this.repo = config.github.repo; this._requestCount = 0; } async createIssue(title, body, labels = []) { try { const response = await this.octokit.issues.create({ owner: this.owner, repo: this.repo, title, body, labels, }); logger.info(`Created issue #${response.data.number}: ${title}`); return response.data; } catch (error) { logger.error(`Failed to create issue: ${error.message}`); throw error; } } async listIssues(state = 'open', labels = []) { try { const response = await this.octokit.issues.listForRepo({ owner: this.owner, repo: this.repo, state, labels: labels.length > 0 ? labels.join(',') : undefined, per_page: 30, }); return response.data; } catch (error) { logger.error(`Failed to list issues: ${error.message}`); return []; } } async createBranch(branchName, baseBranch = 'main') { try { const { data: ref } = await this.octokit.git.getRef({ owner: this.owner, repo: this.repo, ref: `heads/${baseBranch}`, }); await this.octokit.git.createRef({ owner: this.owner, repo: this.repo, ref: `refs/heads/${branchName}`, sha: ref.object.sha, }); logger.info(`Created branch: ${branchName} from ${baseBranch}`); return ref; } catch (error) { if (error.status === 422) { logger.warn(`Branch ${branchName} already exists`); } else { logger.error(`Failed to create branch: ${error.message}`); throw error; } } } async deleteBranch(branchName) { try { await this.octokit.git.deleteRef({ owner: this.owner, repo: this.repo, ref: `heads/${branchName}`, }); logger.info(`Deleted branch: ${branchName}`); } catch (error) { logger.error(`Failed to delete branch: ${error.message}`); } } async createPullRequest(title, body, head, base = 'main') { try { const response = await this.octokit.pulls.create({ owner: this.owner, repo: this.repo, title, body, head, base, }); logger.info(`Created PR #${response.data.number}: ${title}`); return response.data; } catch (error) { logger.error(`Failed to create PR: ${error.message}`); throw error; } } async listPullRequests(state = 'open') { try { const response = await this.octokit.pulls.list({ owner: this.owner, repo: this.repo, state, per_page: 20, }); return response.data; } catch (error) { logger.error(`Failed to list PRs: ${error.message}`); return []; } } async mergePullRequest(pullNumber, mergeMethod = 'merge') { try { const response = await this.octokit.pulls.merge({ owner: this.owner, repo: this.repo, pull_number: pullNumber, merge_method: mergeMethod, }); logger.info(`Merged PR #${pullNumber}`); return response.data; } catch (error) { logger.error(`Failed to merge PR: ${error.message}`); throw error; } } async addPullRequestComment(pullNumber, body) { try { await this.octokit.issues.createComment({ owner: this.owner, repo: this.repo, issue_number: pullNumber, body, }); logger.info(`Added comment to PR #${pullNumber}`); } catch (error) { logger.error(`Failed to add PR comment: ${error.message}`); } } async addPullRequestReview(pullNumber, event, body, commitId) { try { await this.octokit.pulls.createReview({ owner: this.owner, repo: this.repo, pull_number: pullNumber, body, event, commit_id: commitId, }); logger.info(`Added ${event} review to PR #${pullNumber}`); } catch (error) { logger.error(`Failed to add PR review: ${error.message}`); } } async getPullRequestFiles(pullNumber) { try { const response = await this.octokit.pulls.listFiles({ owner: this.owner, repo: this.repo, pull_number: pullNumber, }); return response.data; } catch (error) { logger.error(`Failed to get PR files: ${error.message}`); return []; } } async getPullRequest(pullNumber) { try { const response = await this.octokit.pulls.get({ owner: this.owner, repo: this.repo, pull_number: pullNumber, }); return response.data; } catch (error) { logger.error(`Failed to get PR: ${error.message}`); return null; } } async closeIssue(issueNumber) { try { await this.octokit.issues.update({ owner: this.owner, repo: this.repo, issue_number: issueNumber, state: 'closed', }); logger.info(`Closed issue #${issueNumber}`); } catch (error) { logger.error(`Failed to close issue: ${error.message}`); } } async addLabels(issueNumber, labels) { try { await this.octokit.issues.addLabels({ owner: this.owner, repo: this.repo, issue_number: issueNumber, labels, }); } catch (error) { logger.error(`Failed to add labels: ${error.message}`); } } async getIssueComments(issueNumber) { try { const response = await this.octokit.issues.listComments({ owner: this.owner, repo: this.repo, issue_number: issueNumber, }); return response.data; } catch (error) { logger.error(`Failed to get issue comments: ${error.message}`); return []; } } async addIssueComment(issueNumber, body) { try { const response = await this.octokit.issues.createComment({ owner: this.owner, repo: this.repo, issue_number: issueNumber, body, }); logger.info(`Added comment to issue #${issueNumber}`); return response.data; } catch (error) { logger.error(`Failed to add issue comment: ${error.message}`); throw error; } } async getRepositoryContent(path, ref = 'main') { try { const response = await this.octokit.repos.getContent({ owner: this.owner, repo: this.repo, path, ref, }); return response.data; } catch (error) { if (error.status === 404) { return null; } logger.error(`Failed to get repo content: ${error.message}`); return null; } } async createOrUpdateFile(path, content, message, branch, sha = null) { try { const contentBase64 = Buffer.from(content).toString('base64'); if (!sha) { const existing = await this.getRepositoryContent(path, branch); sha = existing?.sha || null; } const response = await this.octokit.repos.createOrUpdateFileContents({ owner: this.owner, repo: this.repo, path, message, content: contentBase64, branch, sha, }); logger.info(`Updated file: ${path} on ${branch}`); return response.data; } catch (error) { logger.error(`Failed to update file: ${error.message}`); throw error; } } async getCommitHistory(branch = 'main', perPage = 10) { try { const response = await this.octokit.repos.listCommits({ owner: this.owner, repo: this.repo, sha: branch, per_page: perPage, }); return response.data; } catch (error) { logger.error(`Failed to get commit history: ${error.message}`); return []; } } async getRepositoryInfo() { try { const response = await this.octokit.repos.get({ owner: this.owner, repo: this.repo, }); return response.data; } catch (error) { logger.error(`Failed to get repository info: ${error.message}`); return null; } } getStats() { return { requestCount: this._requestCount, owner: this.owner, repo: this.repo, }; } } export default GitHubService;