@codraoss/provider-github 0.9.4

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/index.js ADDED
@@ -0,0 +1,653 @@
1
+ import {
2
+ exchangeGitHubOAuthCode,
3
+ fetchGitHubOAuthProfile,
4
+ toDashboardSessionUser
5
+ } from "./chunk-OACX7HPZ.js";
6
+ import {
7
+ normalizeGitHubWebhook
8
+ } from "./chunk-JIVK2ZFL.js";
9
+
10
+ // src/client.ts
11
+ import { withTimeout as withTimeout2 } from "@codraoss/core/timeout";
12
+
13
+ // src/http.ts
14
+ import { logger } from "@codraoss/core/logger";
15
+ var GitHubError = class extends Error {
16
+ constructor(status, body, path, message) {
17
+ super(message);
18
+ this.status = status;
19
+ this.body = body;
20
+ this.path = path;
21
+ this.name = "GitHubError";
22
+ }
23
+ status;
24
+ body;
25
+ path;
26
+ };
27
+ async function assertResponseOk(response, path, action) {
28
+ if (!response.ok) {
29
+ let errText;
30
+ try {
31
+ errText = await response.text();
32
+ } catch {
33
+ errText = "<unreadable>";
34
+ }
35
+ throw new GitHubError(response.status, errText, path, `${action} failed with ${response.status}: ${errText}`);
36
+ }
37
+ }
38
+ function isDiffTooLargeError(error) {
39
+ return error instanceof GitHubError && error.status === 406 && /too_large|maximum number of lines/i.test(error.body ?? "");
40
+ }
41
+ async function withRetry(operation, fn, maxRetries = 2) {
42
+ let attempt = 0;
43
+ while (true) {
44
+ try {
45
+ return await fn();
46
+ } catch (error) {
47
+ attempt++;
48
+ const isSecondaryRateLimit = error instanceof GitHubError && error.status === 403 && error.body?.toLowerCase().includes("secondary rate limit");
49
+ const isRetryable = isSecondaryRateLimit || error instanceof GitHubError && (error.status === 429 || error.status >= 500) || error.name === "TimeoutError" || error.message.includes("timeout");
50
+ if (!isRetryable || attempt > maxRetries) {
51
+ throw error;
52
+ }
53
+ const delay = isSecondaryRateLimit ? Math.pow(2, attempt) * 3e4 : Math.pow(2, attempt) * 1e3;
54
+ logger.warn(`Retrying GitHub operation ${operation} (attempt ${attempt}/${maxRetries}) in ${delay}ms`, {
55
+ status: error instanceof GitHubError ? error.status : void 0,
56
+ error: error.message
57
+ });
58
+ await new Promise((resolve) => setTimeout(resolve, delay));
59
+ }
60
+ }
61
+ }
62
+ function installationCacheKey(installationId) {
63
+ return `install:${installationId}`;
64
+ }
65
+ function encodeGitHubContentPath(path) {
66
+ return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
67
+ }
68
+ function repoApiPath(owner, repo) {
69
+ return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
70
+ }
71
+
72
+ // src/constants.ts
73
+ var GITHUB_TIMEOUT_MS = 3e4;
74
+ var GITHUB_APP_INSTALL_URL_CACHE_KEY = "github:app_installation_url";
75
+ var GITHUB_REPOSITORIES_PER_PAGE = 100;
76
+ var GITHUB_REPOSITORY_PAGE_LIMIT = 100;
77
+ var DIFF_FILES_PER_PAGE = 100;
78
+ var MAX_DIFF_FILE_PAGES = 5;
79
+
80
+ // src/app-auth.ts
81
+ import { withTimeout } from "@codraoss/core/timeout";
82
+ function pemToArrayBuffer(pem) {
83
+ const base64 = pem.replace(/-----BEGIN (RSA )?PRIVATE KEY-----/g, "").replace(/-----END (RSA )?PRIVATE KEY-----/g, "").replace(/\\n/g, "").replace(/\s+/g, "");
84
+ const binary = atob(base64);
85
+ const bytes = new Uint8Array(binary.length);
86
+ for (let index = 0; index < binary.length; index += 1) {
87
+ bytes[index] = binary.charCodeAt(index);
88
+ }
89
+ return bytes.buffer;
90
+ }
91
+ function base64UrlEncode(input) {
92
+ return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
93
+ }
94
+ async function createGitHubJwt(appId, privateKeyPem) {
95
+ const now = Math.floor(Date.now() / 1e3);
96
+ const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
97
+ const payload = base64UrlEncode(
98
+ JSON.stringify({
99
+ iat: now - 60,
100
+ exp: now + 9 * 60,
101
+ iss: appId
102
+ })
103
+ );
104
+ const key = await crypto.subtle.importKey(
105
+ "pkcs8",
106
+ pemToArrayBuffer(privateKeyPem),
107
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
108
+ false,
109
+ ["sign"]
110
+ );
111
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(`${header}.${payload}`));
112
+ const signatureString = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
113
+ return `${header}.${payload}.${signatureString}`;
114
+ }
115
+ async function appJwtHeaders(env) {
116
+ const jwt = await createGitHubJwt(env.GITHUB_APP_ID, env.APP_PRIVATE_KEY);
117
+ return {
118
+ Accept: "application/vnd.github+json",
119
+ Authorization: `Bearer ${jwt}`,
120
+ "X-GitHub-Api-Version": "2022-11-28",
121
+ "User-Agent": env.BOT_USERNAME ?? "codra-bot"
122
+ };
123
+ }
124
+ function normalizeGitHubAppSlug(slug) {
125
+ const normalized = slug?.trim().replace(/\[bot\]$/i, "");
126
+ return normalized || null;
127
+ }
128
+ function installUrlFromSlug(slug) {
129
+ return `https://github.com/apps/${encodeURIComponent(slug)}/installations/new`;
130
+ }
131
+ async function readCachedInstallationToken(env, installationId, tracker) {
132
+ if (tracker) tracker.incrementSubrequests(1);
133
+ const cached = await env.APP_KV.get(installationCacheKey(installationId), "json");
134
+ return cached;
135
+ }
136
+ async function writeCachedInstallationToken(env, installationId, record, tracker) {
137
+ const expiresAt = new Date(record.expiresAt).getTime();
138
+ const ttl = Math.max(60, Math.floor((expiresAt - Date.now()) / 1e3) - 300);
139
+ if (tracker) tracker.incrementSubrequests(1);
140
+ await env.APP_KV.put(installationCacheKey(installationId), JSON.stringify(record), { expirationTtl: ttl });
141
+ }
142
+ async function fetchInstallationToken(env, installationId) {
143
+ const headers = await appJwtHeaders(env);
144
+ const response = await withTimeout(
145
+ "GitHub installation token",
146
+ GITHUB_TIMEOUT_MS,
147
+ (signal) => fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
148
+ method: "POST",
149
+ signal,
150
+ headers
151
+ })
152
+ );
153
+ await assertResponseOk(response, "/app/installations/.../access_tokens", "GitHub installation token request");
154
+ const data = await response.json();
155
+ return { token: data.token, expiresAt: data.expires_at };
156
+ }
157
+ async function fetchInstallations(env) {
158
+ return withRetry("listInstallations", async () => {
159
+ const headers = await appJwtHeaders(env);
160
+ const response = await withTimeout(
161
+ "GitHub list installations",
162
+ GITHUB_TIMEOUT_MS,
163
+ (signal) => fetch("https://api.github.com/app/installations", { signal, headers })
164
+ );
165
+ await assertResponseOk(response, "/app/installations", "GitHub list installations");
166
+ return await response.json();
167
+ });
168
+ }
169
+ async function fetchAppInstallationUrl(env) {
170
+ const configuredSlug = normalizeGitHubAppSlug(env.GITHUB_APP_SLUG);
171
+ if (configuredSlug) {
172
+ return installUrlFromSlug(configuredSlug);
173
+ }
174
+ const cached = await env.APP_KV.get(GITHUB_APP_INSTALL_URL_CACHE_KEY);
175
+ if (cached) {
176
+ return cached;
177
+ }
178
+ return withRetry("getAppInstallationUrl", async () => {
179
+ const headers = await appJwtHeaders(env);
180
+ const response = await withTimeout(
181
+ "GitHub app lookup",
182
+ GITHUB_TIMEOUT_MS,
183
+ (signal) => fetch("https://api.github.com/app", { signal, headers })
184
+ );
185
+ await assertResponseOk(response, "/app", "GitHub app lookup");
186
+ const app = await response.json();
187
+ const fallbackSlug = normalizeGitHubAppSlug(app.slug);
188
+ const installUrl = app.html_url ? `${app.html_url.replace(/\/$/, "")}/installations/new` : fallbackSlug ? installUrlFromSlug(fallbackSlug) : null;
189
+ if (!installUrl) {
190
+ throw new Error("GitHub app lookup did not return a usable app URL.");
191
+ }
192
+ await env.APP_KV.put(GITHUB_APP_INSTALL_URL_CACHE_KEY, installUrl, { expirationTtl: 60 * 60 * 24 });
193
+ return installUrl;
194
+ });
195
+ }
196
+
197
+ // src/diff-fetch.ts
198
+ import { logger as logger2 } from "@codraoss/core/logger";
199
+ import { buildUnifiedDiffFromFiles } from "@codraoss/core/diff";
200
+ async function fetchPullRequestDiffFromFiles(ctx, owner, repo, pullNumber) {
201
+ const files = [];
202
+ for (let page = 1; page <= MAX_DIFF_FILE_PAGES; page++) {
203
+ const pageFiles = await withRetry(`getPullRequestFiles ${owner}/${repo}#${pullNumber} p${page}`, async () => {
204
+ const response = await ctx.requestAndCheck(
205
+ `${repoApiPath(owner, repo)}/pulls/${pullNumber}/files?per_page=${DIFF_FILES_PER_PAGE}&page=${page}`
206
+ );
207
+ return await response.json();
208
+ });
209
+ files.push(...pageFiles);
210
+ if (pageFiles.length < DIFF_FILES_PER_PAGE) break;
211
+ if (page === MAX_DIFF_FILE_PAGES) {
212
+ logger2.warn(
213
+ `Stopped rebuilding the diff for ${owner}/${repo}#${pullNumber} at ${files.length} files; later files are not reviewed`
214
+ );
215
+ }
216
+ }
217
+ return buildUnifiedDiffFromFiles(files);
218
+ }
219
+ async function fetchPullRequestDiff(ctx, owner, repo, pullNumber) {
220
+ try {
221
+ return await withRetry(`getPullRequestDiff ${owner}/${repo}#${pullNumber}`, async () => {
222
+ const response = await ctx.requestAndCheck(
223
+ `${repoApiPath(owner, repo)}/pulls/${pullNumber}`,
224
+ {},
225
+ "application/vnd.github.v3.diff"
226
+ );
227
+ return response.text();
228
+ });
229
+ } catch (error) {
230
+ if (!isDiffTooLargeError(error)) throw error;
231
+ logger2.warn(
232
+ `Diff for ${owner}/${repo}#${pullNumber} exceeds GitHub's 20,000-line media-type cap; rebuilding it from the per-file endpoint`
233
+ );
234
+ return fetchPullRequestDiffFromFiles(ctx, owner, repo, pullNumber);
235
+ }
236
+ }
237
+ async function fetchCompareDiff(ctx, owner, repo, base, head) {
238
+ const comparePath = `${repoApiPath(owner, repo)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
239
+ try {
240
+ return await withRetry(`getCompareDiff ${owner}/${repo} ${base}...${head}`, async () => {
241
+ const response = await ctx.requestAndCheck(comparePath, {}, "application/vnd.github.v3.diff");
242
+ return response.text();
243
+ });
244
+ } catch (error) {
245
+ if (!isDiffTooLargeError(error)) throw error;
246
+ logger2.warn(`Compare diff ${owner}/${repo} ${base}...${head} is over the line cap; rebuilding from the JSON file list`);
247
+ return withRetry(`getCompareFiles ${owner}/${repo} ${base}...${head}`, async () => {
248
+ const response = await ctx.requestAndCheck(comparePath);
249
+ const payload = await response.json();
250
+ return buildUnifiedDiffFromFiles(payload.files ?? []);
251
+ });
252
+ }
253
+ }
254
+
255
+ // src/review-post.ts
256
+ import { logger as logger3 } from "@codraoss/core/logger";
257
+ async function postReview(ctx, owner, repo, pullNumber, input) {
258
+ return withRetry(`createReview ${owner}/${repo}#${pullNumber}`, async () => {
259
+ const mapped = input.comments.map((comment) => {
260
+ if (typeof comment.line === "number" && comment.line > 0) {
261
+ return {
262
+ path: comment.path,
263
+ line: comment.line,
264
+ side: comment.side ?? "RIGHT",
265
+ body: comment.body
266
+ };
267
+ }
268
+ if (typeof comment.position === "number" && comment.position > 0) {
269
+ return { path: comment.path, position: comment.position, body: comment.body };
270
+ }
271
+ return null;
272
+ });
273
+ let postedIndices = mapped.flatMap((c, index) => c === null ? [] : [index]);
274
+ const comments = mapped.filter((c) => c !== null);
275
+ const unaddressable = mapped.length - comments.length;
276
+ if (unaddressable > 0) {
277
+ logger3.warn("Dropping review comments with no usable line/position", {
278
+ owner,
279
+ repo,
280
+ pullNumber,
281
+ unaddressable,
282
+ total: mapped.length
283
+ });
284
+ }
285
+ const body = {
286
+ commit_id: input.commitSha,
287
+ event: input.event,
288
+ body: input.body,
289
+ comments
290
+ };
291
+ const reviewPath = `${repoApiPath(owner, repo)}/pulls/${pullNumber}/reviews`;
292
+ let response = await ctx.request(reviewPath, {
293
+ method: "POST",
294
+ headers: {
295
+ "content-type": "application/json"
296
+ },
297
+ body: JSON.stringify(body)
298
+ });
299
+ if (response.status === 422 && body.comments.length > 0) {
300
+ const reason = await response.clone().text().catch(() => "<unreadable>");
301
+ logger3.warn(`GitHub review creation failed with 422, retrying without inline comments`, {
302
+ owner,
303
+ repo,
304
+ pullNumber,
305
+ droppedComments: body.comments.length,
306
+ reason: reason.slice(0, 500)
307
+ });
308
+ response = await ctx.request(reviewPath, {
309
+ method: "POST",
310
+ headers: {
311
+ "content-type": "application/json"
312
+ },
313
+ body: JSON.stringify({
314
+ commit_id: input.commitSha,
315
+ event: input.event,
316
+ body: input.body,
317
+ comments: []
318
+ })
319
+ });
320
+ postedIndices = [];
321
+ }
322
+ await assertResponseOk(response, reviewPath, "GitHub review creation");
323
+ const review = await response.json();
324
+ return { id: review.id, postedIndices };
325
+ });
326
+ }
327
+ async function findBotReviewForCommit(ctx, owner, repo, pullNumber, commitSha, botLogin) {
328
+ return withRetry(`findBotReviewForCommit ${owner}/${repo}#${pullNumber}`, async () => {
329
+ const response = await ctx.requestAndCheck(
330
+ `${repoApiPath(owner, repo)}/pulls/${pullNumber}/reviews?per_page=100`
331
+ );
332
+ const reviews = await response.json();
333
+ const login = botLogin.toLowerCase();
334
+ const match = reviews.find(
335
+ (review) => review.commit_id === commitSha && (review.user?.login ?? "").toLowerCase().startsWith(login)
336
+ );
337
+ return match ? { id: match.id } : null;
338
+ });
339
+ }
340
+
341
+ // src/labels.ts
342
+ async function ensureLabel(ctx, owner, repo, name, color) {
343
+ return withRetry(`ensureLabel ${owner}/${repo} ${name}`, async () => {
344
+ const listResponse = await ctx.request(`${repoApiPath(owner, repo)}/labels/${encodeURIComponent(name)}`);
345
+ if (listResponse.ok) return;
346
+ if (listResponse.status !== 404) {
347
+ await assertResponseOk(listResponse, name, "GitHub label lookup");
348
+ }
349
+ const createResponse = await ctx.request(`${repoApiPath(owner, repo)}/labels`, {
350
+ method: "POST",
351
+ headers: {
352
+ "content-type": "application/json"
353
+ },
354
+ body: JSON.stringify({ name, color })
355
+ });
356
+ if (!createResponse.ok && createResponse.status !== 422) {
357
+ await assertResponseOk(createResponse, name, "GitHub label creation");
358
+ }
359
+ });
360
+ }
361
+ async function addIssueLabels(ctx, owner, repo, issueNumber, labels) {
362
+ return withRetry(`addIssueLabels ${owner}/${repo}#${issueNumber}`, async () => {
363
+ await ctx.requestAndCheck(`${repoApiPath(owner, repo)}/issues/${issueNumber}/labels`, {
364
+ method: "POST",
365
+ headers: {
366
+ "content-type": "application/json"
367
+ },
368
+ body: JSON.stringify({ labels })
369
+ });
370
+ });
371
+ }
372
+ async function listIssueLabels(ctx, owner, repo, issueNumber) {
373
+ return withRetry(`listIssueLabels ${owner}/${repo}#${issueNumber}`, async () => {
374
+ const response = await ctx.requestAndCheck(`${repoApiPath(owner, repo)}/issues/${issueNumber}/labels?per_page=100`);
375
+ const labels = await response.json();
376
+ if (!Array.isArray(labels)) {
377
+ throw new Error("Expected an array of labels from GitHub API.");
378
+ }
379
+ return labels.map((label) => label.name).filter((name) => typeof name === "string" && name.length > 0);
380
+ });
381
+ }
382
+ async function removeIssueLabel(ctx, owner, repo, issueNumber, label) {
383
+ return withRetry(`removeIssueLabel ${owner}/${repo}#${issueNumber} ${label}`, async () => {
384
+ const response = await ctx.request(
385
+ `${repoApiPath(owner, repo)}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`,
386
+ {
387
+ method: "DELETE"
388
+ }
389
+ );
390
+ if (!response.ok && response.status !== 404) {
391
+ await assertResponseOk(response, label, "GitHub label removal");
392
+ }
393
+ });
394
+ }
395
+
396
+ // src/client.ts
397
+ var GitHubClient = class {
398
+ constructor(env, installationId, tracker) {
399
+ this.env = env;
400
+ this.installationId = installationId;
401
+ this.tracker = tracker;
402
+ }
403
+ env;
404
+ installationId;
405
+ tracker;
406
+ // Scoped per invocation to avoid KV hits & 50-subrequest cap.
407
+ memoToken = null;
408
+ async getInstallationToken() {
409
+ if (this.memoToken?.token && new Date(this.memoToken.expiresAt).getTime() > Date.now() + 6e4) {
410
+ return this.memoToken.token;
411
+ }
412
+ const cached = await readCachedInstallationToken(this.env, this.installationId, this.tracker);
413
+ if (cached?.token) {
414
+ this.memoToken = cached;
415
+ return cached.token;
416
+ }
417
+ return withRetry("getInstallationToken", async () => {
418
+ const record = await fetchInstallationToken(this.env, this.installationId);
419
+ await writeCachedInstallationToken(this.env, this.installationId, record, this.tracker);
420
+ this.memoToken = record;
421
+ return record.token;
422
+ });
423
+ }
424
+ static async listInstallations(env) {
425
+ return fetchInstallations(env);
426
+ }
427
+ static async getAppInstallationUrl(env) {
428
+ return fetchAppInstallationUrl(env);
429
+ }
430
+ async listRepositories() {
431
+ return withRetry("listRepositories", async () => {
432
+ const repositories = [];
433
+ for (let page = 1; page <= GITHUB_REPOSITORY_PAGE_LIMIT; page += 1) {
434
+ const response = await this.requestAndCheck(`/installation/repositories?per_page=${GITHUB_REPOSITORIES_PER_PAGE}&page=${page}`);
435
+ const data = await response.json();
436
+ repositories.push(...data.repositories);
437
+ if (data.repositories.length < GITHUB_REPOSITORIES_PER_PAGE) {
438
+ return repositories;
439
+ }
440
+ }
441
+ throw new Error(
442
+ `GitHub repository listing exceeded ${GITHUB_REPOSITORY_PAGE_LIMIT} pages without a terminating page.`
443
+ );
444
+ });
445
+ }
446
+ async request(path, init = {}, accept = "application/vnd.github+json") {
447
+ const token = await this.getInstallationToken();
448
+ if (this.tracker) this.tracker.incrementSubrequests(1);
449
+ return withTimeout2(
450
+ `GitHub ${init.method ?? "GET"} ${path}`,
451
+ GITHUB_TIMEOUT_MS,
452
+ (signal) => fetch(`https://api.github.com${path}`, {
453
+ ...init,
454
+ signal,
455
+ headers: {
456
+ Accept: accept,
457
+ Authorization: `Bearer ${token}`,
458
+ "X-GitHub-Api-Version": "2022-11-28",
459
+ "User-Agent": this.env.BOT_USERNAME ?? "codra-bot",
460
+ ...init.headers
461
+ }
462
+ })
463
+ );
464
+ }
465
+ async requestAndCheck(path, init = {}, accept = "application/vnd.github+json") {
466
+ const response = await this.request(path, init, accept);
467
+ await assertResponseOk(response, path, `GitHub API ${init.method ?? "GET"} ${path}`);
468
+ return response;
469
+ }
470
+ // Hands the extracted helpers the authenticated-request surface.
471
+ ctx() {
472
+ return {
473
+ request: (path, init, accept) => this.request(path, init, accept),
474
+ requestAndCheck: (path, init, accept) => this.requestAndCheck(path, init, accept)
475
+ };
476
+ }
477
+ async graphql(query, variables) {
478
+ return withRetry("graphql", async () => {
479
+ const token = await this.getInstallationToken();
480
+ if (this.tracker) this.tracker.incrementSubrequests(1);
481
+ const response = await withTimeout2(
482
+ "GitHub GraphQL",
483
+ GITHUB_TIMEOUT_MS,
484
+ (signal) => fetch("https://api.github.com/graphql", {
485
+ method: "POST",
486
+ signal,
487
+ headers: {
488
+ Authorization: `Bearer ${token}`,
489
+ "User-Agent": this.env.BOT_USERNAME ?? "codra-bot",
490
+ "content-type": "application/json"
491
+ },
492
+ body: JSON.stringify({ query, variables })
493
+ })
494
+ );
495
+ await assertResponseOk(response, "/graphql", "GitHub GraphQL request");
496
+ const payload = await response.json();
497
+ if (payload.errors?.length) {
498
+ throw new GitHubError(
499
+ 422,
500
+ JSON.stringify(payload.errors),
501
+ "/graphql",
502
+ `GitHub GraphQL error: ${payload.errors[0].message}`
503
+ );
504
+ }
505
+ return payload.data;
506
+ });
507
+ }
508
+ async getPullRequest(owner, repo, pullNumber) {
509
+ return withRetry(`getPullRequest ${owner}/${repo}#${pullNumber}`, async () => {
510
+ const response = await this.requestAndCheck(`${repoApiPath(owner, repo)}/pulls/${pullNumber}`);
511
+ return await response.json();
512
+ });
513
+ }
514
+ async getPullRequestDiff(owner, repo, pullNumber) {
515
+ return fetchPullRequestDiff(this.ctx(), owner, repo, pullNumber);
516
+ }
517
+ async getCompareDiff(owner, repo, base, head) {
518
+ return fetchCompareDiff(this.ctx(), owner, repo, base, head);
519
+ }
520
+ async getRepoFileOrNull(owner, repo, path) {
521
+ return withRetry(`getRepoFileOrNull ${owner}/${repo}/${path}`, async () => {
522
+ const response = await this.request(`${repoApiPath(owner, repo)}/contents/${encodeGitHubContentPath(path)}`);
523
+ if (response.status === 404) return null;
524
+ await assertResponseOk(response, path, "GitHub repo file fetch");
525
+ const data = await response.json();
526
+ if (!data.content) {
527
+ return null;
528
+ }
529
+ return data.encoding === "base64" ? atob(data.content.replace(/\n/g, "")) : data.content;
530
+ });
531
+ }
532
+ async createCheckRun(owner, repo, input) {
533
+ return withRetry(`createCheckRun ${owner}/${repo}`, async () => {
534
+ const response = await this.requestAndCheck(`${repoApiPath(owner, repo)}/check-runs`, {
535
+ method: "POST",
536
+ headers: {
537
+ "content-type": "application/json"
538
+ },
539
+ body: JSON.stringify({
540
+ name: "Codra",
541
+ head_sha: input.headSha,
542
+ status: "in_progress",
543
+ details_url: input.detailsUrl,
544
+ output: {
545
+ title: input.title,
546
+ summary: input.summary
547
+ }
548
+ })
549
+ });
550
+ return await response.json();
551
+ });
552
+ }
553
+ async updateCheckRun(owner, repo, checkRunId, input) {
554
+ return withRetry(`updateCheckRun ${owner}/${repo} ${checkRunId}`, async () => {
555
+ await this.requestAndCheck(`${repoApiPath(owner, repo)}/check-runs/${checkRunId}`, {
556
+ method: "PATCH",
557
+ headers: {
558
+ "content-type": "application/json"
559
+ },
560
+ body: JSON.stringify({
561
+ status: input.status ?? "in_progress",
562
+ conclusion: input.conclusion,
563
+ completed_at: input.status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : void 0,
564
+ output: {
565
+ title: input.title,
566
+ summary: input.summary
567
+ }
568
+ })
569
+ });
570
+ });
571
+ }
572
+ async createReview(owner, repo, pullNumber, input) {
573
+ return postReview(this.ctx(), owner, repo, pullNumber, input);
574
+ }
575
+ async findBotReviewForCommit(owner, repo, pullNumber, commitSha, botLogin) {
576
+ return findBotReviewForCommit(this.ctx(), owner, repo, pullNumber, commitSha, botLogin);
577
+ }
578
+ async ensureLabel(owner, repo, name, color) {
579
+ return ensureLabel(this.ctx(), owner, repo, name, color);
580
+ }
581
+ async addIssueLabels(owner, repo, issueNumber, labels) {
582
+ return addIssueLabels(this.ctx(), owner, repo, issueNumber, labels);
583
+ }
584
+ async listIssueLabels(owner, repo, issueNumber) {
585
+ return listIssueLabels(this.ctx(), owner, repo, issueNumber);
586
+ }
587
+ // Case-insensitive removal using stored casing.
588
+ async removeIssueLabelsIfPresent(owner, repo, issueNumber, labels) {
589
+ const currentLabels = await this.listIssueLabels(owner, repo, issueNumber);
590
+ const currentByLowerName = new Map(currentLabels.map((label) => [label.toLowerCase(), label]));
591
+ const uniqueLabels = Array.from(new Set(labels.map((label) => label.toLowerCase())));
592
+ for (const label of uniqueLabels) {
593
+ const currentLabel = currentByLowerName.get(label);
594
+ if (currentLabel) {
595
+ await this.removeIssueLabel(owner, repo, issueNumber, currentLabel);
596
+ }
597
+ }
598
+ }
599
+ async removeIssueLabel(owner, repo, issueNumber, label) {
600
+ return removeIssueLabel(this.ctx(), owner, repo, issueNumber, label);
601
+ }
602
+ };
603
+
604
+ // src/service.ts
605
+ var GitHubService = class {
606
+ client;
607
+ constructor(env, installationId, tracker) {
608
+ this.client = new GitHubClient(env, installationId, tracker);
609
+ }
610
+ async getPullRequest(owner, repo, prNumber) {
611
+ return this.client.getPullRequest(owner, repo, prNumber);
612
+ }
613
+ async getPullRequestDiff(owner, repo, prNumber) {
614
+ return this.client.getPullRequestDiff(owner, repo, prNumber);
615
+ }
616
+ async getCompareDiff(owner, repo, base, head) {
617
+ return this.client.getCompareDiff(owner, repo, base, head);
618
+ }
619
+ async createCheckRun(owner, repo, params) {
620
+ return this.client.createCheckRun(owner, repo, params);
621
+ }
622
+ async updateCheckRun(owner, repo, checkRunId, params) {
623
+ return this.client.updateCheckRun(owner, repo, checkRunId, params);
624
+ }
625
+ async createReview(owner, repo, prNumber, params) {
626
+ return this.client.createReview(owner, repo, prNumber, params);
627
+ }
628
+ async findBotReviewForCommit(owner, repo, prNumber, commitSha, botLogin) {
629
+ return this.client.findBotReviewForCommit(owner, repo, prNumber, commitSha, botLogin);
630
+ }
631
+ async ensureLabel(owner, repo, name, color) {
632
+ return this.client.ensureLabel(owner, repo, name, color);
633
+ }
634
+ async addIssueLabels(owner, repo, prNumber, labels) {
635
+ return this.client.addIssueLabels(owner, repo, prNumber, labels);
636
+ }
637
+ async removeIssueLabelsIfPresent(owner, repo, prNumber, labels) {
638
+ return this.client.removeIssueLabelsIfPresent(owner, repo, prNumber, labels);
639
+ }
640
+ async removeIssueLabel(owner, repo, prNumber, label) {
641
+ return this.client.removeIssueLabel(owner, repo, prNumber, label);
642
+ }
643
+ };
644
+ export {
645
+ GitHubClient,
646
+ GitHubError,
647
+ GitHubService,
648
+ exchangeGitHubOAuthCode,
649
+ fetchGitHubOAuthProfile,
650
+ normalizeGitHubWebhook,
651
+ toDashboardSessionUser
652
+ };
653
+ //# sourceMappingURL=index.js.map