claude-flow-multilang
Version:
Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture
625 lines (539 loc) • 17.6 kB
JavaScript
/**
* GitHub API Integration Module
* Provides authentication, rate limiting, and API wrappers for GitHub workflow commands
*
* Enhanced with GitHub CLI Safety Wrapper for secure command execution
*/
import { printSuccess, printError, printWarning, printInfo } from '../utils.js';
import { githubCli, GitHubCliSafe } from '../../utils/github-cli-safety-wrapper.js';
// GitHub API Configuration
const GITHUB_API_BASE = 'https://api.github.com';
const GITHUB_RATE_LIMIT = 5000; // API calls per hour
const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
class GitHubAPIClient {
constructor(token = null) {
this.token = token || process.env.GITHUB_TOKEN;
this.rateLimitRemaining = GITHUB_RATE_LIMIT;
this.rateLimitResetTime = null;
this.lastRequestTime = 0;
this.requestQueue = [];
this.isProcessingQueue = false;
// Initialize GitHub CLI safety wrapper
this.cliSafe = new GitHubCliSafe({
timeout: 60000, // 1 minute timeout for CLI operations
maxRetries: 3,
enableRateLimit: true,
enableLogging: false // Can be enabled for debugging
});
}
/**
* Authentication Methods
*/
async authenticate(token = null) {
if (token) {
this.token = token;
}
if (!this.token) {
printError('GitHub token not found. Set GITHUB_TOKEN environment variable or provide token.');
return false;
}
try {
const response = await this.request('/user');
if (response.success) {
printSuccess(`Authenticated as ${response.data.login}`);
return true;
}
return false;
} catch (error) {
printError(`Authentication failed: ${error.message}`);
return false;
}
}
/**
* Rate Limiting Management
*/
async checkRateLimit() {
if (this.rateLimitRemaining <= 1) {
const resetTime = new Date(this.rateLimitResetTime);
const now = new Date();
const waitTime = resetTime.getTime() - now.getTime();
if (waitTime > 0) {
printWarning(`Rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)}s...`);
await this.sleep(waitTime);
}
}
}
updateRateLimitInfo(headers) {
this.rateLimitRemaining = parseInt(headers['x-ratelimit-remaining'] || '0');
this.rateLimitResetTime = new Date((parseInt(headers['x-ratelimit-reset']) || 0) * 1000);
}
/**
* Core API Request Method
*/
async request(endpoint, options = {}) {
await this.checkRateLimit();
const url = endpoint.startsWith('http') ? endpoint : `${GITHUB_API_BASE}${endpoint}`;
const headers = {
Authorization: `token ${this.token}`,
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'Claude-Flow-GitHub-Integration',
...options.headers,
};
const requestOptions = {
method: options.method || 'GET',
headers,
...options,
};
if (options.body) {
requestOptions.body = JSON.stringify(options.body);
headers['Content-Type'] = 'application/json';
}
try {
const response = await fetch(url, requestOptions);
this.updateRateLimitInfo(response.headers);
const data = await response.json();
if (!response.ok) {
throw new Error(`GitHub API error: ${data.message || response.statusText}`);
}
return {
success: true,
data,
headers: response.headers,
status: response.status,
};
} catch (error) {
return {
success: false,
error: error.message,
status: error.status || 500,
};
}
}
/**
* Repository Operations
*/
async getRepository(owner, repo) {
return await this.request(`/repos/${owner}/${repo}`);
}
async listRepositories(options = {}) {
const params = new URLSearchParams({
sort: options.sort || 'updated',
direction: options.direction || 'desc',
per_page: options.perPage || 30,
page: options.page || 1,
});
return await this.request(`/user/repos?${params}`);
}
async createRepository(repoData) {
return await this.request('/user/repos', {
method: 'POST',
body: repoData,
});
}
/**
* Pull Request Operations
*/
async listPullRequests(owner, repo, options = {}) {
const params = new URLSearchParams({
state: options.state || 'open',
sort: options.sort || 'created',
direction: options.direction || 'desc',
per_page: options.perPage || 30,
page: options.page || 1,
});
return await this.request(`/repos/${owner}/${repo}/pulls?${params}`);
}
async createPullRequest(owner, repo, prData) {
return await this.request(`/repos/${owner}/${repo}/pulls`, {
method: 'POST',
body: prData,
});
}
async updatePullRequest(owner, repo, prNumber, prData) {
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}`, {
method: 'PATCH',
body: prData,
});
}
async mergePullRequest(owner, repo, prNumber, mergeData) {
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}/merge`, {
method: 'PUT',
body: mergeData,
});
}
async requestPullRequestReview(owner, repo, prNumber, reviewData) {
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, {
method: 'POST',
body: reviewData,
});
}
/**
* Issue Operations
*/
async listIssues(owner, repo, options = {}) {
const params = new URLSearchParams({
state: options.state || 'open',
sort: options.sort || 'created',
direction: options.direction || 'desc',
per_page: options.perPage || 30,
page: options.page || 1,
});
if (options.labels) {
params.append('labels', options.labels);
}
return await this.request(`/repos/${owner}/${repo}/issues?${params}`);
}
async createIssue(owner, repo, issueData) {
return await this.request(`/repos/${owner}/${repo}/issues`, {
method: 'POST',
body: issueData,
});
}
async updateIssue(owner, repo, issueNumber, issueData) {
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}`, {
method: 'PATCH',
body: issueData,
});
}
async addIssueLabels(owner, repo, issueNumber, labels) {
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
method: 'POST',
body: { labels },
});
}
async assignIssue(owner, repo, issueNumber, assignees) {
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/assignees`, {
method: 'POST',
body: { assignees },
});
}
/**
* Release Operations
*/
async listReleases(owner, repo, options = {}) {
const params = new URLSearchParams({
per_page: options.perPage || 30,
page: options.page || 1,
});
return await this.request(`/repos/${owner}/${repo}/releases?${params}`);
}
async createRelease(owner, repo, releaseData) {
return await this.request(`/repos/${owner}/${repo}/releases`, {
method: 'POST',
body: releaseData,
});
}
async updateRelease(owner, repo, releaseId, releaseData) {
return await this.request(`/repos/${owner}/${repo}/releases/${releaseId}`, {
method: 'PATCH',
body: releaseData,
});
}
async deleteRelease(owner, repo, releaseId) {
return await this.request(`/repos/${owner}/${repo}/releases/${releaseId}`, {
method: 'DELETE',
});
}
/**
* Workflow Operations
*/
async listWorkflows(owner, repo) {
return await this.request(`/repos/${owner}/${repo}/actions/workflows`);
}
async triggerWorkflow(owner, repo, workflowId, ref = 'main', inputs = {}) {
return await this.request(
`/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`,
{
method: 'POST',
body: { ref, inputs },
},
);
}
async listWorkflowRuns(owner, repo, options = {}) {
const params = new URLSearchParams({
per_page: options.perPage || 30,
page: options.page || 1,
});
if (options.status) {
params.append('status', options.status);
}
return await this.request(`/repos/${owner}/${repo}/actions/runs?${params}`);
}
/**
* Branch Operations
*/
async listBranches(owner, repo) {
return await this.request(`/repos/${owner}/${repo}/branches`);
}
async createBranch(owner, repo, branchName, sha) {
return await this.request(`/repos/${owner}/${repo}/git/refs`, {
method: 'POST',
body: {
ref: `refs/heads/${branchName}`,
sha,
},
});
}
async getBranchProtection(owner, repo, branch) {
return await this.request(`/repos/${owner}/${repo}/branches/${branch}/protection`);
}
async updateBranchProtection(owner, repo, branch, protection) {
return await this.request(`/repos/${owner}/${repo}/branches/${branch}/protection`, {
method: 'PUT',
body: protection,
});
}
/**
* Webhook Operations
*/
async listWebhooks(owner, repo) {
return await this.request(`/repos/${owner}/${repo}/hooks`);
}
async createWebhook(owner, repo, webhookData) {
return await this.request(`/repos/${owner}/${repo}/hooks`, {
method: 'POST',
body: webhookData,
});
}
async updateWebhook(owner, repo, hookId, webhookData) {
return await this.request(`/repos/${owner}/${repo}/hooks/${hookId}`, {
method: 'PATCH',
body: webhookData,
});
}
async deleteWebhook(owner, repo, hookId) {
return await this.request(`/repos/${owner}/${repo}/hooks/${hookId}`, {
method: 'DELETE',
});
}
/**
* Event Processing
*/
async processWebhookEvent(event, signature, payload) {
if (!this.verifyWebhookSignature(signature, payload)) {
throw new Error('Invalid webhook signature');
}
const eventData = JSON.parse(payload);
switch (event) {
case 'push':
return this.handlePushEvent(eventData);
case 'pull_request':
return this.handlePullRequestEvent(eventData);
case 'issues':
return this.handleIssuesEvent(eventData);
case 'release':
return this.handleReleaseEvent(eventData);
case 'workflow_run':
return this.handleWorkflowRunEvent(eventData);
default:
printInfo(`Unhandled event type: ${event}`);
return { handled: false, event };
}
}
verifyWebhookSignature(signature, payload) {
if (!GITHUB_WEBHOOK_SECRET) {
printWarning('GITHUB_WEBHOOK_SECRET not set. Skipping signature verification.');
return true;
}
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', GITHUB_WEBHOOK_SECRET);
hmac.update(payload);
const expectedSignature = `sha256=${hmac.digest('hex')}`;
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
/**
* Event Handlers
*/
async handlePushEvent(eventData) {
printInfo(`Push event: ${eventData.commits.length} commits to ${eventData.ref}`);
return { handled: true, event: 'push', data: eventData };
}
async handlePullRequestEvent(eventData) {
const action = eventData.action;
const pr = eventData.pull_request;
printInfo(`Pull request ${action}: #${pr.number} - ${pr.title}`);
return { handled: true, event: 'pull_request', action, data: eventData };
}
async handleIssuesEvent(eventData) {
const action = eventData.action;
const issue = eventData.issue;
printInfo(`Issue ${action}: #${issue.number} - ${issue.title}`);
return { handled: true, event: 'issues', action, data: eventData };
}
async handleReleaseEvent(eventData) {
const action = eventData.action;
const release = eventData.release;
printInfo(`Release ${action}: ${release.tag_name} - ${release.name}`);
return { handled: true, event: 'release', action, data: eventData };
}
async handleWorkflowRunEvent(eventData) {
const action = eventData.action;
const workflowRun = eventData.workflow_run;
printInfo(`Workflow run ${action}: ${workflowRun.name} - ${workflowRun.conclusion}`);
return { handled: true, event: 'workflow_run', action, data: eventData };
}
/**
* Utility Methods
*/
async sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
parseRepository(repoString) {
const match = repoString.match(/^([^/]+)\/([^/]+)$/);
if (!match) {
throw new Error('Invalid repository format. Use: owner/repo');
}
return { owner: match[1], repo: match[2] };
}
formatDate(dateString) {
return new Date(dateString).toLocaleString();
}
formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
/**
* Safe GitHub CLI Methods
* These methods use the GitHubCliSafe wrapper for secure command execution
*/
/**
* Create issue using GitHub CLI (safe alternative to createIssue API method)
* @param {Object} issueData - Issue data
* @returns {Promise<Object>} - CLI execution result
*/
async createIssueCLI(issueData) {
try {
const result = await this.cliSafe.createIssue({
title: issueData.title,
body: issueData.body,
labels: issueData.labels || [],
assignees: issueData.assignees || []
});
printSuccess(`Issue created via CLI: ${issueData.title}`);
return { success: true, data: result };
} catch (error) {
printError(`Failed to create issue via CLI: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Create PR using GitHub CLI (safe alternative to createPullRequest API method)
* @param {Object} prData - PR data
* @returns {Promise<Object>} - CLI execution result
*/
async createPullRequestCLI(prData) {
try {
const result = await this.cliSafe.createPR({
title: prData.title,
body: prData.body,
base: prData.base || 'main',
head: prData.head,
draft: prData.draft || false
});
printSuccess(`PR created via CLI: ${prData.title}`);
return { success: true, data: result };
} catch (error) {
printError(`Failed to create PR via CLI: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Add issue comment using GitHub CLI
* @param {number} issueNumber - Issue number
* @param {string} body - Comment body
* @returns {Promise<Object>} - CLI execution result
*/
async addIssueCommentCLI(issueNumber, body) {
try {
const result = await this.cliSafe.addIssueComment(issueNumber, body);
printSuccess(`Comment added to issue #${issueNumber}`);
return { success: true, data: result };
} catch (error) {
printError(`Failed to add comment to issue #${issueNumber}: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Add PR comment using GitHub CLI
* @param {number} prNumber - PR number
* @param {string} body - Comment body
* @returns {Promise<Object>} - CLI execution result
*/
async addPRCommentCLI(prNumber, body) {
try {
const result = await this.cliSafe.addPRComment(prNumber, body);
printSuccess(`Comment added to PR #${prNumber}`);
return { success: true, data: result };
} catch (error) {
printError(`Failed to add comment to PR #${prNumber}: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Create release using GitHub CLI
* @param {Object} releaseData - Release data
* @returns {Promise<Object>} - CLI execution result
*/
async createReleaseCLI(releaseData) {
try {
const result = await this.cliSafe.createRelease({
tag: releaseData.tag_name,
title: releaseData.name,
body: releaseData.body,
prerelease: releaseData.prerelease || false,
draft: releaseData.draft || false
});
printSuccess(`Release created via CLI: ${releaseData.tag_name}`);
return { success: true, data: result };
} catch (error) {
printError(`Failed to create release via CLI: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Check GitHub CLI authentication and availability
* @returns {Promise<boolean>} - True if CLI is ready to use
*/
async checkCLIStatus() {
try {
const isAvailable = await this.cliSafe.checkGitHubCli();
if (!isAvailable) {
printWarning('GitHub CLI is not installed or not in PATH');
return false;
}
const isAuthenticated = await this.cliSafe.checkAuthentication();
if (!isAuthenticated) {
printWarning('GitHub CLI is not authenticated. Run: gh auth login');
return false;
}
printSuccess('GitHub CLI is ready for use');
return true;
} catch (error) {
printError(`GitHub CLI status check failed: ${error.message}`);
return false;
}
}
/**
* Get GitHub CLI wrapper statistics
* @returns {Object} - Usage statistics
*/
getCLIStats() {
return this.cliSafe.getStats();
}
/**
* Cleanup CLI resources (call before shutdown)
*/
async cleanupCLI() {
await this.cliSafe.cleanup();
}
}
// Export singleton instance
export const githubAPI = new GitHubAPIClient();
export default GitHubAPIClient;