@0xbigboss/gh-pulse-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.cjs +312 -0
- package/dist/cache.d.cts +58 -0
- package/dist/cache.d.ts +58 -0
- package/dist/cache.js +303 -0
- package/dist/config.cjs +182 -0
- package/dist/config.d.cts +248 -0
- package/dist/config.d.ts +248 -0
- package/dist/config.js +172 -0
- package/dist/filters.cjs +16 -0
- package/dist/filters.d.cts +3 -0
- package/dist/filters.d.ts +3 -0
- package/dist/filters.js +12 -0
- package/dist/github.cjs +240 -0
- package/dist/github.d.cts +46 -0
- package/dist/github.d.ts +46 -0
- package/dist/github.js +235 -0
- package/dist/index.cjs +28 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/reports/context.cjs +8 -0
- package/dist/reports/context.d.cts +7 -0
- package/dist/reports/context.d.ts +7 -0
- package/dist/reports/context.js +5 -0
- package/dist/reports/exec.cjs +160 -0
- package/dist/reports/exec.d.cts +6 -0
- package/dist/reports/exec.d.ts +6 -0
- package/dist/reports/exec.js +157 -0
- package/dist/reports/index.cjs +21 -0
- package/dist/reports/index.d.cts +5 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/meta.cjs +15 -0
- package/dist/reports/meta.d.cts +12 -0
- package/dist/reports/meta.d.ts +12 -0
- package/dist/reports/meta.js +12 -0
- package/dist/reports/personal.cjs +90 -0
- package/dist/reports/personal.d.cts +8 -0
- package/dist/reports/personal.d.ts +8 -0
- package/dist/reports/personal.js +87 -0
- package/dist/reports/team.cjs +127 -0
- package/dist/reports/team.d.cts +6 -0
- package/dist/reports/team.d.ts +6 -0
- package/dist/reports/team.js +124 -0
- package/dist/reports/types.cjs +2 -0
- package/dist/reports/types.d.cts +144 -0
- package/dist/reports/types.d.ts +144 -0
- package/dist/reports/types.js +1 -0
- package/dist/reports/utils.cjs +71 -0
- package/dist/reports/utils.d.cts +6 -0
- package/dist/reports/utils.d.ts +6 -0
- package/dist/reports/utils.js +65 -0
- package/dist/repos.cjs +102 -0
- package/dist/repos.d.cts +12 -0
- package/dist/repos.d.ts +12 -0
- package/dist/repos.js +96 -0
- package/dist/sync.cjs +360 -0
- package/dist/sync.d.cts +24 -0
- package/dist/sync.d.ts +24 -0
- package/dist/sync.js +357 -0
- package/dist/team.cjs +45 -0
- package/dist/team.d.cts +10 -0
- package/dist/team.d.ts +10 -0
- package/dist/team.js +42 -0
- package/dist/time.cjs +153 -0
- package/dist/time.d.cts +13 -0
- package/dist/time.d.ts +13 -0
- package/dist/time.js +145 -0
- package/dist/types.cjs +2 -0
- package/dist/types.d.cts +133 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +1 -0
- package/package.json +29 -0
package/dist/filters.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isBotUsername(username) {
|
|
2
|
+
return /\[bot\]$/i.test(username);
|
|
3
|
+
}
|
|
4
|
+
export function isExcludedAuthor(username, exclude, excludeBots) {
|
|
5
|
+
if (exclude.has(username)) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (excludeBots && isBotUsername(username)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
package/dist/github.cjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitHubClient = void 0;
|
|
4
|
+
exports.splitRepoFullName = splitRepoFullName;
|
|
5
|
+
const octokit_1 = require("octokit");
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
7
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
8
|
+
const DEFAULT_BASE_DELAY_MS = 500;
|
|
9
|
+
class GitHubClient {
|
|
10
|
+
octokit;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
maxRetries;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.octokit = new octokit_1.Octokit({ auth: options.token });
|
|
15
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
16
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
17
|
+
}
|
|
18
|
+
async getAuthenticatedUsername() {
|
|
19
|
+
const response = await this.withRetry((signal) => this.octokit.rest.users.getAuthenticated({ request: { signal } }));
|
|
20
|
+
return response.data.login;
|
|
21
|
+
}
|
|
22
|
+
async userExists(username) {
|
|
23
|
+
try {
|
|
24
|
+
await this.withRetry((signal) => this.octokit.rest.users.getByUsername({ username, request: { signal } }));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const status = getStatus(error);
|
|
29
|
+
if (status === 404) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async listOrgMembers(org) {
|
|
36
|
+
const members = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.orgs.listMembers, {
|
|
37
|
+
org,
|
|
38
|
+
per_page: 100,
|
|
39
|
+
request: { signal },
|
|
40
|
+
}));
|
|
41
|
+
return members.map((member) => member.login);
|
|
42
|
+
}
|
|
43
|
+
async listOrgRepos(org) {
|
|
44
|
+
const repos = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listForOrg, {
|
|
45
|
+
org,
|
|
46
|
+
per_page: 100,
|
|
47
|
+
type: 'all',
|
|
48
|
+
request: { signal },
|
|
49
|
+
}));
|
|
50
|
+
return repos.map((repo) => repo.full_name);
|
|
51
|
+
}
|
|
52
|
+
async listUserRepos(username) {
|
|
53
|
+
const repos = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listForUser, {
|
|
54
|
+
username,
|
|
55
|
+
per_page: 100,
|
|
56
|
+
type: 'all',
|
|
57
|
+
request: { signal },
|
|
58
|
+
}));
|
|
59
|
+
return repos.map((repo) => repo.full_name);
|
|
60
|
+
}
|
|
61
|
+
async listPullRequests(repo) {
|
|
62
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
63
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.pulls.list, {
|
|
64
|
+
owner,
|
|
65
|
+
repo: name,
|
|
66
|
+
per_page: 100,
|
|
67
|
+
state: 'all',
|
|
68
|
+
sort: 'updated',
|
|
69
|
+
direction: 'desc',
|
|
70
|
+
request: { signal },
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
async getPullRequest(repo, number) {
|
|
74
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
75
|
+
const response = await this.withRetry((signal) => this.octokit.rest.pulls.get({
|
|
76
|
+
owner,
|
|
77
|
+
repo: name,
|
|
78
|
+
pull_number: number,
|
|
79
|
+
request: { signal },
|
|
80
|
+
}));
|
|
81
|
+
return response.data;
|
|
82
|
+
}
|
|
83
|
+
async listPullRequestReviews(repo, number) {
|
|
84
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
85
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.pulls.listReviews, {
|
|
86
|
+
owner,
|
|
87
|
+
repo: name,
|
|
88
|
+
pull_number: number,
|
|
89
|
+
per_page: 100,
|
|
90
|
+
request: { signal },
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
async listIssueComments(repo, number) {
|
|
94
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
95
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.issues.listComments, {
|
|
96
|
+
owner,
|
|
97
|
+
repo: name,
|
|
98
|
+
issue_number: number,
|
|
99
|
+
per_page: 100,
|
|
100
|
+
request: { signal },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
async listIssueEvents(repo, number) {
|
|
104
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
105
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.issues.listEvents, {
|
|
106
|
+
owner,
|
|
107
|
+
repo: name,
|
|
108
|
+
issue_number: number,
|
|
109
|
+
per_page: 100,
|
|
110
|
+
request: { signal },
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
async listCommits(repo, options = {}) {
|
|
114
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
115
|
+
return this.withRetry((signal) => {
|
|
116
|
+
const params = {
|
|
117
|
+
owner,
|
|
118
|
+
repo: name,
|
|
119
|
+
per_page: 100,
|
|
120
|
+
request: { signal },
|
|
121
|
+
};
|
|
122
|
+
if (options.since) {
|
|
123
|
+
params.since = options.since;
|
|
124
|
+
}
|
|
125
|
+
if (options.until) {
|
|
126
|
+
params.until = options.until;
|
|
127
|
+
}
|
|
128
|
+
return this.octokit.paginate(this.octokit.rest.repos.listCommits, params);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async getCommit(repo, sha) {
|
|
132
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
133
|
+
const response = await this.withRetry((signal) => this.octokit.rest.repos.getCommit({
|
|
134
|
+
owner,
|
|
135
|
+
repo: name,
|
|
136
|
+
ref: sha,
|
|
137
|
+
request: { signal },
|
|
138
|
+
}));
|
|
139
|
+
return response.data;
|
|
140
|
+
}
|
|
141
|
+
async ensureRepoAccessible(repo) {
|
|
142
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
143
|
+
await this.withRetry((signal) => this.octokit.rest.repos.get({ owner, repo: name, request: { signal } }));
|
|
144
|
+
}
|
|
145
|
+
async listContributors(repo) {
|
|
146
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
147
|
+
const contributors = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listContributors, {
|
|
148
|
+
owner,
|
|
149
|
+
repo: name,
|
|
150
|
+
per_page: 100,
|
|
151
|
+
request: { signal },
|
|
152
|
+
}));
|
|
153
|
+
return contributors
|
|
154
|
+
.map((contributor) => contributor.login)
|
|
155
|
+
.filter((login) => Boolean(login));
|
|
156
|
+
}
|
|
157
|
+
async getRepo(repo) {
|
|
158
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
159
|
+
const response = await this.withRetry((signal) => this.octokit.rest.repos.get({ owner, repo: name, request: { signal } }));
|
|
160
|
+
return response.data;
|
|
161
|
+
}
|
|
162
|
+
async withRetry(fn) {
|
|
163
|
+
let attempt = 0;
|
|
164
|
+
let lastError;
|
|
165
|
+
while (attempt < this.maxRetries) {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
168
|
+
try {
|
|
169
|
+
const result = await fn(controller.signal);
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
lastError = error;
|
|
176
|
+
const retryAfter = await this.getRetryDelay(error, attempt);
|
|
177
|
+
if (retryAfter === null) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
await delay(retryAfter);
|
|
181
|
+
attempt += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw wrapError(lastError, 'GitHub API request failed');
|
|
185
|
+
}
|
|
186
|
+
async getRetryDelay(error, attempt) {
|
|
187
|
+
const status = getStatus(error);
|
|
188
|
+
if (status === 403) {
|
|
189
|
+
const reset = getHeader(error, 'x-ratelimit-reset');
|
|
190
|
+
const remaining = getHeader(error, 'x-ratelimit-remaining');
|
|
191
|
+
if (remaining === '0' && reset) {
|
|
192
|
+
const resetMs = Number(reset) * 1000;
|
|
193
|
+
const wait = Math.max(resetMs - Date.now(), DEFAULT_BASE_DELAY_MS);
|
|
194
|
+
return Math.min(wait, 60_000);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (status && status < 500 && status !== 429) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const backoff = DEFAULT_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
201
|
+
return Math.min(backoff, 8_000);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
exports.GitHubClient = GitHubClient;
|
|
205
|
+
function splitRepoFullName(repo) {
|
|
206
|
+
const [owner, name] = repo.split('/');
|
|
207
|
+
if (!owner || !name) {
|
|
208
|
+
throw new Error(`Invalid repo full name: ${repo}`);
|
|
209
|
+
}
|
|
210
|
+
return { owner, name };
|
|
211
|
+
}
|
|
212
|
+
function delay(ms) {
|
|
213
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
214
|
+
}
|
|
215
|
+
function getStatus(error) {
|
|
216
|
+
if (typeof error === 'object' && error && 'status' in error) {
|
|
217
|
+
const status = error.status;
|
|
218
|
+
return status;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
function getHeader(error, name) {
|
|
223
|
+
if (typeof error === 'object' && error && 'response' in error) {
|
|
224
|
+
const response = error.response;
|
|
225
|
+
return response?.headers?.[name];
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
function wrapError(error, message) {
|
|
230
|
+
if (error instanceof Error) {
|
|
231
|
+
const wrapped = new Error(`${message}: ${error.message}`);
|
|
232
|
+
// Preserve status for HTTP errors (e.g., Octokit RequestError)
|
|
233
|
+
if ('status' in error && typeof error.status === 'number') {
|
|
234
|
+
;
|
|
235
|
+
wrapped.status = error.status;
|
|
236
|
+
}
|
|
237
|
+
return wrapped;
|
|
238
|
+
}
|
|
239
|
+
return new Error(message);
|
|
240
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Endpoints } from '@octokit/types';
|
|
2
|
+
import type { GitHubUsername, RepoFullName } from "./types.cjs";
|
|
3
|
+
export interface GitHubClientOptions {
|
|
4
|
+
token: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
}
|
|
8
|
+
export type PullRequestListItem = Endpoints['GET /repos/{owner}/{repo}/pulls']['response']['data'][number];
|
|
9
|
+
export type PullRequestDetail = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response']['data'];
|
|
10
|
+
export type PullRequestReview = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews']['response']['data'][number];
|
|
11
|
+
export type IssueComment = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data'][number];
|
|
12
|
+
export type IssueEvent = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/events']['response']['data'][number];
|
|
13
|
+
export type CommitListItem = Endpoints['GET /repos/{owner}/{repo}/commits']['response']['data'][number];
|
|
14
|
+
export type CommitDetail = Endpoints['GET /repos/{owner}/{repo}/commits/{ref}']['response']['data'];
|
|
15
|
+
export type RepoDetail = Endpoints['GET /repos/{owner}/{repo}']['response']['data'];
|
|
16
|
+
export type Contributor = Endpoints['GET /repos/{owner}/{repo}/contributors']['response']['data'][number];
|
|
17
|
+
export declare class GitHubClient {
|
|
18
|
+
private readonly octokit;
|
|
19
|
+
private readonly timeoutMs;
|
|
20
|
+
private readonly maxRetries;
|
|
21
|
+
constructor(options: GitHubClientOptions);
|
|
22
|
+
getAuthenticatedUsername(): Promise<GitHubUsername>;
|
|
23
|
+
userExists(username: GitHubUsername): Promise<boolean>;
|
|
24
|
+
listOrgMembers(org: string): Promise<GitHubUsername[]>;
|
|
25
|
+
listOrgRepos(org: string): Promise<RepoFullName[]>;
|
|
26
|
+
listUserRepos(username: string): Promise<RepoFullName[]>;
|
|
27
|
+
listPullRequests(repo: RepoFullName): Promise<PullRequestListItem[]>;
|
|
28
|
+
getPullRequest(repo: RepoFullName, number: number): Promise<PullRequestDetail>;
|
|
29
|
+
listPullRequestReviews(repo: RepoFullName, number: number): Promise<PullRequestReview[]>;
|
|
30
|
+
listIssueComments(repo: RepoFullName, number: number): Promise<IssueComment[]>;
|
|
31
|
+
listIssueEvents(repo: RepoFullName, number: number): Promise<IssueEvent[]>;
|
|
32
|
+
listCommits(repo: RepoFullName, options?: {
|
|
33
|
+
since?: string;
|
|
34
|
+
until?: string;
|
|
35
|
+
}): Promise<CommitListItem[]>;
|
|
36
|
+
getCommit(repo: RepoFullName, sha: string): Promise<CommitDetail>;
|
|
37
|
+
ensureRepoAccessible(repo: RepoFullName): Promise<void>;
|
|
38
|
+
listContributors(repo: RepoFullName): Promise<GitHubUsername[]>;
|
|
39
|
+
getRepo(repo: RepoFullName): Promise<RepoDetail>;
|
|
40
|
+
private withRetry;
|
|
41
|
+
private getRetryDelay;
|
|
42
|
+
}
|
|
43
|
+
export declare function splitRepoFullName(repo: RepoFullName): {
|
|
44
|
+
owner: string;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Endpoints } from '@octokit/types';
|
|
2
|
+
import type { GitHubUsername, RepoFullName } from "./types.js";
|
|
3
|
+
export interface GitHubClientOptions {
|
|
4
|
+
token: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
}
|
|
8
|
+
export type PullRequestListItem = Endpoints['GET /repos/{owner}/{repo}/pulls']['response']['data'][number];
|
|
9
|
+
export type PullRequestDetail = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response']['data'];
|
|
10
|
+
export type PullRequestReview = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews']['response']['data'][number];
|
|
11
|
+
export type IssueComment = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data'][number];
|
|
12
|
+
export type IssueEvent = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/events']['response']['data'][number];
|
|
13
|
+
export type CommitListItem = Endpoints['GET /repos/{owner}/{repo}/commits']['response']['data'][number];
|
|
14
|
+
export type CommitDetail = Endpoints['GET /repos/{owner}/{repo}/commits/{ref}']['response']['data'];
|
|
15
|
+
export type RepoDetail = Endpoints['GET /repos/{owner}/{repo}']['response']['data'];
|
|
16
|
+
export type Contributor = Endpoints['GET /repos/{owner}/{repo}/contributors']['response']['data'][number];
|
|
17
|
+
export declare class GitHubClient {
|
|
18
|
+
private readonly octokit;
|
|
19
|
+
private readonly timeoutMs;
|
|
20
|
+
private readonly maxRetries;
|
|
21
|
+
constructor(options: GitHubClientOptions);
|
|
22
|
+
getAuthenticatedUsername(): Promise<GitHubUsername>;
|
|
23
|
+
userExists(username: GitHubUsername): Promise<boolean>;
|
|
24
|
+
listOrgMembers(org: string): Promise<GitHubUsername[]>;
|
|
25
|
+
listOrgRepos(org: string): Promise<RepoFullName[]>;
|
|
26
|
+
listUserRepos(username: string): Promise<RepoFullName[]>;
|
|
27
|
+
listPullRequests(repo: RepoFullName): Promise<PullRequestListItem[]>;
|
|
28
|
+
getPullRequest(repo: RepoFullName, number: number): Promise<PullRequestDetail>;
|
|
29
|
+
listPullRequestReviews(repo: RepoFullName, number: number): Promise<PullRequestReview[]>;
|
|
30
|
+
listIssueComments(repo: RepoFullName, number: number): Promise<IssueComment[]>;
|
|
31
|
+
listIssueEvents(repo: RepoFullName, number: number): Promise<IssueEvent[]>;
|
|
32
|
+
listCommits(repo: RepoFullName, options?: {
|
|
33
|
+
since?: string;
|
|
34
|
+
until?: string;
|
|
35
|
+
}): Promise<CommitListItem[]>;
|
|
36
|
+
getCommit(repo: RepoFullName, sha: string): Promise<CommitDetail>;
|
|
37
|
+
ensureRepoAccessible(repo: RepoFullName): Promise<void>;
|
|
38
|
+
listContributors(repo: RepoFullName): Promise<GitHubUsername[]>;
|
|
39
|
+
getRepo(repo: RepoFullName): Promise<RepoDetail>;
|
|
40
|
+
private withRetry;
|
|
41
|
+
private getRetryDelay;
|
|
42
|
+
}
|
|
43
|
+
export declare function splitRepoFullName(repo: RepoFullName): {
|
|
44
|
+
owner: string;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Octokit } from 'octokit';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
4
|
+
const DEFAULT_BASE_DELAY_MS = 500;
|
|
5
|
+
export class GitHubClient {
|
|
6
|
+
octokit;
|
|
7
|
+
timeoutMs;
|
|
8
|
+
maxRetries;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.octokit = new Octokit({ auth: options.token });
|
|
11
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
12
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
13
|
+
}
|
|
14
|
+
async getAuthenticatedUsername() {
|
|
15
|
+
const response = await this.withRetry((signal) => this.octokit.rest.users.getAuthenticated({ request: { signal } }));
|
|
16
|
+
return response.data.login;
|
|
17
|
+
}
|
|
18
|
+
async userExists(username) {
|
|
19
|
+
try {
|
|
20
|
+
await this.withRetry((signal) => this.octokit.rest.users.getByUsername({ username, request: { signal } }));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const status = getStatus(error);
|
|
25
|
+
if (status === 404) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async listOrgMembers(org) {
|
|
32
|
+
const members = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.orgs.listMembers, {
|
|
33
|
+
org,
|
|
34
|
+
per_page: 100,
|
|
35
|
+
request: { signal },
|
|
36
|
+
}));
|
|
37
|
+
return members.map((member) => member.login);
|
|
38
|
+
}
|
|
39
|
+
async listOrgRepos(org) {
|
|
40
|
+
const repos = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listForOrg, {
|
|
41
|
+
org,
|
|
42
|
+
per_page: 100,
|
|
43
|
+
type: 'all',
|
|
44
|
+
request: { signal },
|
|
45
|
+
}));
|
|
46
|
+
return repos.map((repo) => repo.full_name);
|
|
47
|
+
}
|
|
48
|
+
async listUserRepos(username) {
|
|
49
|
+
const repos = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listForUser, {
|
|
50
|
+
username,
|
|
51
|
+
per_page: 100,
|
|
52
|
+
type: 'all',
|
|
53
|
+
request: { signal },
|
|
54
|
+
}));
|
|
55
|
+
return repos.map((repo) => repo.full_name);
|
|
56
|
+
}
|
|
57
|
+
async listPullRequests(repo) {
|
|
58
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
59
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.pulls.list, {
|
|
60
|
+
owner,
|
|
61
|
+
repo: name,
|
|
62
|
+
per_page: 100,
|
|
63
|
+
state: 'all',
|
|
64
|
+
sort: 'updated',
|
|
65
|
+
direction: 'desc',
|
|
66
|
+
request: { signal },
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async getPullRequest(repo, number) {
|
|
70
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
71
|
+
const response = await this.withRetry((signal) => this.octokit.rest.pulls.get({
|
|
72
|
+
owner,
|
|
73
|
+
repo: name,
|
|
74
|
+
pull_number: number,
|
|
75
|
+
request: { signal },
|
|
76
|
+
}));
|
|
77
|
+
return response.data;
|
|
78
|
+
}
|
|
79
|
+
async listPullRequestReviews(repo, number) {
|
|
80
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
81
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.pulls.listReviews, {
|
|
82
|
+
owner,
|
|
83
|
+
repo: name,
|
|
84
|
+
pull_number: number,
|
|
85
|
+
per_page: 100,
|
|
86
|
+
request: { signal },
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
async listIssueComments(repo, number) {
|
|
90
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
91
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.issues.listComments, {
|
|
92
|
+
owner,
|
|
93
|
+
repo: name,
|
|
94
|
+
issue_number: number,
|
|
95
|
+
per_page: 100,
|
|
96
|
+
request: { signal },
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
async listIssueEvents(repo, number) {
|
|
100
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
101
|
+
return this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.issues.listEvents, {
|
|
102
|
+
owner,
|
|
103
|
+
repo: name,
|
|
104
|
+
issue_number: number,
|
|
105
|
+
per_page: 100,
|
|
106
|
+
request: { signal },
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
async listCommits(repo, options = {}) {
|
|
110
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
111
|
+
return this.withRetry((signal) => {
|
|
112
|
+
const params = {
|
|
113
|
+
owner,
|
|
114
|
+
repo: name,
|
|
115
|
+
per_page: 100,
|
|
116
|
+
request: { signal },
|
|
117
|
+
};
|
|
118
|
+
if (options.since) {
|
|
119
|
+
params.since = options.since;
|
|
120
|
+
}
|
|
121
|
+
if (options.until) {
|
|
122
|
+
params.until = options.until;
|
|
123
|
+
}
|
|
124
|
+
return this.octokit.paginate(this.octokit.rest.repos.listCommits, params);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async getCommit(repo, sha) {
|
|
128
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
129
|
+
const response = await this.withRetry((signal) => this.octokit.rest.repos.getCommit({
|
|
130
|
+
owner,
|
|
131
|
+
repo: name,
|
|
132
|
+
ref: sha,
|
|
133
|
+
request: { signal },
|
|
134
|
+
}));
|
|
135
|
+
return response.data;
|
|
136
|
+
}
|
|
137
|
+
async ensureRepoAccessible(repo) {
|
|
138
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
139
|
+
await this.withRetry((signal) => this.octokit.rest.repos.get({ owner, repo: name, request: { signal } }));
|
|
140
|
+
}
|
|
141
|
+
async listContributors(repo) {
|
|
142
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
143
|
+
const contributors = await this.withRetry((signal) => this.octokit.paginate(this.octokit.rest.repos.listContributors, {
|
|
144
|
+
owner,
|
|
145
|
+
repo: name,
|
|
146
|
+
per_page: 100,
|
|
147
|
+
request: { signal },
|
|
148
|
+
}));
|
|
149
|
+
return contributors
|
|
150
|
+
.map((contributor) => contributor.login)
|
|
151
|
+
.filter((login) => Boolean(login));
|
|
152
|
+
}
|
|
153
|
+
async getRepo(repo) {
|
|
154
|
+
const { owner, name } = splitRepoFullName(repo);
|
|
155
|
+
const response = await this.withRetry((signal) => this.octokit.rest.repos.get({ owner, repo: name, request: { signal } }));
|
|
156
|
+
return response.data;
|
|
157
|
+
}
|
|
158
|
+
async withRetry(fn) {
|
|
159
|
+
let attempt = 0;
|
|
160
|
+
let lastError;
|
|
161
|
+
while (attempt < this.maxRetries) {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
164
|
+
try {
|
|
165
|
+
const result = await fn(controller.signal);
|
|
166
|
+
clearTimeout(timeoutId);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
lastError = error;
|
|
172
|
+
const retryAfter = await this.getRetryDelay(error, attempt);
|
|
173
|
+
if (retryAfter === null) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
await delay(retryAfter);
|
|
177
|
+
attempt += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw wrapError(lastError, 'GitHub API request failed');
|
|
181
|
+
}
|
|
182
|
+
async getRetryDelay(error, attempt) {
|
|
183
|
+
const status = getStatus(error);
|
|
184
|
+
if (status === 403) {
|
|
185
|
+
const reset = getHeader(error, 'x-ratelimit-reset');
|
|
186
|
+
const remaining = getHeader(error, 'x-ratelimit-remaining');
|
|
187
|
+
if (remaining === '0' && reset) {
|
|
188
|
+
const resetMs = Number(reset) * 1000;
|
|
189
|
+
const wait = Math.max(resetMs - Date.now(), DEFAULT_BASE_DELAY_MS);
|
|
190
|
+
return Math.min(wait, 60_000);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (status && status < 500 && status !== 429) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const backoff = DEFAULT_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
197
|
+
return Math.min(backoff, 8_000);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function splitRepoFullName(repo) {
|
|
201
|
+
const [owner, name] = repo.split('/');
|
|
202
|
+
if (!owner || !name) {
|
|
203
|
+
throw new Error(`Invalid repo full name: ${repo}`);
|
|
204
|
+
}
|
|
205
|
+
return { owner, name };
|
|
206
|
+
}
|
|
207
|
+
function delay(ms) {
|
|
208
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
209
|
+
}
|
|
210
|
+
function getStatus(error) {
|
|
211
|
+
if (typeof error === 'object' && error && 'status' in error) {
|
|
212
|
+
const status = error.status;
|
|
213
|
+
return status;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
function getHeader(error, name) {
|
|
218
|
+
if (typeof error === 'object' && error && 'response' in error) {
|
|
219
|
+
const response = error.response;
|
|
220
|
+
return response?.headers?.[name];
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
function wrapError(error, message) {
|
|
225
|
+
if (error instanceof Error) {
|
|
226
|
+
const wrapped = new Error(`${message}: ${error.message}`);
|
|
227
|
+
// Preserve status for HTTP errors (e.g., Octokit RequestError)
|
|
228
|
+
if ('status' in error && typeof error.status === 'number') {
|
|
229
|
+
;
|
|
230
|
+
wrapped.status = error.status;
|
|
231
|
+
}
|
|
232
|
+
return wrapped;
|
|
233
|
+
}
|
|
234
|
+
return new Error(message);
|
|
235
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.VERSION = void 0;
|
|
18
|
+
exports.VERSION = '1.0.0';
|
|
19
|
+
__exportStar(require("./types.cjs"), exports);
|
|
20
|
+
__exportStar(require("./config.cjs"), exports);
|
|
21
|
+
__exportStar(require("./cache.cjs"), exports);
|
|
22
|
+
__exportStar(require("./github.cjs"), exports);
|
|
23
|
+
__exportStar(require("./time.cjs"), exports);
|
|
24
|
+
__exportStar(require("./sync.cjs"), exports);
|
|
25
|
+
__exportStar(require("./repos.cjs"), exports);
|
|
26
|
+
__exportStar(require("./team.cjs"), exports);
|
|
27
|
+
__exportStar(require("./reports/index.cjs"), exports);
|
|
28
|
+
__exportStar(require("./filters.cjs"), exports);
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const VERSION = "1.0.0";
|
|
2
|
+
export * from "./types.cjs";
|
|
3
|
+
export * from "./config.cjs";
|
|
4
|
+
export * from "./cache.cjs";
|
|
5
|
+
export * from "./github.cjs";
|
|
6
|
+
export * from "./time.cjs";
|
|
7
|
+
export * from "./sync.cjs";
|
|
8
|
+
export * from "./repos.cjs";
|
|
9
|
+
export * from "./team.cjs";
|
|
10
|
+
export * from "./reports/index.cjs";
|
|
11
|
+
export * from "./filters.cjs";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const VERSION = "1.0.0";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./cache.js";
|
|
5
|
+
export * from "./github.js";
|
|
6
|
+
export * from "./time.js";
|
|
7
|
+
export * from "./sync.js";
|
|
8
|
+
export * from "./repos.js";
|
|
9
|
+
export * from "./team.js";
|
|
10
|
+
export * from "./reports/index.js";
|
|
11
|
+
export * from "./filters.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const VERSION = '1.0.0';
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./cache.js";
|
|
5
|
+
export * from "./github.js";
|
|
6
|
+
export * from "./time.js";
|
|
7
|
+
export * from "./sync.js";
|
|
8
|
+
export * from "./repos.js";
|
|
9
|
+
export * from "./team.js";
|
|
10
|
+
export * from "./reports/index.js";
|
|
11
|
+
export * from "./filters.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadReportData = loadReportData;
|
|
4
|
+
function loadReportData(cache, repos) {
|
|
5
|
+
const pullRequests = repos.flatMap((repo) => cache.getPullRequests({ repo }));
|
|
6
|
+
const commits = repos.flatMap((repo) => cache.getCommits({ repo }));
|
|
7
|
+
return { pullRequests, commits };
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Cache } from "../cache.cjs";
|
|
2
|
+
import type { Commit, PullRequest, RepoFullName } from "../types.cjs";
|
|
3
|
+
export interface ReportContextData {
|
|
4
|
+
pullRequests: PullRequest[];
|
|
5
|
+
commits: Commit[];
|
|
6
|
+
}
|
|
7
|
+
export declare function loadReportData(cache: Cache, repos: RepoFullName[]): ReportContextData;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Cache } from "../cache.js";
|
|
2
|
+
import type { Commit, PullRequest, RepoFullName } from "../types.js";
|
|
3
|
+
export interface ReportContextData {
|
|
4
|
+
pullRequests: PullRequest[];
|
|
5
|
+
commits: Commit[];
|
|
6
|
+
}
|
|
7
|
+
export declare function loadReportData(cache: Cache, repos: RepoFullName[]): ReportContextData;
|