UNPKG

near-protocol-rewards

Version:

A transparent, metric-based rewards system for NEAR projects

237 lines (236 loc) 8.37 kB
"use strict"; /** * GitHub Metrics Collector * * Collects metrics from GitHub repositories including: * - Commits * - Pull requests * - Issues and comments * * Features built-in rate limiting and error handling for API calls. * Uses exponential backoff for retries on transient failures. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GitHubCollector = void 0; const rest_1 = require("@octokit/rest"); const base_1 = require("./base"); const errors_1 = require("../types/errors"); class GitHubCollector extends base_1.BaseCollector { constructor({ token, repo, logger, rateLimiter, }) { super(logger, rateLimiter); const [owner, repoName] = repo.split("/"); if (!owner || !repoName) { throw new Error("Invalid repository format. Expected 'owner/repo'"); } this.owner = owner; this.repo = repoName; this.octokit = new rest_1.Octokit({ auth: token }); } async testConnection() { try { await this.withRateLimit(async () => { await this.octokit.rest.repos.get({ owner: this.owner, repo: this.repo, }); }); } catch (error) { this.error("Failed to test GitHub connection", { error }); throw error; } } async collectCommitMetrics() { const commits = await this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/commits', { owner: this.owner, repo: this.repo, per_page: 100, }); return response; }); const authors = new Map(); const daily = new Array(7).fill(0); let weekly = 0; let monthly = 0; for (const commit of commits) { const login = commit.author?.login; if (login) { authors.set(login, (authors.get(login) || 0) + 1); } const date = new Date(commit.commit.author?.date || ""); const dayIndex = date.getDay(); if (!isNaN(dayIndex)) { daily[dayIndex]++; } const now = new Date(); const daysDiff = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff <= 7) { weekly++; } if (daysDiff <= 30) { monthly++; } } return { count: commits.length, frequency: { daily, weekly, monthly, }, authors: Array.from(authors.entries()).map(([login, count]) => ({ login, count, })), }; } async collectPullRequestMetrics() { const [openPRs, closedPRs] = await Promise.all([ this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', { owner: this.owner, repo: this.repo, state: "open", per_page: 100, }); return response; }), this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', { owner: this.owner, repo: this.repo, state: "closed", per_page: 100, }); return response; }), ]); const authors = new Set(); [...openPRs, ...closedPRs].forEach((pr) => { if (pr.user?.login) { authors.add(pr.user.login); } }); return { open: openPRs.length, merged: closedPRs.filter((pr) => pr.merged_at !== null).length, closed: closedPRs.filter((pr) => pr.merged_at === null).length, authors: Array.from(authors), }; } async collectReviewMetrics() { const allPRs = await this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', { owner: this.owner, repo: this.repo, state: "all", per_page: 100, }); return response; }); const authors = new Set(); let count = 0; for (const pr of allPRs) { const reviews = await this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews', { owner: this.owner, repo: this.repo, pull_number: pr.number, per_page: 100, }); return response; }); count += reviews.length; reviews.forEach((review) => { if (review.user?.login) { authors.add(review.user.login); } }); } return { count, authors: Array.from(authors), }; } async collectIssueMetrics() { const [openIssues, closedIssues] = await Promise.all([ this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', { owner: this.owner, repo: this.repo, state: "open", per_page: 100, }); return response; }), this.withRateLimit(async () => { const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', { owner: this.owner, repo: this.repo, state: "closed", per_page: 100, }); return response; }), ]); const participants = new Set(); [...openIssues, ...closedIssues].forEach((issue) => { if (issue.user?.login) { participants.add(issue.user.login); } if (issue.assignee?.login) { participants.add(issue.assignee.login); } issue.assignees?.forEach((assignee) => { if (assignee.login) { participants.add(assignee.login); } }); }); return { open: openIssues.length, closed: closedIssues.length, participants: Array.from(participants), }; } async collectMetrics() { try { const [commits, pullRequests, reviews, issues] = await Promise.all([ this.collectCommitMetrics(), this.collectPullRequestMetrics(), this.collectReviewMetrics(), this.collectIssueMetrics(), ]); return { commits, pullRequests, reviews, issues, metadata: { collectionTimestamp: Date.now(), source: "github", projectId: `${this.owner}/${this.repo}`, }, }; } catch (error) { if (error && typeof error === 'object' && 'status' in error && 'message' in error && error.status === 403 && typeof error.message === 'string' && error.message.includes('not accessible by integration')) { throw new errors_1.BaseError('GitHub permissions error: Please ensure your workflow has the correct permissions. ' + 'Add these permissions to your workflow file:\n\n' + 'permissions:\n' + ' contents: read\n' + ' issues: read\n' + ' pull-requests: read\n', errors_1.ErrorCode.UNAUTHORIZED, { originalError: error }); } throw error; } } } exports.GitHubCollector = GitHubCollector;