activity-simulator / src /services /githubService.js
abedelbahnasy55's picture
fix: timeouts, error handling, duplicate title fix
ca1b6c1
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;