forges 0.0.0 → 0.1.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.
Files changed (152) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +72 -0
  3. package/dist/azure-devops/index.d.mts +2 -0
  4. package/dist/azure-devops/index.mjs +1 -0
  5. package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
  6. package/dist/bitbucket/index.d.mts +2 -0
  7. package/dist/bitbucket/index.mjs +1 -0
  8. package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
  9. package/dist/crypto-CQNEyYHD.mjs +156 -0
  10. package/dist/cursor-origin/index.d.mts +2 -0
  11. package/dist/cursor-origin/index.mjs +1 -0
  12. package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
  13. package/dist/define-B2_4ob2i.d.mts +307 -0
  14. package/dist/define-D3yEXtKK.mjs +2276 -0
  15. package/dist/env.d.mts +46 -0
  16. package/dist/env.mjs +283 -0
  17. package/dist/fake/index.d.mts +227 -0
  18. package/dist/fake/index.mjs +1001 -0
  19. package/dist/fetch-D4pSRvvA.d.mts +1723 -0
  20. package/dist/forgejo/index.d.mts +2 -0
  21. package/dist/forgejo/index.mjs +1 -0
  22. package/dist/forgejo-BCefbHUN.mjs +2005 -0
  23. package/dist/gitea/index.d.mts +2 -0
  24. package/dist/gitea/index.mjs +22 -0
  25. package/dist/gitee/index.d.mts +2 -0
  26. package/dist/gitee/index.mjs +1 -0
  27. package/dist/gitee-CA-f7Rs8.mjs +1272 -0
  28. package/dist/github/index.d.mts +2 -0
  29. package/dist/github/index.mjs +1 -0
  30. package/dist/github-B0umL8_f.mjs +3259 -0
  31. package/dist/gitlab/index.d.mts +2 -0
  32. package/dist/gitlab/index.mjs +1 -0
  33. package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
  34. package/dist/graphql-D7uGzZTf.mjs +366 -0
  35. package/dist/index-BQ6VS31s.d.mts +32 -0
  36. package/dist/index-C-jz9Mo3.d.mts +16 -0
  37. package/dist/index-C4fYzmK3.d.mts +102 -0
  38. package/dist/index-CQiIX4SH.d.mts +27 -0
  39. package/dist/index-CTj5gQzl.d.mts +35 -0
  40. package/dist/index-CliBWzOF.d.mts +23 -0
  41. package/dist/index-Cz_y-t3d.d.mts +36 -0
  42. package/dist/index-DEt25E1w.d.mts +77 -0
  43. package/dist/index-DIuS0EOy.d.mts +43 -0
  44. package/dist/index-uSpsMCrC.d.mts +26 -0
  45. package/dist/index.d.mts +41 -0
  46. package/dist/index.mjs +13 -0
  47. package/dist/kit.d.mts +183 -0
  48. package/dist/kit.mjs +4 -0
  49. package/dist/provider-CoZ7cCxp.d.mts +1439 -0
  50. package/dist/provider-Ed1YhkQN.mjs +169 -0
  51. package/dist/pushin/index.d.mts +2 -0
  52. package/dist/pushin/index.mjs +1 -0
  53. package/dist/pushin-CkxgJ2cU.mjs +343 -0
  54. package/dist/schema/Actor.json +96 -0
  55. package/dist/schema/AlertSupport.json +49 -0
  56. package/dist/schema/AuthKind.json +15 -0
  57. package/dist/schema/Branch.json +31 -0
  58. package/dist/schema/ChangedFile.json +58 -0
  59. package/dist/schema/Check.json +169 -0
  60. package/dist/schema/CheckRef.json +114 -0
  61. package/dist/schema/CheckState.json +14 -0
  62. package/dist/schema/ChecksSummary.json +34 -0
  63. package/dist/schema/CiJob.json +186 -0
  64. package/dist/schema/CiJobRef.json +132 -0
  65. package/dist/schema/CiRun.json +234 -0
  66. package/dist/schema/CiRunRef.json +103 -0
  67. package/dist/schema/CodeFragment.json +19 -0
  68. package/dist/schema/CodeMatch.json +128 -0
  69. package/dist/schema/Collaborator.json +169 -0
  70. package/dist/schema/Comment.json +336 -0
  71. package/dist/schema/CommentRef.json +158 -0
  72. package/dist/schema/Commit.json +307 -0
  73. package/dist/schema/CommitRef.json +104 -0
  74. package/dist/schema/CommitSignature.json +118 -0
  75. package/dist/schema/Comparison.json +355 -0
  76. package/dist/schema/Cursor.json +21 -0
  77. package/dist/schema/EventAction.json +33 -0
  78. package/dist/schema/EventDetail.json +649 -0
  79. package/dist/schema/EventKind.json +26 -0
  80. package/dist/schema/FileContent.json +73 -0
  81. package/dist/schema/FileStatus.json +15 -0
  82. package/dist/schema/ForgeCapabilities.json +737 -0
  83. package/dist/schema/ForgeEvent.json +789 -0
  84. package/dist/schema/ForgeInstance.json +7 -0
  85. package/dist/schema/ForgeKind.json +32 -0
  86. package/dist/schema/ForgeWarning.json +49 -0
  87. package/dist/schema/ForgeWarningCode.json +7 -0
  88. package/dist/schema/GetManyResult.json +699 -0
  89. package/dist/schema/Installation.json +131 -0
  90. package/dist/schema/InstallationToken.json +31 -0
  91. package/dist/schema/KnownForgeKind.json +19 -0
  92. package/dist/schema/Label.json +22 -0
  93. package/dist/schema/MergeMethod.json +14 -0
  94. package/dist/schema/Milestone.json +40 -0
  95. package/dist/schema/Notification.json +498 -0
  96. package/dist/schema/NotificationReason.json +28 -0
  97. package/dist/schema/NotificationRef.json +60 -0
  98. package/dist/schema/NotificationSubject.json +376 -0
  99. package/dist/schema/PerKind.json +49 -0
  100. package/dist/schema/PullBranches.json +125 -0
  101. package/dist/schema/PushCommit.json +24 -0
  102. package/dist/schema/Reaction.json +148 -0
  103. package/dist/schema/ReactionContent.json +17 -0
  104. package/dist/schema/ReactionSummary.json +70 -0
  105. package/dist/schema/Release.json +286 -0
  106. package/dist/schema/ReleaseAsset.json +169 -0
  107. package/dist/schema/ReleaseAssetRef.json +138 -0
  108. package/dist/schema/ReleaseRef.json +107 -0
  109. package/dist/schema/Repo.json +354 -0
  110. package/dist/schema/RepoFeatures.json +35 -0
  111. package/dist/schema/RepoPermissions.json +35 -0
  112. package/dist/schema/RepoRef.json +77 -0
  113. package/dist/schema/RepoRole.json +15 -0
  114. package/dist/schema/RepoVisibility.json +12 -0
  115. package/dist/schema/Review.json +371 -0
  116. package/dist/schema/ReviewComment.json +280 -0
  117. package/dist/schema/ReviewCommentInput.json +34 -0
  118. package/dist/schema/ReviewRef.json +158 -0
  119. package/dist/schema/ReviewState.json +15 -0
  120. package/dist/schema/Reviewer.json +133 -0
  121. package/dist/schema/SecurityAlert.json +219 -0
  122. package/dist/schema/SecurityAlertKind.json +14 -0
  123. package/dist/schema/SecurityAlertRef.json +122 -0
  124. package/dist/schema/SecuritySeverity.json +14 -0
  125. package/dist/schema/SourceKind.json +12 -0
  126. package/dist/schema/SubscriptionState.json +12 -0
  127. package/dist/schema/Support.json +23 -0
  128. package/dist/schema/Tag.json +25 -0
  129. package/dist/schema/TextLimits.json +18 -0
  130. package/dist/schema/Thread.json +653 -0
  131. package/dist/schema/ThreadKind.json +14 -0
  132. package/dist/schema/ThreadRef.json +131 -0
  133. package/dist/schema/ThreadStack.json +146 -0
  134. package/dist/schema/ThreadState.json +13 -0
  135. package/dist/schema/TreeEntry.json +46 -0
  136. package/dist/schema/TreeEntryType.json +13 -0
  137. package/dist/schema/UpsertCommentResult.json +355 -0
  138. package/dist/schema/User.json +123 -0
  139. package/dist/schema/Webhook.json +188 -0
  140. package/dist/schema/WebhookDeliveryRecord.json +173 -0
  141. package/dist/schema/WebhookDeliveryRef.json +135 -0
  142. package/dist/schema/WebhookRef.json +105 -0
  143. package/dist/schema/index.d.mts +4 -0
  144. package/dist/schema/index.mjs +3219 -0
  145. package/dist/tangled/index.d.mts +2 -0
  146. package/dist/tangled/index.mjs +1 -0
  147. package/dist/tangled-ChdPmiv-.mjs +1570 -0
  148. package/dist/testing/index.d.mts +123 -0
  149. package/dist/testing/index.mjs +242 -0
  150. package/dist/webhooks-rn9-8lxT.mjs +70 -0
  151. package/package.json +115 -3
  152. package/dist/.gitkeep +0 -0
@@ -0,0 +1,3259 @@
1
+ import { A as toPage, B as isNamespaceRef, C as requireIssueOrPull, E as summariseChecks, L as REACTION_CONTENTS, M as versionAtLeast, Mt as WebhookVerificationError, Nt as soleMergeMethod, O as toDate, Pt as toMergeError, S as phased, T as resolveToken, V as isResolvedThread, Y as reactionContent, _t as ForgeError, bt as InsufficientScopeError, c as checkRunState, ct as eventAction, d as forgeIterable, dt as fromBase64, f as getManyConcurrently, ft as toFileContent, g as iteratePages, gt as ForgeApiError, h as hostOf, ht as ForbiddenError, i as githubShapedWeb, j as toWarning, jt as UnsupportedOperationError, k as toFileStatus, l as createListing, lt as reviewState, n as perKind, nt as anySignal, ot as sleep, r as verb, rt as createFetcher, s as actorLogin, t as defineForgeProvider, v as memo, w as requireThread, wt as NotFoundError, x as milestoneId, xt as MergeBlockedError, yt as ForgeTimeoutError } from "./define-D3yEXtKK.mjs";
2
+ import { n as headerValue, s as signRs256Jwt, t as bodyText } from "./crypto-CQNEyYHD.mjs";
3
+ import { a as verifyHmacSignature, i as refEvent, r as nativeEventsFor, t as eventKindsOf } from "./webhooks-rn9-8lxT.mjs";
4
+ //#region src/github/auth.ts
5
+ const JWT_LIFETIME_SECONDS = 540;
6
+ const REFRESH_MARGIN_MS = 6e4;
7
+ function createAppCredentials(auth, context) {
8
+ let jwt;
9
+ const tokens = /* @__PURE__ */ new Map();
10
+ async function appJwt() {
11
+ if (jwt && jwt.expiresAt > Date.now() + REFRESH_MARGIN_MS) return jwt.value;
12
+ const now = Math.floor(Date.now() / 1e3);
13
+ const value = await signRs256Jwt(auth.privateKey, {
14
+ iss: String(auth.appId),
15
+ iat: now - 60,
16
+ exp: now + JWT_LIFETIME_SECONDS
17
+ });
18
+ jwt = {
19
+ value,
20
+ expiresAt: (now + JWT_LIFETIME_SECONDS) * 1e3
21
+ };
22
+ return value;
23
+ }
24
+ const fetcher = createFetcher({
25
+ baseUrl: context.baseUrl,
26
+ fetch: context.fetch,
27
+ timeout: context.timeout,
28
+ headers: context.headers,
29
+ authHeaders: async () => ({ authorization: `Bearer ${await appJwt()}` })
30
+ });
31
+ async function installationTokenDetails(installationId, options) {
32
+ const cached = tokens.get(installationId);
33
+ if (cached && cached.expiresAt.getTime() > Date.now() + REFRESH_MARGIN_MS) return cached;
34
+ const { data } = await fetcher.json(`/app/installations/${installationId}/access_tokens`, {
35
+ method: "POST",
36
+ signal: options?.signal
37
+ });
38
+ const token = {
39
+ token: data.token,
40
+ expiresAt: new Date(data.expires_at),
41
+ permissions: data.permissions ?? {},
42
+ repositorySelection: data.repository_selection
43
+ };
44
+ tokens.set(installationId, token);
45
+ return token;
46
+ }
47
+ return {
48
+ appJwt,
49
+ installationTokenDetails,
50
+ installationToken: async (installationId, options) => (await installationTokenDetails(installationId, options)).token
51
+ };
52
+ }
53
+ function createAuthHeaders(auth, credentials) {
54
+ if (!auth || auth.type === "anonymous") return async () => ({});
55
+ if (auth.type === "token") return async () => ({ authorization: `Bearer ${await resolveToken(auth)}` });
56
+ const app = credentials;
57
+ const installationId = auth.installationId;
58
+ if (installationId === void 0) return async () => ({ authorization: `Bearer ${await app.appJwt()}` });
59
+ return async () => ({ authorization: `Bearer ${await app.installationToken(String(installationId))}` });
60
+ }
61
+ //#endregion
62
+ //#region src/github/webhook-events.ts
63
+ /** Native GitHub event names per normalised kind, for hook subscriptions. */
64
+ const GITHUB_NATIVE_EVENTS = {
65
+ comment: [
66
+ "issue_comment",
67
+ "commit_comment",
68
+ "discussion_comment"
69
+ ],
70
+ review: ["pull_request_review"],
71
+ review_comment: ["pull_request_review_comment"],
72
+ state_change: [
73
+ "issues",
74
+ "pull_request",
75
+ "discussion"
76
+ ],
77
+ label: [
78
+ "issues",
79
+ "pull_request",
80
+ "label"
81
+ ],
82
+ assignment: ["issues", "pull_request"],
83
+ push: ["push"],
84
+ ref: ["create", "delete"],
85
+ repo: ["repository"],
86
+ installation: ["installation", "installation_repositories"],
87
+ membership: [
88
+ "member",
89
+ "membership",
90
+ "organization",
91
+ "team"
92
+ ],
93
+ release: ["release"]
94
+ };
95
+ /** Normalised kinds and actions GitHub deliveries translate into. */
96
+ const GITHUB_WEBHOOK_EVENTS = [
97
+ {
98
+ kind: "comment",
99
+ action: "created"
100
+ },
101
+ {
102
+ kind: "comment",
103
+ action: "edited"
104
+ },
105
+ {
106
+ kind: "comment",
107
+ action: "deleted"
108
+ },
109
+ {
110
+ kind: "review",
111
+ action: "submitted"
112
+ },
113
+ {
114
+ kind: "review_comment",
115
+ action: "created"
116
+ },
117
+ {
118
+ kind: "state_change",
119
+ action: "opened"
120
+ },
121
+ {
122
+ kind: "state_change",
123
+ action: "closed"
124
+ },
125
+ {
126
+ kind: "state_change",
127
+ action: "reopened"
128
+ },
129
+ {
130
+ kind: "state_change",
131
+ action: "merged"
132
+ },
133
+ {
134
+ kind: "state_change",
135
+ action: "ready_for_review"
136
+ },
137
+ {
138
+ kind: "state_change",
139
+ action: "converted_to_draft"
140
+ },
141
+ {
142
+ kind: "label",
143
+ action: "labelled"
144
+ },
145
+ {
146
+ kind: "label",
147
+ action: "unlabelled"
148
+ },
149
+ {
150
+ kind: "assignment",
151
+ action: "assigned"
152
+ },
153
+ {
154
+ kind: "assignment",
155
+ action: "unassigned"
156
+ },
157
+ {
158
+ kind: "assignment",
159
+ action: "review_requested"
160
+ },
161
+ { kind: "push" },
162
+ {
163
+ kind: "ref",
164
+ action: "created"
165
+ },
166
+ {
167
+ kind: "ref",
168
+ action: "deleted"
169
+ },
170
+ {
171
+ kind: "repo",
172
+ action: "renamed"
173
+ },
174
+ {
175
+ kind: "repo",
176
+ action: "transferred"
177
+ },
178
+ {
179
+ kind: "repo",
180
+ action: "archived"
181
+ },
182
+ {
183
+ kind: "repo",
184
+ action: "unarchived"
185
+ },
186
+ { kind: "installation" },
187
+ { kind: "membership" },
188
+ {
189
+ kind: "release",
190
+ action: "published"
191
+ }
192
+ ];
193
+ //#endregion
194
+ //#region src/github/normalise.ts
195
+ const FORGE = "github";
196
+ function toActor(instance, user) {
197
+ if (!user) return;
198
+ return {
199
+ forge: FORGE,
200
+ instance,
201
+ login: user.login,
202
+ id: String(user.id ?? user.login),
203
+ name: user.name ?? void 0,
204
+ avatarUrl: user.avatar_url,
205
+ url: user.html_url,
206
+ typeRaw: user.type,
207
+ isBotHint: user.type === "Bot" || user.login.endsWith("[bot]"),
208
+ ...user.type === "Bot" && user.login.endsWith("[bot]") ? { app: { slug: user.login.slice(0, -5) } } : {}
209
+ };
210
+ }
211
+ function toUser(instance, user) {
212
+ return {
213
+ ...toActor(instance, user),
214
+ bio: user.bio ?? void 0,
215
+ company: user.company ?? void 0,
216
+ location: user.location ?? void 0,
217
+ websiteUrl: user.blog || void 0,
218
+ createdAt: toDate(user.created_at),
219
+ followers: user.followers,
220
+ following: user.following,
221
+ publicRepos: user.public_repos,
222
+ raw: user
223
+ };
224
+ }
225
+ function toRepoRef(instance, repo) {
226
+ const [owner, name] = repo.full_name.split("/");
227
+ return {
228
+ forge: FORGE,
229
+ instance,
230
+ owner: repo.owner?.login ?? owner ?? "",
231
+ name: repo.name ?? name ?? "",
232
+ externalId: repo.id === void 0 ? void 0 : String(repo.id)
233
+ };
234
+ }
235
+ /** Reads `owner` and `name` out of an API repository URL, which search results carry instead of a nested repository. */
236
+ function repoRefFromApiUrl(instance, url) {
237
+ const [, owner, name] = url?.match(/\/repos\/([^/]+)\/([^/]+)$/) ?? [];
238
+ return owner && name ? {
239
+ forge: FORGE,
240
+ instance,
241
+ owner,
242
+ name
243
+ } : void 0;
244
+ }
245
+ function toRepo(instance, raw) {
246
+ const visibility = raw.visibility === "internal" ? "internal" : raw.private || raw.visibility === "private" ? "private" : "public";
247
+ return {
248
+ ref: toRepoRef(instance, raw),
249
+ description: raw.description ?? void 0,
250
+ defaultBranch: raw.default_branch,
251
+ visibility,
252
+ visibilityRaw: raw.visibility ?? (raw.private === void 0 ? void 0 : raw.private ? "private" : "public"),
253
+ isFork: raw.fork ?? false,
254
+ isArchived: raw.archived ?? false,
255
+ parent: raw.parent ? toRepoRef(instance, raw.parent) : void 0,
256
+ topics: raw.topics ?? [],
257
+ url: raw.html_url,
258
+ cloneUrls: raw.clone_url || raw.ssh_url ? {
259
+ https: raw.clone_url,
260
+ ssh: raw.ssh_url
261
+ } : void 0,
262
+ createdAt: toDate(raw.created_at),
263
+ updatedAt: toDate(raw.updated_at),
264
+ pushedAt: toDate(raw.pushed_at),
265
+ owner: toActor(instance, raw.owner),
266
+ language: raw.language ?? void 0,
267
+ homepage: raw.homepage || void 0,
268
+ licence: raw.license?.spdx_id && raw.license.spdx_id !== "NOASSERTION" ? raw.license.spdx_id : void 0,
269
+ stars: raw.stargazers_count,
270
+ forks: raw.forks_count,
271
+ watchers: raw.subscribers_count,
272
+ mergeMethods: raw.allow_merge_commit === void 0 && raw.allow_squash_merge === void 0 && raw.allow_rebase_merge === void 0 ? void 0 : [
273
+ ...raw.allow_merge_commit ? ["merge"] : [],
274
+ ...raw.allow_squash_merge ? ["squash"] : [],
275
+ ...raw.allow_rebase_merge ? ["rebase"] : []
276
+ ],
277
+ features: raw.has_issues === void 0 ? void 0 : {
278
+ issues: raw.has_issues,
279
+ pullRequests: true,
280
+ discussions: raw.has_discussions ?? false,
281
+ wiki: raw.has_wiki ?? false,
282
+ projects: raw.has_projects ?? false,
283
+ releases: true
284
+ },
285
+ permissions: raw.permissions ? {
286
+ admin: raw.permissions.admin,
287
+ maintain: raw.permissions.maintain ?? raw.permissions.admin,
288
+ push: raw.permissions.push,
289
+ triage: raw.permissions.triage ?? raw.permissions.push,
290
+ pull: raw.permissions.pull
291
+ } : void 0,
292
+ raw
293
+ };
294
+ }
295
+ function toReactions(raw) {
296
+ if (!raw) return;
297
+ const counts = Object.fromEntries(REACTION_CONTENTS.flatMap((name) => raw[name] ? [[name, raw[name]]] : []));
298
+ return {
299
+ total: raw.total_count ?? Object.values(counts).reduce((sum, count) => sum + count, 0),
300
+ counts
301
+ };
302
+ }
303
+ function toMilestone(raw) {
304
+ return raw ? {
305
+ id: String(raw.number ?? raw.id ?? ""),
306
+ title: raw.title,
307
+ state: raw.state === "closed" ? "closed" : "open",
308
+ description: raw.description ?? void 0,
309
+ dueOn: toDate(raw.due_on),
310
+ url: raw.html_url,
311
+ raw
312
+ } : void 0;
313
+ }
314
+ /** GitHub's `role_name`, or the permission block when a listing reports only that. */
315
+ function toRole(raw) {
316
+ switch (raw.role_name) {
317
+ case "admin":
318
+ case "maintain":
319
+ case "write":
320
+ case "triage":
321
+ case "read": return raw.role_name;
322
+ }
323
+ const permissions = raw.permissions;
324
+ if (!permissions) return "none";
325
+ return permissions.admin ? "admin" : permissions.maintain ? "maintain" : permissions.push ? "write" : permissions.triage ? "triage" : permissions.pull ? "read" : "none";
326
+ }
327
+ function toCollaborator(instance, raw) {
328
+ return {
329
+ actor: toActor(instance, raw),
330
+ role: toRole(raw),
331
+ roleRaw: raw.role_name,
332
+ raw
333
+ };
334
+ }
335
+ function toThreadKind(subjectType) {
336
+ switch (subjectType) {
337
+ case "PullRequest": return "pull_request";
338
+ case "Discussion": return "discussion";
339
+ case "Commit": return "commit";
340
+ case "Issue": return "issue";
341
+ default: return "other";
342
+ }
343
+ }
344
+ function toThreadState(issue) {
345
+ if (issue.merged || issue.merged_at || issue.pull_request?.merged_at) return "merged";
346
+ if (issue.state === "open" || issue.state === "closed") return issue.state;
347
+ return "unknown";
348
+ }
349
+ function numberFromUrl(url) {
350
+ return /\/(?:issues|pulls|commits|discussions)\/([^/?#]+)/.exec(url ?? "")?.[1];
351
+ }
352
+ const REASONS = {
353
+ approval_requested: "approval_requested",
354
+ assign: "assigned",
355
+ author: "author",
356
+ ci_activity: "ci_activity",
357
+ comment: "comment",
358
+ invitation: "invitation",
359
+ manual: "manual",
360
+ mention: "mention",
361
+ review_requested: "review_requested",
362
+ security_alert: "security_alert",
363
+ security_advisory_credit: "security_alert",
364
+ state_change: "state_change",
365
+ subscribed: "subscribed",
366
+ team_mention: "team_mention"
367
+ };
368
+ function toReason(reason) {
369
+ return REASONS[reason] ?? "unknown";
370
+ }
371
+ const SECURITY_SUBJECTS = /* @__PURE__ */ new Set([
372
+ "RepositoryVulnerabilityAlert",
373
+ "RepositoryDependabotAlertsThread",
374
+ "SecretScanningAlert",
375
+ "RepositoryAdvisory"
376
+ ]);
377
+ const SECURITY_SUBJECTS_KIND = {
378
+ RepositoryVulnerabilityAlert: "dependency",
379
+ RepositoryDependabotAlertsThread: "dependency",
380
+ SecretScanningAlert: "secret",
381
+ RepositoryAdvisory: "advisory"
382
+ };
383
+ function lastSegment(url) {
384
+ return /\/([^/?#]+)\/?$/.exec(url ?? "")?.[1];
385
+ }
386
+ function toSubject(instance, raw) {
387
+ const repo = toRepoRef(instance, raw.repository);
388
+ const type = raw.subject.type;
389
+ const kind = toThreadKind(type);
390
+ if (kind !== "other") return {
391
+ type: "thread",
392
+ thread: {
393
+ forge: FORGE,
394
+ instance,
395
+ repo,
396
+ kind,
397
+ typeRaw: type,
398
+ number: numberFromUrl(raw.subject.url)
399
+ }
400
+ };
401
+ if (type === "Release") return {
402
+ type: "release",
403
+ release: {
404
+ forge: FORGE,
405
+ instance,
406
+ repo,
407
+ id: lastSegment(raw.subject.url) ?? raw.id
408
+ }
409
+ };
410
+ if (SECURITY_SUBJECTS.has(type)) {
411
+ const kind = SECURITY_SUBJECTS_KIND[type];
412
+ return {
413
+ type: "security_alert",
414
+ alert: {
415
+ forge: FORGE,
416
+ instance,
417
+ repo,
418
+ id: lastSegment(raw.subject.url) ?? raw.id,
419
+ ...kind ? { kind } : {}
420
+ }
421
+ };
422
+ }
423
+ if (type === "RepositoryInvitation") return {
424
+ type: "repo",
425
+ repo
426
+ };
427
+ return {
428
+ type: "other",
429
+ typeRaw: type,
430
+ repo,
431
+ id: lastSegment(raw.subject.url),
432
+ url: raw.subject.url ?? void 0
433
+ };
434
+ }
435
+ function toNotification(instance, raw) {
436
+ return {
437
+ ref: {
438
+ forge: FORGE,
439
+ instance,
440
+ id: raw.id
441
+ },
442
+ subject: toSubject(instance, raw),
443
+ subjectTypeRaw: raw.subject.type,
444
+ reason: toReason(raw.reason),
445
+ reasonRaw: raw.reason,
446
+ unread: raw.unread,
447
+ title: raw.subject.title,
448
+ subjectState: "unknown",
449
+ updatedAt: toDate(raw.updated_at) ?? /* @__PURE__ */ new Date(0),
450
+ lastReadAt: toDate(raw.last_read_at),
451
+ url: raw.subject.url ?? raw.url,
452
+ raw
453
+ };
454
+ }
455
+ function toLabel(raw) {
456
+ return {
457
+ name: raw.name,
458
+ colour: raw.color ?? void 0,
459
+ description: raw.description ?? void 0
460
+ };
461
+ }
462
+ function toLabels(labels) {
463
+ return (labels ?? []).flatMap((label) => {
464
+ if (typeof label === "string") return [{ name: label }];
465
+ return label.name ? [{
466
+ name: label.name,
467
+ colour: label.color ?? void 0,
468
+ description: label.description ?? void 0
469
+ }] : [];
470
+ });
471
+ }
472
+ function toReviewers(instance, raw) {
473
+ const users = (raw.requested_reviewers ?? []).flatMap((user) => {
474
+ const actor = toActor(instance, user);
475
+ return actor ? [{
476
+ actor,
477
+ state: "pending",
478
+ stateRaw: "requested"
479
+ }] : [];
480
+ });
481
+ const teams = (raw.requested_teams ?? []).map((team) => ({
482
+ actor: {
483
+ forge: FORGE,
484
+ instance,
485
+ login: team.slug,
486
+ id: String(team.id),
487
+ name: team.name,
488
+ url: team.html_url,
489
+ typeRaw: "Team",
490
+ isBotHint: false
491
+ },
492
+ state: "pending",
493
+ stateRaw: "requested",
494
+ isTeam: true
495
+ }));
496
+ return [...users, ...teams];
497
+ }
498
+ /** Builds a thread from an issue, a pull request, or a pull request as returned by the issues API. */
499
+ function toThread(ref, raw) {
500
+ const kind = raw.pull_request || raw.head ? "pull_request" : ref.kind;
501
+ return {
502
+ ref: {
503
+ ...ref,
504
+ kind,
505
+ externalId: raw.node_id ?? ref.externalId
506
+ },
507
+ kind,
508
+ title: raw.title,
509
+ body: raw.body ?? void 0,
510
+ state: toThreadState(raw),
511
+ stateRaw: raw.state,
512
+ stateReason: raw.state_reason ?? void 0,
513
+ isDraft: raw.draft ?? false,
514
+ author: toActor(ref.instance, raw.user),
515
+ assignees: (raw.assignees ?? []).flatMap((user) => toActor(ref.instance, user) ?? []),
516
+ reviewers: toReviewers(ref.instance, raw),
517
+ labels: toLabels(raw.labels),
518
+ url: raw.html_url,
519
+ createdAt: toDate(raw.created_at),
520
+ updatedAt: toDate(raw.updated_at),
521
+ closedAt: toDate(raw.closed_at),
522
+ lastActivityAt: toDate(raw.updated_at),
523
+ locked: raw.locked,
524
+ commentCount: raw.comments,
525
+ milestone: toMilestone(raw.milestone),
526
+ reactions: toReactions(raw.reactions),
527
+ branches: raw.head && raw.base ? {
528
+ head: {
529
+ ref: raw.head.ref,
530
+ sha: raw.head.sha,
531
+ repo: raw.head.repo ? toRepoRef(ref.instance, raw.head.repo) : void 0
532
+ },
533
+ base: {
534
+ ref: raw.base.ref,
535
+ sha: raw.base.sha
536
+ },
537
+ mergeCommitSha: raw.merge_commit_sha ?? void 0
538
+ } : void 0,
539
+ raw
540
+ };
541
+ }
542
+ function toComment(thread, raw) {
543
+ return {
544
+ ref: {
545
+ forge: FORGE,
546
+ instance: thread.instance,
547
+ thread,
548
+ id: String(raw.id)
549
+ },
550
+ body: raw.body ?? "",
551
+ author: toActor(thread.instance, raw.user),
552
+ createdAt: toDate(raw.created_at),
553
+ updatedAt: toDate(raw.updated_at),
554
+ url: raw.html_url,
555
+ reactions: toReactions(raw.reactions),
556
+ raw
557
+ };
558
+ }
559
+ const EVENT_KINDS = {
560
+ "commented": "comment",
561
+ "reviewed": "review",
562
+ "line_commented": "review_comment",
563
+ "review_requested": "assignment",
564
+ "review_request_removed": "assignment",
565
+ "committed": "commit",
566
+ "labeled": "label",
567
+ "unlabeled": "label",
568
+ "closed": "state_change",
569
+ "reopened": "state_change",
570
+ "merged": "state_change",
571
+ "convert_to_draft": "state_change",
572
+ "ready_for_review": "state_change",
573
+ "assigned": "assignment",
574
+ "unassigned": "assignment",
575
+ "mentioned": "mention",
576
+ "cross-referenced": "referenced",
577
+ "referenced": "referenced",
578
+ "reaction": "reaction"
579
+ };
580
+ function toEventKind(event) {
581
+ return EVENT_KINDS[event ?? ""] ?? "other";
582
+ }
583
+ function summarise$1(entry, kind, actor) {
584
+ const who = actor?.login ?? "someone";
585
+ switch (kind) {
586
+ case "comment": return `${who} commented`;
587
+ case "review": return `${who} reviewed (${entry.state ?? "commented"})`;
588
+ case "review_comment": return `${who} commented on a line`;
589
+ case "commit": return `${entry.author?.name ?? who} committed ${entry.sha?.slice(0, 7) ?? "a commit"}`;
590
+ case "label": return `${who} ${entry.event === "unlabeled" ? "removed" : "added"} label ${entry.label?.name ?? ""}`.trim();
591
+ case "state_change": return `${who} ${entry.event ?? "changed state"}`;
592
+ case "referenced": return `${who} referenced this`;
593
+ case "assignment":
594
+ if (entry.event === "review_requested" || entry.event === "review_request_removed") return `${who} ${entry.event === "review_requested" ? "requested review from" : "removed the review request for"} ${entry.requested_reviewer?.login ?? ""}`.trim();
595
+ return `${who} ${entry.event === "unassigned" ? "unassigned" : "assigned"} ${entry.assignee?.login ?? ""}`.trim();
596
+ default: return `${who} ${entry.event ?? "acted"}`;
597
+ }
598
+ }
599
+ /** A `cross-referenced` entry names the thread that mentioned this one; a `referenced` entry names a commit. */
600
+ function crossReference(instance, thread, entry) {
601
+ const source = entry.source?.issue;
602
+ if (source) {
603
+ const repo = source.repository ? toRepoRef(instance, source.repository) : thread.repo;
604
+ return {
605
+ type: "referenced",
606
+ from: {
607
+ forge: FORGE,
608
+ instance,
609
+ repo,
610
+ kind: source.pull_request ? "pull_request" : "issue",
611
+ number: String(source.number)
612
+ },
613
+ fromRepo: repo
614
+ };
615
+ }
616
+ return entry.commit_id ? {
617
+ type: "referenced",
618
+ from: {
619
+ forge: FORGE,
620
+ instance,
621
+ repo: thread.repo,
622
+ sha: entry.commit_id
623
+ },
624
+ fromRepo: thread.repo
625
+ } : { type: "referenced" };
626
+ }
627
+ function timelineDetail(instance, thread, entry, kind) {
628
+ switch (kind) {
629
+ case "comment": return {
630
+ type: "comment",
631
+ comment: entry.id !== void 0 ? {
632
+ forge: FORGE,
633
+ instance,
634
+ thread,
635
+ id: String(entry.id)
636
+ } : void 0,
637
+ body: entry.body ?? void 0
638
+ };
639
+ case "review": return {
640
+ type: "review",
641
+ state: reviewState(entry.state),
642
+ stateRaw: entry.state,
643
+ body: entry.body ?? void 0
644
+ };
645
+ case "label": return entry.label ? {
646
+ type: "label",
647
+ label: entry.label.name
648
+ } : void 0;
649
+ case "assignment": return {
650
+ type: "assignment",
651
+ assignee: toActor(instance, entry.assignee ?? entry.requested_reviewer)
652
+ };
653
+ case "referenced": return crossReference(instance, thread, entry);
654
+ case "state_change": return entry.event === "convert_to_draft" || entry.event === "ready_for_review" ? {
655
+ type: "state_change",
656
+ state: "open",
657
+ draft: entry.event === "convert_to_draft"
658
+ } : {
659
+ type: "state_change",
660
+ state: entry.event === "merged" ? "merged" : entry.event === "closed" ? "closed" : "open"
661
+ };
662
+ default: return;
663
+ }
664
+ }
665
+ function toEvent(instance, thread, entry, source = "poll") {
666
+ const kind = toEventKind(entry.event);
667
+ const actor = toActor(instance, entry.actor ?? entry.user);
668
+ return {
669
+ forge: FORGE,
670
+ instance,
671
+ id: String(entry.node_id ?? entry.id ?? entry.sha ?? `${thread.number ?? ""}-${entry.event}-${entry.created_at}`),
672
+ kind,
673
+ kindRaw: entry.event,
674
+ detail: timelineDetail(instance, thread, entry, kind),
675
+ summary: summarise$1(entry, kind, actor),
676
+ occurredAt: toDate(entry.created_at ?? entry.submitted_at ?? entry.author?.date ?? entry.committer?.date) ?? /* @__PURE__ */ new Date(0),
677
+ actor,
678
+ repo: thread.repo,
679
+ thread,
680
+ source,
681
+ payload: entry
682
+ };
683
+ }
684
+ function statusState(state) {
685
+ return state === "success" ? "success" : state === "pending" ? "pending" : state === "failure" || state === "error" ? "failure" : "unknown";
686
+ }
687
+ function toCheckRun(repo, raw) {
688
+ return {
689
+ ref: {
690
+ forge: FORGE,
691
+ instance: repo.instance,
692
+ repo,
693
+ id: String(raw.id),
694
+ type: "check_run"
695
+ },
696
+ name: raw.name,
697
+ state: checkRunState(raw.status, raw.conclusion),
698
+ stateRaw: raw.status,
699
+ conclusionRaw: raw.conclusion ?? void 0,
700
+ url: raw.html_url ?? raw.details_url ?? void 0,
701
+ startedAt: toDate(raw.started_at),
702
+ completedAt: toDate(raw.completed_at),
703
+ raw
704
+ };
705
+ }
706
+ function toStatusCheck(repo, raw) {
707
+ const state = statusState(raw.state);
708
+ return {
709
+ ref: {
710
+ forge: FORGE,
711
+ instance: repo.instance,
712
+ repo,
713
+ id: String(raw.id),
714
+ type: "status"
715
+ },
716
+ name: raw.context,
717
+ state,
718
+ stateRaw: raw.state,
719
+ url: raw.target_url ?? void 0,
720
+ startedAt: toDate(raw.created_at),
721
+ completedAt: state === "pending" ? void 0 : toDate(raw.updated_at),
722
+ raw
723
+ };
724
+ }
725
+ function toRelease(repo, raw) {
726
+ const ref = {
727
+ forge: FORGE,
728
+ instance: repo.instance,
729
+ repo,
730
+ id: String(raw.id),
731
+ tag: raw.tag_name
732
+ };
733
+ return {
734
+ ref,
735
+ name: raw.name ?? void 0,
736
+ tag: raw.tag_name,
737
+ body: raw.body ?? void 0,
738
+ isDraft: raw.draft,
739
+ isPrerelease: raw.prerelease,
740
+ immutable: raw.immutable,
741
+ author: toActor(repo.instance, raw.author),
742
+ publishedAt: toDate(raw.published_at),
743
+ createdAt: toDate(raw.created_at),
744
+ url: raw.html_url,
745
+ assets: raw.assets?.map((asset) => ({
746
+ ...asset.id === void 0 ? {} : { ref: {
747
+ forge: FORGE,
748
+ instance: repo.instance,
749
+ repo,
750
+ release: ref,
751
+ id: String(asset.id)
752
+ } },
753
+ name: asset.name,
754
+ url: asset.browser_download_url,
755
+ size: asset.size,
756
+ downloadCount: asset.download_count,
757
+ contentType: asset.content_type
758
+ })),
759
+ raw
760
+ };
761
+ }
762
+ function toSeverity(raw) {
763
+ switch (raw?.toLowerCase()) {
764
+ case "critical": return "critical";
765
+ case "high":
766
+ case "error": return "high";
767
+ case "medium":
768
+ case "moderate":
769
+ case "warning": return "medium";
770
+ case "low":
771
+ case "note": return "low";
772
+ default: return "unknown";
773
+ }
774
+ }
775
+ function alertState(raw) {
776
+ return raw === "open" ? "open" : raw === "fixed" ? "fixed" : raw === "dismissed" || raw === "auto_dismissed" ? "dismissed" : "unknown";
777
+ }
778
+ function toDependabotAlert(repo, raw) {
779
+ const severityRaw = raw.security_vulnerability?.severity ?? raw.security_advisory?.severity;
780
+ const pkg = raw.dependency?.package;
781
+ return {
782
+ ref: {
783
+ forge: FORGE,
784
+ instance: repo.instance,
785
+ repo,
786
+ id: String(raw.number),
787
+ kind: "dependency",
788
+ severityRaw
789
+ },
790
+ kind: "dependency",
791
+ kindRaw: "dependabot",
792
+ severity: toSeverity(severityRaw),
793
+ severityRaw,
794
+ state: alertState(raw.state),
795
+ stateRaw: raw.state,
796
+ title: raw.security_advisory?.summary ?? `Vulnerable dependency ${pkg?.name ?? ""}`.trim(),
797
+ ...pkg ? { package: {
798
+ ecosystem: pkg.ecosystem,
799
+ name: pkg.name,
800
+ vulnerableRange: raw.security_vulnerability?.vulnerable_version_range,
801
+ fixedIn: raw.security_vulnerability?.first_patched_version?.identifier
802
+ } } : {},
803
+ url: raw.html_url,
804
+ createdAt: toDate(raw.created_at),
805
+ updatedAt: toDate(raw.updated_at),
806
+ dismissedAt: toDate(raw.dismissed_at),
807
+ raw
808
+ };
809
+ }
810
+ function toCodeScanningAlert(repo, raw) {
811
+ const severityRaw = raw.rule?.security_severity_level ?? raw.rule?.severity ?? void 0;
812
+ return {
813
+ ref: {
814
+ forge: FORGE,
815
+ instance: repo.instance,
816
+ repo,
817
+ id: String(raw.number),
818
+ kind: "code_scanning",
819
+ severityRaw
820
+ },
821
+ kind: "code_scanning",
822
+ kindRaw: raw.tool?.name ?? "code_scanning",
823
+ severity: toSeverity(severityRaw),
824
+ severityRaw,
825
+ state: alertState(raw.state),
826
+ stateRaw: raw.state,
827
+ title: raw.rule?.description ?? raw.rule?.name ?? raw.rule?.id ?? "Code scanning alert",
828
+ url: raw.html_url,
829
+ createdAt: toDate(raw.created_at),
830
+ updatedAt: toDate(raw.updated_at),
831
+ dismissedAt: toDate(raw.dismissed_at),
832
+ raw
833
+ };
834
+ }
835
+ /** Secret scanning reports no severity. A revoked secret is fixed; any other resolution dismisses the alert. */
836
+ function toSecretScanningAlert(repo, raw) {
837
+ const state = raw.state === "open" ? "open" : raw.state === "resolved" ? raw.resolution === "revoked" ? "fixed" : "dismissed" : "unknown";
838
+ return {
839
+ ref: {
840
+ forge: FORGE,
841
+ instance: repo.instance,
842
+ repo,
843
+ id: String(raw.number),
844
+ kind: "secret"
845
+ },
846
+ kind: "secret",
847
+ kindRaw: raw.secret_type ?? "secret_scanning",
848
+ severity: "unknown",
849
+ state,
850
+ stateRaw: raw.resolution ? `${raw.state}:${raw.resolution}` : raw.state,
851
+ title: raw.secret_type_display_name ?? raw.secret_type ?? "Secret scanning alert",
852
+ url: raw.html_url,
853
+ createdAt: toDate(raw.created_at),
854
+ updatedAt: toDate(raw.updated_at),
855
+ dismissedAt: state === "dismissed" ? toDate(raw.resolved_at) : void 0,
856
+ raw
857
+ };
858
+ }
859
+ function toReviewComment(thread, raw, reviewThread) {
860
+ return {
861
+ ref: {
862
+ forge: FORGE,
863
+ instance: thread.instance,
864
+ thread,
865
+ id: String(raw.id)
866
+ },
867
+ body: raw.body ?? "",
868
+ author: toActor(thread.instance, raw.user),
869
+ path: raw.path,
870
+ line: raw.line ?? raw.original_line ?? void 0,
871
+ side: raw.side?.toLowerCase() === "left" ? "left" : raw.side ? "right" : void 0,
872
+ inReplyTo: raw.in_reply_to_id === null || raw.in_reply_to_id === void 0 ? void 0 : String(raw.in_reply_to_id),
873
+ thread: reviewThread,
874
+ createdAt: toDate(raw.created_at),
875
+ url: raw.html_url,
876
+ raw
877
+ };
878
+ }
879
+ function toReview(thread, raw, comments) {
880
+ return {
881
+ ref: {
882
+ forge: FORGE,
883
+ instance: thread.instance,
884
+ thread,
885
+ id: String(raw.id)
886
+ },
887
+ author: toActor(thread.instance, raw.user),
888
+ state: reviewState(raw.state),
889
+ stateRaw: raw.state,
890
+ body: raw.body || void 0,
891
+ submittedAt: toDate(raw.submitted_at ?? void 0),
892
+ comments,
893
+ url: raw.html_url,
894
+ raw
895
+ };
896
+ }
897
+ function toWorkflowRun(repo, raw) {
898
+ return {
899
+ ref: {
900
+ forge: FORGE,
901
+ instance: repo.instance,
902
+ repo,
903
+ id: String(raw.id)
904
+ },
905
+ name: raw.name || raw.display_title || String(raw.id),
906
+ state: checkRunState(raw.status ?? "queued", raw.conclusion),
907
+ stateRaw: raw.status ?? "unknown",
908
+ number: raw.run_number === void 0 ? void 0 : String(raw.run_number),
909
+ eventRaw: raw.event,
910
+ branch: raw.head_branch ?? void 0,
911
+ sha: raw.head_sha,
912
+ url: raw.html_url,
913
+ actor: toActor(repo.instance, raw.actor),
914
+ createdAt: toDate(raw.created_at),
915
+ startedAt: toDate(raw.run_started_at),
916
+ completedAt: raw.status === "completed" ? toDate(raw.updated_at) : void 0,
917
+ raw
918
+ };
919
+ }
920
+ function toWorkflowJob(run, raw) {
921
+ return {
922
+ ref: {
923
+ forge: FORGE,
924
+ instance: run.instance,
925
+ repo: run.repo,
926
+ id: String(raw.id),
927
+ run
928
+ },
929
+ name: raw.name,
930
+ state: checkRunState(raw.status ?? "queued", raw.conclusion),
931
+ stateRaw: raw.status ?? "unknown",
932
+ url: raw.html_url ?? void 0,
933
+ startedAt: toDate(raw.started_at),
934
+ completedAt: toDate(raw.completed_at),
935
+ raw
936
+ };
937
+ }
938
+ function toChangedFile(raw) {
939
+ return {
940
+ path: raw.filename,
941
+ previousPath: raw.previous_filename,
942
+ status: toFileStatus(raw.status),
943
+ statusRaw: raw.status,
944
+ additions: raw.additions,
945
+ deletions: raw.deletions,
946
+ patch: raw.patch,
947
+ sha: raw.sha
948
+ };
949
+ }
950
+ function toCommit(repo, raw) {
951
+ return {
952
+ ref: {
953
+ forge: FORGE,
954
+ instance: repo.instance,
955
+ repo,
956
+ sha: raw.sha
957
+ },
958
+ sha: raw.sha,
959
+ message: raw.commit.message,
960
+ author: {
961
+ actor: toActor(repo.instance, raw.author),
962
+ name: raw.commit.author?.name,
963
+ email: raw.commit.author?.email,
964
+ date: toDate(raw.commit.author?.date)
965
+ },
966
+ committer: {
967
+ actor: toActor(repo.instance, raw.committer),
968
+ name: raw.commit.committer?.name,
969
+ email: raw.commit.committer?.email,
970
+ date: toDate(raw.commit.committer?.date)
971
+ },
972
+ parents: raw.parents?.map((parent) => parent.sha) ?? [],
973
+ url: raw.html_url,
974
+ ...raw.stats ? { stats: {
975
+ additions: raw.stats.additions ?? 0,
976
+ deletions: raw.stats.deletions ?? 0,
977
+ total: raw.stats.total
978
+ } } : {},
979
+ ...raw.files ? { files: raw.files.map(toChangedFile) } : {},
980
+ raw
981
+ };
982
+ }
983
+ function toBranch(raw, defaultBranch) {
984
+ return {
985
+ name: raw.name,
986
+ sha: raw.commit.sha,
987
+ isDefault: defaultBranch === void 0 ? void 0 : raw.name === defaultBranch,
988
+ isProtected: raw.protected,
989
+ raw
990
+ };
991
+ }
992
+ function toTag(raw) {
993
+ return {
994
+ name: raw.name,
995
+ sha: raw.commit.sha,
996
+ raw
997
+ };
998
+ }
999
+ const TREE_TYPES = {
1000
+ blob: "file",
1001
+ tree: "directory",
1002
+ commit: "submodule"
1003
+ };
1004
+ function toTreeEntry(raw) {
1005
+ return {
1006
+ path: raw.path,
1007
+ type: raw.mode === "120000" ? "symlink" : TREE_TYPES[raw.type ?? ""] ?? "file",
1008
+ sha: raw.sha,
1009
+ size: raw.size,
1010
+ mode: raw.mode
1011
+ };
1012
+ }
1013
+ function toWebhook(target, raw) {
1014
+ const nativeEvents = raw.events ?? [];
1015
+ return {
1016
+ ref: {
1017
+ forge: FORGE,
1018
+ instance: target.instance,
1019
+ target,
1020
+ id: String(raw.id)
1021
+ },
1022
+ url: raw.config?.url ?? "",
1023
+ events: eventKindsOf(GITHUB_NATIVE_EVENTS, nativeEvents),
1024
+ nativeEvents,
1025
+ active: raw.active ?? true,
1026
+ contentType: raw.config?.content_type === "form" ? "form" : "json",
1027
+ createdAt: toDate(raw.created_at),
1028
+ updatedAt: toDate(raw.updated_at),
1029
+ raw
1030
+ };
1031
+ }
1032
+ function toWebhookDelivery(hook, raw) {
1033
+ const status = raw.status_code ?? 0;
1034
+ return {
1035
+ ref: {
1036
+ forge: FORGE,
1037
+ instance: hook.instance,
1038
+ hook,
1039
+ id: String(raw.id),
1040
+ guid: raw.guid
1041
+ },
1042
+ event: raw.action ? `${raw.event}.${raw.action}` : raw.event ?? "",
1043
+ status,
1044
+ ok: status >= 200 && status < 300,
1045
+ deliveredAt: toDate(raw.delivered_at),
1046
+ duration: raw.duration,
1047
+ raw
1048
+ };
1049
+ }
1050
+ //#endregion
1051
+ //#region src/github/graphql-client.ts
1052
+ function graphqlUrl(baseUrl) {
1053
+ const url = new URL(baseUrl);
1054
+ if (url.host === "api.github.com") return "https://api.github.com/graphql";
1055
+ return `${url.origin}${url.pathname.replace(/\/v3\/?$/, "")}/graphql`;
1056
+ }
1057
+ /** A failed GraphQL response as an error: `InsufficientScopeError` when the token lacks a scope a field needs. */
1058
+ function graphqlError(operationName, errors, url, context) {
1059
+ const message = errors?.map((error) => error.message).join("; ") || "Empty GraphQL response";
1060
+ return new (errors?.some((error) => error.type === "INSUFFICIENT_SCOPES") ? InsufficientScopeError : ForgeApiError)(`GraphQL ${operationName ?? "request"} failed: ${message}`, 200, JSON.stringify(errors ?? null).slice(0, 512), {
1061
+ forge: FORGE,
1062
+ instance: context.instance,
1063
+ url,
1064
+ method: "POST"
1065
+ });
1066
+ }
1067
+ function createGraphQLClient(fetcher, url, context) {
1068
+ return async (document, variables, options) => {
1069
+ const query = typeof document === "string" ? (await import("./graphql-D7uGzZTf.mjs"))[document] : document.query;
1070
+ const operationName = /(?:query|mutation)\s+(\w+)/.exec(query)?.[1];
1071
+ const { data } = await fetcher.json(url, {
1072
+ method: "POST",
1073
+ json: {
1074
+ query,
1075
+ variables,
1076
+ operationName
1077
+ },
1078
+ signal: options?.signal
1079
+ });
1080
+ if (data.errors?.length || !data.data) throw graphqlError(operationName, data.errors, url, context);
1081
+ return data.data;
1082
+ };
1083
+ }
1084
+ //#endregion
1085
+ //#region src/github/webhooks.ts
1086
+ const SIGNATURE_HEADER = "x-hub-signature-256";
1087
+ const EVENT_HEADER = "x-github-event";
1088
+ function verifyGitHubSignature(delivery, secret) {
1089
+ return verifyHmacSignature(delivery, secret, {
1090
+ header: SIGNATURE_HEADER,
1091
+ prefix: "sha256="
1092
+ });
1093
+ }
1094
+ function repoLevel(instance, event, payload, who) {
1095
+ switch (event) {
1096
+ case "push": return {
1097
+ ...refEvent(who, {
1098
+ ref: payload.ref ?? "",
1099
+ before: payload.before,
1100
+ after: payload.after,
1101
+ deleted: payload.deleted,
1102
+ forced: payload.forced,
1103
+ commits: (payload.commits ?? []).map((commit) => ({
1104
+ sha: commit.id,
1105
+ message: commit.message,
1106
+ author: commit.author?.username ?? commit.author?.name,
1107
+ url: commit.url
1108
+ }))
1109
+ }),
1110
+ occurredAt: payload.deleted ? void 0 : toDate(payload.head_commit?.timestamp)
1111
+ };
1112
+ case "create":
1113
+ case "delete": return {
1114
+ kind: "ref",
1115
+ action: event === "create" ? "created" : "deleted",
1116
+ detail: {
1117
+ type: "ref",
1118
+ ref: payload.ref ?? "",
1119
+ refType: payload.ref_type ?? "branch"
1120
+ },
1121
+ summary: `${who} ${event === "create" ? "created" : "deleted"} ${payload.ref_type ?? "ref"} ${payload.ref ?? ""}`.trim()
1122
+ };
1123
+ case "repository":
1124
+ if (payload.action === "renamed") return {
1125
+ kind: "repo",
1126
+ action: "renamed",
1127
+ detail: {
1128
+ type: "repo_renamed",
1129
+ from: payload.changes?.repository?.name?.from ?? "",
1130
+ to: payload.repository?.name ?? ""
1131
+ },
1132
+ summary: `${who} renamed the repository`
1133
+ };
1134
+ if (payload.action === "transferred") {
1135
+ const from = payload.changes?.owner?.from;
1136
+ return {
1137
+ kind: "repo",
1138
+ action: "transferred",
1139
+ detail: {
1140
+ type: "repo_transferred",
1141
+ fromOwner: (from?.organization ?? from?.user)?.login,
1142
+ toOwner: payload.repository?.owner?.login
1143
+ },
1144
+ summary: `${who} transferred the repository`
1145
+ };
1146
+ }
1147
+ if (payload.action === "archived" || payload.action === "unarchived") return {
1148
+ kind: "repo",
1149
+ action: payload.action,
1150
+ detail: {
1151
+ type: "repo_archived",
1152
+ archived: payload.action === "archived"
1153
+ },
1154
+ summary: `${who} ${payload.action} the repository`
1155
+ };
1156
+ return;
1157
+ case "installation":
1158
+ case "installation_repositories": return {
1159
+ kind: "installation",
1160
+ detail: {
1161
+ type: "installation",
1162
+ actionRaw: payload.action ?? event,
1163
+ installationId: payload.installation ? String(payload.installation.id) : void 0,
1164
+ added: (payload.repositories_added ?? (payload.action === "created" ? payload.repositories : void 0) ?? []).map((repo) => toRepoRef(instance, repo)),
1165
+ removed: (payload.repositories_removed ?? []).map((repo) => toRepoRef(instance, repo))
1166
+ },
1167
+ summary: `${who} ${payload.action ?? "changed"} the installation`
1168
+ };
1169
+ case "member":
1170
+ case "membership": return {
1171
+ kind: "membership",
1172
+ detail: {
1173
+ type: "membership",
1174
+ actionRaw: payload.action ?? event,
1175
+ member: toActor(instance, payload.member ?? payload.membership?.user)
1176
+ },
1177
+ summary: `${who} ${payload.action ?? "changed"} a member`
1178
+ };
1179
+ case "release":
1180
+ if (payload.action !== "published" || !payload.release || !payload.repository) return;
1181
+ return {
1182
+ kind: "release",
1183
+ action: "published",
1184
+ detail: {
1185
+ type: "release",
1186
+ release: {
1187
+ forge: FORGE,
1188
+ instance,
1189
+ repo: toRepoRef(instance, payload.repository),
1190
+ id: String(payload.release.id),
1191
+ tag: payload.release.tag_name
1192
+ },
1193
+ name: payload.release.name ?? void 0
1194
+ },
1195
+ summary: `${who} published ${payload.release.name ?? payload.release.tag_name ?? "a release"}`,
1196
+ occurredAt: toDate(payload.release.published_at)
1197
+ };
1198
+ default: return;
1199
+ }
1200
+ }
1201
+ const THREAD_EVENT_KIND = {
1202
+ issue_comment: "comment",
1203
+ discussion_comment: "comment",
1204
+ commit_comment: "comment",
1205
+ pull_request_review: "review",
1206
+ pull_request_review_comment: "review_comment"
1207
+ };
1208
+ function threadOf(payload, repo, instance) {
1209
+ if (!repo) return;
1210
+ const candidates = [
1211
+ ["pull_request", payload.pull_request],
1212
+ ["discussion", payload.discussion],
1213
+ [payload.issue?.pull_request ? "pull_request" : "issue", payload.issue]
1214
+ ];
1215
+ for (const [kind, subject] of candidates) if (subject) return {
1216
+ forge: FORGE,
1217
+ instance,
1218
+ repo,
1219
+ kind,
1220
+ number: String(subject.number),
1221
+ externalId: subject.node_id
1222
+ };
1223
+ }
1224
+ function eventKindFor(event, payload) {
1225
+ const mapped = THREAD_EVENT_KIND[event];
1226
+ if (mapped) return mapped;
1227
+ if (event === "issues" || event === "pull_request" || event === "discussion") switch (payload.action) {
1228
+ case "labeled":
1229
+ case "unlabeled": return "label";
1230
+ case "assigned":
1231
+ case "unassigned": return "assignment";
1232
+ case "closed":
1233
+ case "reopened":
1234
+ case "opened":
1235
+ case "ready_for_review":
1236
+ case "converted_to_draft": return "state_change";
1237
+ case "synchronize": return "commit";
1238
+ case "review_requested":
1239
+ case "review_request_removed": return "assignment";
1240
+ default: return "other";
1241
+ }
1242
+ if (event.endsWith("reaction")) return "reaction";
1243
+ return "other";
1244
+ }
1245
+ function summarise(event, payload, actor) {
1246
+ switch (event) {
1247
+ case "issue_comment":
1248
+ case "discussion_comment":
1249
+ case "commit_comment": return `${actor} ${payload.action ?? "created"} a comment`;
1250
+ case "pull_request_review": return `${actor} ${payload.action ?? "submitted"} a review (${payload.review?.state ?? "commented"})`;
1251
+ case "pull_request_review_comment": return `${actor} ${payload.action ?? "created"} a review comment`;
1252
+ case "push": return `${actor} pushed ${payload.commits?.length ?? 0} commit(s)`;
1253
+ case "label": return `${actor} ${payload.action ?? "changed"} label ${payload.label?.name ?? ""}`.trim();
1254
+ default: return `${actor} ${payload.action ? `${payload.action} ` : ""}${event}`.trim();
1255
+ }
1256
+ }
1257
+ function actionOf(event, payload) {
1258
+ if (event === "pull_request" && payload.action === "closed" && payload.pull_request?.merged) return "merged";
1259
+ return eventAction(payload.action);
1260
+ }
1261
+ function detailOf(kind, thread, instance, payload) {
1262
+ const comment = payload.comment;
1263
+ const commentRef = comment?.id !== void 0 && thread ? {
1264
+ forge: FORGE,
1265
+ instance,
1266
+ thread,
1267
+ id: String(comment.id)
1268
+ } : void 0;
1269
+ switch (kind) {
1270
+ case "comment": return {
1271
+ type: "comment",
1272
+ comment: commentRef,
1273
+ body: comment?.body
1274
+ };
1275
+ case "review_comment": return {
1276
+ type: "review_comment",
1277
+ comment: commentRef,
1278
+ body: comment?.body,
1279
+ path: comment?.path,
1280
+ line: comment?.line ?? void 0
1281
+ };
1282
+ case "review": return {
1283
+ type: "review",
1284
+ state: reviewState(payload.review?.state),
1285
+ stateRaw: payload.review?.state,
1286
+ body: payload.review?.body ?? void 0
1287
+ };
1288
+ case "label": return payload.label?.name ? {
1289
+ type: "label",
1290
+ label: payload.label.name
1291
+ } : void 0;
1292
+ case "assignment": return {
1293
+ type: "assignment",
1294
+ assignee: toActor(instance, payload.assignee ?? payload.requested_reviewer ?? void 0)
1295
+ };
1296
+ case "state_change": {
1297
+ const pull = payload.pull_request;
1298
+ return {
1299
+ type: "state_change",
1300
+ state: pull?.merged ? "merged" : payload.action === "closed" ? "closed" : "open",
1301
+ draft: pull?.draft
1302
+ };
1303
+ }
1304
+ default: return;
1305
+ }
1306
+ }
1307
+ function translateGitHubWebhook(instance, delivery) {
1308
+ const event = headerValue(delivery.headers, EVENT_HEADER);
1309
+ if (!event) throw new WebhookVerificationError(`Missing ${EVENT_HEADER} header`, {
1310
+ forge: FORGE,
1311
+ instance
1312
+ });
1313
+ const deliveryId = headerValue(delivery.headers, "x-github-delivery") ?? crypto.randomUUID();
1314
+ const payload = JSON.parse(bodyText(delivery.body));
1315
+ const repo = payload.repository ? toRepoRef(instance, payload.repository) : void 0;
1316
+ const thread = threadOf(payload, repo, instance);
1317
+ const actor = toActor(instance, payload.sender);
1318
+ const occurredAt = toDate(payload.comment?.created_at ?? payload.review?.submitted_at ?? payload.head_commit?.timestamp) ?? /* @__PURE__ */ new Date();
1319
+ const installationId = payload.installation ? String(payload.installation.id) : void 0;
1320
+ const level = repoLevel(instance, event, payload, actor?.login ?? "someone");
1321
+ if (level) return [{
1322
+ forge: FORGE,
1323
+ instance,
1324
+ id: deliveryId,
1325
+ kind: level.kind,
1326
+ action: level.action,
1327
+ kindRaw: payload.action ? `${event}.${payload.action}` : event,
1328
+ summary: level.summary,
1329
+ occurredAt: level.occurredAt ?? /* @__PURE__ */ new Date(),
1330
+ actor,
1331
+ repo,
1332
+ detail: level.detail,
1333
+ installationId,
1334
+ source: "webhook",
1335
+ payload
1336
+ }];
1337
+ const kind = eventKindFor(event, payload);
1338
+ return [{
1339
+ forge: FORGE,
1340
+ instance,
1341
+ id: deliveryId,
1342
+ kind,
1343
+ kindRaw: payload.action ? `${event}.${payload.action}` : event,
1344
+ action: actionOf(event, payload),
1345
+ actionRaw: payload.action,
1346
+ detail: detailOf(kind, thread, instance, payload),
1347
+ summary: summarise(event, payload, actor?.login ?? "someone"),
1348
+ occurredAt,
1349
+ actor,
1350
+ repo,
1351
+ thread,
1352
+ installationId,
1353
+ source: "webhook",
1354
+ payload
1355
+ }];
1356
+ }
1357
+ const githubWebhooks = ({ options, instance }) => ({
1358
+ events: GITHUB_WEBHOOK_EVENTS,
1359
+ verify: (delivery) => verifyGitHubSignature(delivery, options.webhookSecret),
1360
+ translate: (delivery) => translateGitHubWebhook(instance, delivery)
1361
+ });
1362
+ //#endregion
1363
+ //#region src/github/index.ts
1364
+ const DISCUSSION_CLOSE_REASONS = {
1365
+ completed: "resolved",
1366
+ duplicate: "duplicate"
1367
+ };
1368
+ /** GitHub Enterprise Server versions that support marking a notification done. */
1369
+ const GHES_MARK_DONE = "3.13";
1370
+ /** GitHub Enterprise Server versions with the Dependabot alerts REST API. */
1371
+ const GHES_DEPENDABOT_ALERTS = "3.8";
1372
+ const GRAPHQL_BATCH = 20;
1373
+ const REVIEW_CONTEXT_TTL = 3e5;
1374
+ const POLL_DELAYS = [
1375
+ 500,
1376
+ 1e3,
1377
+ 2e3
1378
+ ];
1379
+ const GITHUB_RESERVED_PATHS = [
1380
+ "about",
1381
+ "account",
1382
+ "apps",
1383
+ "codespaces",
1384
+ "collections",
1385
+ "contact",
1386
+ "customer-stories",
1387
+ "dashboard",
1388
+ "enterprise",
1389
+ "enterprises",
1390
+ "events",
1391
+ "explore",
1392
+ "features",
1393
+ "gist",
1394
+ "issues",
1395
+ "join",
1396
+ "login",
1397
+ "logout",
1398
+ "marketplace",
1399
+ "new",
1400
+ "notifications",
1401
+ "organizations",
1402
+ "orgs",
1403
+ "pricing",
1404
+ "pulls",
1405
+ "search",
1406
+ "security",
1407
+ "settings",
1408
+ "signup",
1409
+ "site",
1410
+ "sponsors",
1411
+ "stars",
1412
+ "topics",
1413
+ "trending",
1414
+ "users",
1415
+ "watching"
1416
+ ];
1417
+ const ISSUE_LIKE = {
1418
+ issue: true,
1419
+ pull_request: true,
1420
+ discussion: true
1421
+ };
1422
+ function repoPath(repo) {
1423
+ return `/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}`;
1424
+ }
1425
+ function threadPath(ref) {
1426
+ switch (ref.kind) {
1427
+ case "pull_request": return `${repoPath(ref.repo)}/pulls/${encodeURIComponent(ref.number)}`;
1428
+ case "commit": return `${repoPath(ref.repo)}/commits/${encodeURIComponent(ref.number)}`;
1429
+ default: return `${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}`;
1430
+ }
1431
+ }
1432
+ function isGheCom(host) {
1433
+ return /^api\.[^.]+\.ghe\.com$/.test(host);
1434
+ }
1435
+ /** The web host for an API host: `api.github.com` and `api.<name>.ghe.com` drop the `api.` prefix. */
1436
+ function webHost(host) {
1437
+ return host === "api.github.com" || isGheCom(host) ? host.slice(4) : host;
1438
+ }
1439
+ function installationId(installation) {
1440
+ return typeof installation === "string" ? installation : installation.id;
1441
+ }
1442
+ /**
1443
+ * Free text as legacy search reads it: advanced search parses parentheses and
1444
+ * an unclosed quote as syntax, so those are dropped outside quoted phrases.
1445
+ */
1446
+ function advancedFreeText(text) {
1447
+ const parts = text.split("\"");
1448
+ if (parts.length % 2 === 0) {
1449
+ const unclosed = parts.pop();
1450
+ parts.push(`${parts.pop()} ${unclosed}`);
1451
+ }
1452
+ return parts.map((part, index) => index % 2 ? part : part.replace(/[()\s]+/g, " ")).join("\"").trim();
1453
+ }
1454
+ function setupGitHub({ options, baseUrl, instance, origin: context, fetcher, createFetcher, derive, state: credentials }) {
1455
+ const list = createListing(fetcher, "per_page");
1456
+ const auth = options.auth ?? { type: "anonymous" };
1457
+ /** GitHub GraphQL needs a credential, so anonymous providers stay on REST. */
1458
+ const anonymous = auth.type === "anonymous";
1459
+ const host = hostOf(baseUrl);
1460
+ /** GitHub Enterprise Server; `ghe.com` data residency hosts follow github.com. */
1461
+ const enterprise = host !== "api.github.com" && !isGheCom(host);
1462
+ const graphql = createGraphQLClient(fetcher, graphqlUrl(baseUrl), { instance });
1463
+ /**
1464
+ * Discussion notification subjects carry no URL. The repo's most recently
1465
+ * updated discussions are listed and matched on an exact, unique title.
1466
+ */
1467
+ async function resolveDiscussionNumbers(items) {
1468
+ const pending = items.filter((item) => item.subject.type === "thread" && item.subject.thread.kind === "discussion" && !item.subject.thread.number);
1469
+ if (!pending.length) return [];
1470
+ const repos = [...new Map(pending.map((item) => {
1471
+ const repo = item.subject.thread.repo;
1472
+ return [`${repo.owner}/${repo.name}`, repo];
1473
+ })).values()];
1474
+ let result;
1475
+ try {
1476
+ const { query, variables } = (await import("./graphql-D7uGzZTf.mjs")).recentDiscussionsQuery(repos);
1477
+ result = await graphql({ query }, variables);
1478
+ } catch (error) {
1479
+ if (error instanceof InsufficientScopeError || error instanceof ForgeApiError && error.status === 200) return [toWarning("discussion_unresolved", error)];
1480
+ throw error;
1481
+ }
1482
+ for (const item of pending) {
1483
+ const thread = item.subject.thread;
1484
+ const index = repos.findIndex((repo) => repo.owner === thread.repo.owner && repo.name === thread.repo.name);
1485
+ const matches = (result[`r${index}`]?.discussions.nodes ?? []).filter((node) => node.title === item.title);
1486
+ if (matches.length === 1) item.subject = {
1487
+ type: "thread",
1488
+ thread: {
1489
+ ...thread,
1490
+ number: String(matches[0].number),
1491
+ externalId: matches[0].id
1492
+ }
1493
+ };
1494
+ }
1495
+ return [];
1496
+ }
1497
+ async function notificationPage(listOptions = {}) {
1498
+ const result = await fetcher.json(listOptions.cursor?.nextUrl ?? "/notifications", {
1499
+ query: listOptions.cursor?.nextUrl ? void 0 : {
1500
+ all: listOptions.all ?? false,
1501
+ since: listOptions.since?.toISOString(),
1502
+ per_page: listOptions.perPage
1503
+ },
1504
+ etag: listOptions.cursor?.nextUrl ? void 0 : listOptions.cursor?.etag,
1505
+ signal: listOptions.signal
1506
+ });
1507
+ const items = (result.data ?? []).map((raw) => toNotification(instance, raw));
1508
+ const warnings = await resolveDiscussionNumbers(items);
1509
+ const etag = listOptions.cursor?.nextUrl ? void 0 : result.cursor?.etag ?? listOptions.cursor?.etag;
1510
+ return {
1511
+ items,
1512
+ cursor: result.cursor?.nextUrl ? result.cursor : void 0,
1513
+ ...etag ? { etag } : {},
1514
+ notModified: result.notModified,
1515
+ warnings: warnings.length ? warnings : void 0
1516
+ };
1517
+ }
1518
+ async function discussionId(ref, options) {
1519
+ if (ref.externalId) return ref.externalId;
1520
+ return (await getDiscussion(ref, options)).ref.externalId;
1521
+ }
1522
+ async function getDiscussion(ref, options) {
1523
+ const discussion = (await graphql("DISCUSSION_THREAD", {
1524
+ owner: ref.repo.owner,
1525
+ name: ref.repo.name,
1526
+ number: Number(ref.number)
1527
+ }, options)).repository?.discussion;
1528
+ if (!discussion) throw new NotFoundError(`Discussion ${ref.number} not found`, 404, "", context);
1529
+ return (await import("./graphql-D7uGzZTf.mjs")).toDiscussionThread(ref, discussion);
1530
+ }
1531
+ async function discussionCommentPage(ref, listOptions) {
1532
+ const comments = (await graphql("DISCUSSION_COMMENTS", {
1533
+ owner: ref.repo.owner,
1534
+ name: ref.repo.name,
1535
+ number: Number(ref.number),
1536
+ after: listOptions.cursor?.token ?? null
1537
+ }, listOptions)).repository?.discussion?.comments;
1538
+ return {
1539
+ items: (comments?.nodes ?? []).flatMap((comment) => [{
1540
+ comment,
1541
+ isReply: false
1542
+ }, ...(comment.replies?.nodes ?? []).map((reply) => ({
1543
+ comment: reply,
1544
+ isReply: true
1545
+ }))]),
1546
+ cursor: comments?.pageInfo.hasNextPage && comments.pageInfo.endCursor ? { token: comments.pageInfo.endCursor } : void 0
1547
+ };
1548
+ }
1549
+ async function get(thread, options) {
1550
+ const ref = requireThread(thread, context);
1551
+ if (ref.kind === "discussion") return getDiscussion(ref, options);
1552
+ const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
1553
+ const result = toThread(ref, data);
1554
+ if (ref.kind === "pull_request" && data.head?.sha) try {
1555
+ const checks = await headChecks(ref.repo, data.head.sha, options?.signal);
1556
+ result.checks = summariseChecks(checks.items.map((check) => check.state), data.html_url ? `${data.html_url}/checks` : void 0);
1557
+ if (checks.warnings?.length) result.warnings = [...result.warnings ?? [], ...checks.warnings];
1558
+ } catch (error) {
1559
+ if (!(error instanceof ForgeError)) throw error;
1560
+ result.warnings = [...result.warnings ?? [], toWarning("checks_unreadable", error, ref.number)];
1561
+ }
1562
+ return result;
1563
+ }
1564
+ async function headChecks(repo, sha, signal) {
1565
+ const [runs, status] = await Promise.allSettled([Array.fromAsync(fetcher.items(`${repoPath(repo)}/commits/${sha}/check-runs`, {
1566
+ query: { per_page: 100 },
1567
+ select: (body, next) => ({
1568
+ items: body.check_runs,
1569
+ next
1570
+ }),
1571
+ signal
1572
+ }), (raw) => toCheckRun(repo, raw)), fetcher.json(`${repoPath(repo)}/commits/${sha}/status`, {
1573
+ query: { per_page: 100 },
1574
+ signal
1575
+ })]);
1576
+ if (signal?.aborted) throw signal.reason;
1577
+ if (runs.status === "rejected" && status.status === "rejected") throw runs.reason;
1578
+ return {
1579
+ items: [...runs.status === "fulfilled" ? runs.value : [], ...status.status === "fulfilled" ? status.value.data.statuses.map((raw) => toStatusCheck(repo, raw)) : []],
1580
+ warnings: [...runs.status === "rejected" ? [toWarning("checks_unreadable", runs.reason, "check-runs")] : [], ...status.status === "rejected" ? [toWarning("checks_unreadable", status.reason, "statuses")] : []]
1581
+ };
1582
+ }
1583
+ async function getMany(refs, options) {
1584
+ if (anonymous) return getManyConcurrently(refs, (ref, getOptions) => ref.kind === "discussion" ? Promise.reject(new UnsupportedOperationError("github does not read discussions without credentials", context)) : get(ref, getOptions), options);
1585
+ const results = Array.from({ length: refs.length });
1586
+ const batchable = [];
1587
+ const commits = [];
1588
+ for (const [index, ref] of refs.entries()) if (!isResolvedThread(ref) || ref.kind === "other") results[index] = {
1589
+ ok: false,
1590
+ ref,
1591
+ warning: toWarning("thread_unresolved", /* @__PURE__ */ new Error("Thread ref has no number"), ref.number)
1592
+ };
1593
+ else if (ref.kind === "commit") commits.push(index);
1594
+ else batchable.push({
1595
+ index,
1596
+ ref
1597
+ });
1598
+ const read = await getManyConcurrently(commits.map((index) => refs[index]), get, options);
1599
+ commits.forEach((index, position) => {
1600
+ results[index] = read[position];
1601
+ });
1602
+ if (!batchable.length) return results;
1603
+ const g = await import("./graphql-D7uGzZTf.mjs");
1604
+ for (let start = 0; start < batchable.length; start += GRAPHQL_BATCH) {
1605
+ const chunk = batchable.slice(start, start + GRAPHQL_BATCH);
1606
+ const { query, variables } = g.threadsBatchQuery(chunk.map((item) => item.ref));
1607
+ const url = graphqlUrl(baseUrl);
1608
+ const { data } = await fetcher.json(url, {
1609
+ method: "POST",
1610
+ json: {
1611
+ query,
1612
+ variables,
1613
+ operationName: "ThreadsBatch"
1614
+ },
1615
+ signal: options?.signal
1616
+ });
1617
+ const failure = data.data ? void 0 : graphqlError("ThreadsBatch", data.errors, url, { instance });
1618
+ chunk.forEach(({ index, ref }, offset) => {
1619
+ const node = data.data?.[`t${offset}`];
1620
+ const discussion = ref.kind === "discussion" ? node?.discussion : void 0;
1621
+ const issueOrPullRequest = ref.kind === "discussion" ? void 0 : node?.issueOrPullRequest;
1622
+ if (failure) {
1623
+ results[index] = {
1624
+ ok: false,
1625
+ ref,
1626
+ warning: toWarning(failure instanceof InsufficientScopeError ? "insufficient_scope" : "thread_unreadable", failure, ref.number)
1627
+ };
1628
+ return;
1629
+ }
1630
+ if (!discussion && !issueOrPullRequest) {
1631
+ const error = data.errors?.find((error) => error.path?.[0] === `t${offset}`);
1632
+ if (error?.type === "INSUFFICIENT_SCOPES") {
1633
+ results[index] = {
1634
+ ok: false,
1635
+ ref,
1636
+ warning: toWarning("insufficient_scope", graphqlError("ThreadsBatch", [error], url, { instance }), ref.number)
1637
+ };
1638
+ return;
1639
+ }
1640
+ results[index] = {
1641
+ ok: false,
1642
+ ref,
1643
+ warning: {
1644
+ code: "thread_unreadable",
1645
+ message: error?.message ?? "Not found",
1646
+ subject: ref.number
1647
+ }
1648
+ };
1649
+ return;
1650
+ }
1651
+ results[index] = {
1652
+ ok: true,
1653
+ ref,
1654
+ thread: discussion ? g.toDiscussionThread(ref, discussion) : g.toGraphQLThread(ref, issueOrPullRequest)
1655
+ };
1656
+ });
1657
+ }
1658
+ return results;
1659
+ }
1660
+ async function listPage(repo, query = {}) {
1661
+ const state = query.state ?? "open";
1662
+ const direction = query.direction ?? "desc";
1663
+ const createdAfter = query.createdAfter?.getTime();
1664
+ if (query.kind === "discussion") {
1665
+ if (query.assignee) return {
1666
+ items: [],
1667
+ warnings: [{
1668
+ code: "filter_unsupported",
1669
+ message: "Discussions have no assignees"
1670
+ }]
1671
+ };
1672
+ const warnings = !query.cursor && query.sort === "comments" ? [{
1673
+ code: "sort_unsupported",
1674
+ message: "Discussions cannot be sorted by comments; sorted by update time"
1675
+ }] : [];
1676
+ const data = await graphql("DISCUSSIONS_LIST", {
1677
+ owner: repo.owner,
1678
+ name: repo.name,
1679
+ first: Math.min(query.perPage ?? 50, 100),
1680
+ after: query.cursor?.token ?? null,
1681
+ field: query.sort === "created" || !query.sort ? "CREATED_AT" : "UPDATED_AT",
1682
+ direction: direction.toUpperCase()
1683
+ }, query);
1684
+ const { toDiscussionThread } = await import("./graphql-D7uGzZTf.mjs");
1685
+ const discussions = data.repository?.discussions;
1686
+ return {
1687
+ items: (discussions?.nodes ?? []).map((node) => toDiscussionThread({
1688
+ forge: FORGE,
1689
+ instance,
1690
+ repo,
1691
+ kind: "discussion",
1692
+ number: String(node.number)
1693
+ }, node)).filter((thread) => (state === "all" || thread.state === state) && (!query.author || thread.author?.login === query.author) && (!query.labels?.length || query.labels.every((label) => thread.labels.some((item) => item.name === label))) && (!query.since || (thread.updatedAt?.getTime() ?? 0) >= query.since.getTime()) && (createdAfter === void 0 || (thread.createdAt?.getTime() ?? 0) >= createdAfter)),
1694
+ cursor: discussions?.pageInfo.hasNextPage && discussions.pageInfo.endCursor ? { token: discussions.pageInfo.endCursor } : void 0,
1695
+ ...warnings.length ? { warnings } : {}
1696
+ };
1697
+ }
1698
+ if (state === "merged") {
1699
+ const page = await searchThreadsPage({
1700
+ repo,
1701
+ kind: "pull_request",
1702
+ labels: query.labels,
1703
+ author: query.author,
1704
+ assignee: query.assignee,
1705
+ involves: query.involves,
1706
+ since: query.since,
1707
+ queryRaw: `is:merged${query.createdAfter ? ` created:>=${query.createdAfter.toISOString()}` : ""}`,
1708
+ sort: query.sort ?? "created",
1709
+ direction,
1710
+ perPage: query.perPage,
1711
+ cursor: query.cursor,
1712
+ signal: query.signal
1713
+ });
1714
+ await withPullChecks(page, query);
1715
+ return page;
1716
+ }
1717
+ const pullsOnly = query.kind === "pull_request" && !query.labels?.length && !query.author && !query.assignee && !query.since && query.sort !== "comments";
1718
+ const result = await fetcher.page(`${repoPath(repo)}/${pullsOnly ? "pulls" : "issues"}`, {
1719
+ query: {
1720
+ state,
1721
+ labels: query.labels?.join(","),
1722
+ creator: query.author,
1723
+ assignee: query.assignee,
1724
+ since: query.since?.toISOString(),
1725
+ sort: query.sort ?? "created",
1726
+ direction,
1727
+ per_page: query.perPage
1728
+ },
1729
+ cursor: query.cursor,
1730
+ signal: query.signal
1731
+ });
1732
+ const older = (raw) => createdAfter !== void 0 && (Date.parse(raw.created_at ?? "") || 0) < createdAfter;
1733
+ const page = toPage(result, (raw) => {
1734
+ const isPull = Boolean(raw.pull_request || raw.head);
1735
+ if (query.kind === "issue" && isPull || query.kind === "pull_request" && !isPull || older(raw)) return;
1736
+ return toThread({
1737
+ forge: FORGE,
1738
+ instance,
1739
+ repo,
1740
+ kind: isPull ? "pull_request" : "issue",
1741
+ number: String(raw.number)
1742
+ }, raw);
1743
+ });
1744
+ const ordered = (query.sort ?? "created") === "created" && direction === "desc";
1745
+ await withPullChecks(page, query);
1746
+ return ordered && (result.data ?? []).some(older) ? {
1747
+ ...page,
1748
+ cursor: void 0
1749
+ } : page;
1750
+ }
1751
+ /** Fills `checks`, and a missing `commentCount`, on every pull in the page with one batched read. */
1752
+ async function withPullChecks(page, options) {
1753
+ const pulls = page.items.filter((thread) => thread.kind === "pull_request");
1754
+ if (!pulls.length || anonymous) return;
1755
+ try {
1756
+ const results = await getMany(pulls.map((thread) => thread.ref), options);
1757
+ for (const [index, result] of results.entries()) if (result.ok) {
1758
+ pulls[index].checks = result.thread.checks ?? pulls[index].checks;
1759
+ pulls[index].commentCount ??= result.thread.commentCount;
1760
+ }
1761
+ } catch (error) {
1762
+ if (!(error instanceof ForgeError)) throw error;
1763
+ page.warnings = [...page.warnings ?? [], toWarning("checks_unreadable", error)];
1764
+ }
1765
+ }
1766
+ async function commentsPage(thread, listOptions = {}) {
1767
+ const ref = requireThread(thread, context);
1768
+ if (ref.kind === "discussion") {
1769
+ const [page, { toDiscussionComment }] = await Promise.all([discussionCommentPage(ref, listOptions), import("./graphql-D7uGzZTf.mjs")]);
1770
+ return {
1771
+ ...page,
1772
+ items: page.items.map(({ comment }) => toDiscussionComment(ref, comment))
1773
+ };
1774
+ }
1775
+ const path = ref.kind === "commit" ? `${repoPath(ref.repo)}/commits/${encodeURIComponent(ref.number)}/comments` : `${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/comments`;
1776
+ return list(path, listOptions, (raw) => toComment(ref, raw));
1777
+ }
1778
+ function commentPath(ref) {
1779
+ return ref.thread.kind === "commit" ? `${repoPath(ref.thread.repo)}/comments/${encodeURIComponent(ref.id)}` : `${repoPath(ref.thread.repo)}/issues/comments/${encodeURIComponent(ref.id)}`;
1780
+ }
1781
+ async function setState(thread, state, options_ = {}) {
1782
+ const ref = requireThread(thread, context);
1783
+ if (ref.kind === "commit") throw new UnsupportedOperationError("Commits have no open or closed state", context);
1784
+ if (ref.kind === "discussion") {
1785
+ const reason = options_.reasonRaw ?? (options_.reason && DISCUSSION_CLOSE_REASONS[options_.reason]);
1786
+ if (state === "closed" && options_.reason && !reason) throw new UnsupportedOperationError(`GitHub discussions have no ${options_.reason} close reason; pass reasonRaw: 'outdated'`, context);
1787
+ const id = await discussionId(ref, options_);
1788
+ await (state === "closed" ? graphql("CLOSE_DISCUSSION", {
1789
+ id,
1790
+ reason: reason?.toUpperCase() ?? null
1791
+ }, options_) : graphql("REOPEN_DISCUSSION", { id }, options_));
1792
+ return;
1793
+ }
1794
+ const reason = options_.reasonRaw ?? options_.reason;
1795
+ await fetcher.raw(threadPath(ref), {
1796
+ method: "PATCH",
1797
+ json: ref.kind === "issue" && state === "closed" && reason ? {
1798
+ state,
1799
+ state_reason: reason
1800
+ } : { state },
1801
+ signal: options_.signal
1802
+ });
1803
+ }
1804
+ async function merge(thread, options_ = {}, hooks = {}) {
1805
+ const ref = requireThread(thread, context);
1806
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only pull requests can be merged", context);
1807
+ if (options_.whenChecksPass) throw new UnsupportedOperationError("GitHub auto-merge is not available through the REST API", context);
1808
+ if (options_.method === "rebase_merge" || options_.method === "fast_forward_only") throw new UnsupportedOperationError(`GitHub does not support the ${options_.method} merge method`, context);
1809
+ let method = options_.method;
1810
+ if (!method) {
1811
+ const { data: repo } = await fetcher.json(repoPath(ref.repo), { signal: options_.signal });
1812
+ method = soleMergeMethod({
1813
+ merge: repo.allow_merge_commit,
1814
+ squash: repo.allow_squash_merge,
1815
+ rebase: repo.allow_rebase_merge
1816
+ }, context);
1817
+ }
1818
+ await hooks.beforeMerge?.();
1819
+ await directMerge(ref, method, options_);
1820
+ }
1821
+ async function directMerge(ref, method, mergeOptions) {
1822
+ const path = `${threadPath(ref)}/merge`;
1823
+ const { sha, title, message } = mergeOptions;
1824
+ const body = {
1825
+ merge_method: method,
1826
+ sha,
1827
+ commit_title: title,
1828
+ commit_message: message
1829
+ };
1830
+ const timeout = options.timeout ?? 3e4;
1831
+ const budget = new AbortController();
1832
+ const signal = mergeOptions.signal ? anySignal([budget.signal, mergeOptions.signal]) : budget.signal;
1833
+ const timer = setTimeout(() => budget.abort(new DOMException("Merge timed out", "TimeoutError")), timeout);
1834
+ const timedOut = (url, requestMethod, cause, uuid) => new ForgeTimeoutError(uuid ? `Merge request ${uuid} for pull request ${ref.number} was still pending after ${timeout}ms; GitHub may still complete it` : `Merge of pull request ${ref.number} got no response within ${timeout}ms; GitHub may still complete it`, timeout, {
1835
+ ...context,
1836
+ url,
1837
+ method: requestMethod
1838
+ }, { cause });
1839
+ let url = fetcher.resolve(`${path}-async`);
1840
+ let requestMethod = "PUT";
1841
+ let uuid;
1842
+ try {
1843
+ let state;
1844
+ let status;
1845
+ try {
1846
+ const { data, response } = await fetcher.json(`${path}-async`, {
1847
+ method: "PUT",
1848
+ json: {
1849
+ ...body,
1850
+ merge_action: "direct_merge"
1851
+ },
1852
+ signal
1853
+ });
1854
+ state = data;
1855
+ status = response.status;
1856
+ } catch (error) {
1857
+ if (budget.signal.aborted) throw error;
1858
+ if (enterprise && error instanceof NotFoundError) {
1859
+ url = fetcher.resolve(path);
1860
+ await fetcher.raw(path, {
1861
+ method: "PUT",
1862
+ json: body,
1863
+ mapError: toMergeError,
1864
+ signal
1865
+ });
1866
+ return;
1867
+ }
1868
+ state = pendingMergeFrom(error, method, sha, title, message);
1869
+ status = 409;
1870
+ }
1871
+ for (let attempt = 0; state.status === "pending"; attempt++) {
1872
+ uuid = state.details?.uuid;
1873
+ if (!uuid) throw new ForgeApiError("GitHub reported a pending merge without a request id", status, JSON.stringify(state).slice(0, 512), {
1874
+ ...context,
1875
+ url,
1876
+ method: requestMethod
1877
+ });
1878
+ const poll = `${path}-async/${encodeURIComponent(uuid)}`;
1879
+ url = fetcher.resolve(poll);
1880
+ requestMethod = "GET";
1881
+ await sleep(POLL_DELAYS[Math.min(attempt, POLL_DELAYS.length - 1)], signal);
1882
+ const { data, response } = await fetcher.json(poll, { signal });
1883
+ state = data;
1884
+ status = response.status;
1885
+ }
1886
+ const errorContext = {
1887
+ ...context,
1888
+ url,
1889
+ method: requestMethod
1890
+ };
1891
+ const excerpt = JSON.stringify(state).slice(0, 512);
1892
+ switch (state.status) {
1893
+ case "merged": return;
1894
+ case "enqueued": throw new MergeBlockedError("Pull request was added to a merge queue and has not merged", status, excerpt, errorContext);
1895
+ case "failed": throw new MergeBlockedError(state.details?.message || "GitHub could not merge the pull request", status, excerpt, errorContext);
1896
+ default: throw new ForgeApiError(`GitHub reported an unknown merge status ${JSON.stringify(state.status)}`, status, excerpt, errorContext);
1897
+ }
1898
+ } catch (error) {
1899
+ throw budget.signal.aborted ? timedOut(url, requestMethod, error, uuid) : error;
1900
+ } finally {
1901
+ clearTimeout(timer);
1902
+ }
1903
+ }
1904
+ function pendingMergeFrom(error, method, sha, title, message) {
1905
+ if (!(error instanceof ForgeApiError)) throw error;
1906
+ if (isPlainApiError(error, 400)) throw new MergeBlockedError("Pull request cannot be merged in its current state", 400, error.body, {
1907
+ ...context,
1908
+ url: error.url,
1909
+ method: error.method
1910
+ }, { cause: error });
1911
+ if (!isPlainApiError(error, 409)) throw toMergeError(error);
1912
+ let pending;
1913
+ try {
1914
+ pending = JSON.parse(error.body);
1915
+ } catch {}
1916
+ const details = pending?.details;
1917
+ if (!details?.uuid) throw toMergeError(error);
1918
+ if (title !== void 0 || message !== void 0 || details.merge_action !== "direct_merge" || details.merge_method !== method || details.bypass_rules || sha !== void 0 && details.expected_head_sha !== sha) throw new MergeBlockedError("Another merge request is pending whose options do not match or cannot be verified", 409, error.body, {
1919
+ ...context,
1920
+ url: error.url,
1921
+ method: error.method
1922
+ }, { cause: error });
1923
+ return {
1924
+ status: "pending",
1925
+ details
1926
+ };
1927
+ }
1928
+ function isPlainApiError(error, status) {
1929
+ return error instanceof ForgeApiError && error.constructor === ForgeApiError && error.status === status;
1930
+ }
1931
+ const REVIEW_EVENTS = {
1932
+ approve: "APPROVE",
1933
+ request_changes: "REQUEST_CHANGES",
1934
+ comment: "COMMENT"
1935
+ };
1936
+ function requirePull(thread, action) {
1937
+ const ref = requireThread(thread, context);
1938
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError(`Only pull requests can be ${action}`, context);
1939
+ return ref;
1940
+ }
1941
+ function reviewCommentInput(comments) {
1942
+ return comments?.map((comment) => ({
1943
+ path: comment.path,
1944
+ body: comment.body,
1945
+ line: comment.line,
1946
+ start_line: comment.startLine,
1947
+ side: comment.side?.toUpperCase()
1948
+ }));
1949
+ }
1950
+ /** Resolvable conversations, by the id of each comment in them; empty when GraphQL is unreachable. */
1951
+ async function reviewThreadsByComment(ref, options) {
1952
+ const byComment = /* @__PURE__ */ new Map();
1953
+ if (anonymous) return byComment;
1954
+ let data;
1955
+ try {
1956
+ data = await graphql("REVIEW_THREADS", {
1957
+ owner: ref.repo.owner,
1958
+ name: ref.repo.name,
1959
+ number: Number(ref.number)
1960
+ }, options);
1961
+ } catch (error) {
1962
+ if (error instanceof ForgeError) return byComment;
1963
+ throw error;
1964
+ }
1965
+ for (const node of data.repository?.pullRequest?.reviewThreads.nodes ?? []) for (const comment of node.comments.nodes) if (comment.databaseId !== null) byComment.set(String(comment.databaseId), {
1966
+ id: node.id,
1967
+ resolved: node.isResolved
1968
+ });
1969
+ return byComment;
1970
+ }
1971
+ /** Review comments and conversations read for a pull's first page of reviews, reused by its later pages. */
1972
+ const reviewContext = /* @__PURE__ */ new Map();
1973
+ async function reviewsPage(thread, listOptions = {}) {
1974
+ const ref = requirePull(thread, "reviewed");
1975
+ const result = await fetcher.page(`${threadPath(ref)}/reviews`, {
1976
+ query: { per_page: listOptions.perPage },
1977
+ cursor: listOptions.cursor,
1978
+ signal: listOptions.signal
1979
+ });
1980
+ const now = Date.now();
1981
+ for (const [path, entry] of reviewContext) if (now - entry.at >= REVIEW_CONTEXT_TTL) reviewContext.delete(path);
1982
+ const key = threadPath(ref);
1983
+ const kept = listOptions.cursor && reviewContext.get(key) || {
1984
+ at: now,
1985
+ read: Promise.all([Array.fromAsync(fetcher.items(`${key}/comments`, {
1986
+ query: { per_page: 100 },
1987
+ signal: listOptions.signal
1988
+ })), reviewThreadsByComment(ref, listOptions)])
1989
+ };
1990
+ reviewContext.delete(key);
1991
+ const [comments, byComment] = await kept.read;
1992
+ if (result.cursor) reviewContext.set(key, kept);
1993
+ return toPage(result, (raw) => toReview(ref, raw, comments.filter((comment) => String(comment.pull_request_review_id ?? "") === String(raw.id)).map((comment) => toReviewComment(ref, comment, byComment.get(String(comment.id))))));
1994
+ }
1995
+ async function createReview(thread, input, options) {
1996
+ const ref = requirePull(thread, "reviewed");
1997
+ const { data } = await fetcher.json(`${threadPath(ref)}/reviews`, {
1998
+ method: "POST",
1999
+ json: {
2000
+ event: input.event ? REVIEW_EVENTS[input.event] : void 0,
2001
+ body: input.body,
2002
+ comments: reviewCommentInput(input.comments)
2003
+ },
2004
+ signal: options?.signal
2005
+ });
2006
+ return toReview(ref, data, []);
2007
+ }
2008
+ async function subscriptionNode(thread, options) {
2009
+ const ref = requireThread(thread, context);
2010
+ if (ref.kind !== "issue" && ref.kind !== "pull_request" && ref.kind !== "discussion") throw new UnsupportedOperationError(`GitHub has no subscription for a ${ref.kind}`, context);
2011
+ const data = await graphql("SUBSCRIPTION_STATE", {
2012
+ owner: ref.repo.owner,
2013
+ name: ref.repo.name,
2014
+ number: Number(ref.number),
2015
+ discussion: ref.kind === "discussion"
2016
+ }, options);
2017
+ const node = ref.kind === "discussion" ? data.repository?.discussion : data.repository?.issueOrPullRequest;
2018
+ if (!node) throw new NotFoundError(`Thread ${ref.number} not found`, 404, "", context);
2019
+ return {
2020
+ id: node.id,
2021
+ state: node.viewerSubscription
2022
+ };
2023
+ }
2024
+ async function setSubscription(thread, state, options) {
2025
+ const { id } = await subscriptionNode(thread, options);
2026
+ await graphql("UPDATE_SUBSCRIPTION", {
2027
+ id,
2028
+ state
2029
+ }, options);
2030
+ }
2031
+ async function reposPage(repoFetcher, path, wrapped, listOptions = {}) {
2032
+ return toPage(await repoFetcher.page(path, {
2033
+ query: { per_page: listOptions.perPage },
2034
+ cursor: listOptions.cursor,
2035
+ signal: listOptions.signal,
2036
+ select: wrapped ? (body, next) => ({
2037
+ items: body.repositories,
2038
+ next
2039
+ }) : void 0
2040
+ }), (raw) => toRepo(instance, raw));
2041
+ }
2042
+ /** Repository hooks, or organisation hooks for a namespace ref. */
2043
+ function hooksPath(target) {
2044
+ return isNamespaceRef(target) ? `/orgs/${encodeURIComponent(target.owner)}/hooks` : `${repoPath(target)}/hooks`;
2045
+ }
2046
+ function hookConfig(input) {
2047
+ return {
2048
+ ...input.url ? { url: input.url } : {},
2049
+ content_type: input.contentType ?? "json",
2050
+ ...input.secret ? { secret: input.secret } : {}
2051
+ };
2052
+ }
2053
+ /**
2054
+ * The hook endpoints respond with `404` when the credential lacks
2055
+ * `admin:repo_hook` or `admin:org_hook`, so the status alone is misleading.
2056
+ */
2057
+ function scopeIs404(error) {
2058
+ if (error instanceof NotFoundError) return new ForbiddenError("GitHub responded with 404 for a webhook endpoint, which it also does when the credential lacks admin:repo_hook or admin:org_hook", 404, error.body, "resource_protected", {
2059
+ forge: error.forge,
2060
+ instance: error.instance,
2061
+ url: error.url,
2062
+ method: error.method,
2063
+ reasonRaw: "hook_scope_404"
2064
+ }, { cause: error });
2065
+ return error;
2066
+ }
2067
+ /** GitHub responds with `422` to a change of the tag, target or draft state of an immutable release. */
2068
+ function immutableReleaseIs422(error) {
2069
+ if (!(error instanceof ForgeApiError) || error.constructor !== ForgeApiError || error.status !== 422) return error;
2070
+ const reasonRaw = error.body.match(/\w+ cannot be changed when release is immutable/)?.[0];
2071
+ if (!reasonRaw) return error;
2072
+ return new ForbiddenError("GitHub refused the change because the release is immutable", error.status, error.body, "immutable_release", {
2073
+ forge: error.forge,
2074
+ instance: error.instance,
2075
+ url: error.url,
2076
+ method: error.method,
2077
+ reasonRaw
2078
+ }, { cause: error });
2079
+ }
2080
+ /** GitHub search qualifiers, in the order the docs list them. */
2081
+ function searchQualifiers(query) {
2082
+ const qualifiers = [];
2083
+ const advanced = !query.kind;
2084
+ /** In advanced search a bare `OR` binds looser than the other qualifiers, so caller syntax is grouped. */
2085
+ const group = (value) => advanced ? `(${value})` : value;
2086
+ const text = advanced && query.text ? advancedFreeText(query.text) : query.text;
2087
+ if (text) qualifiers.push(group(text));
2088
+ if (query.repo) qualifiers.push(`repo:${query.repo.owner}/${query.repo.name}`);
2089
+ qualifiers.push(query.kind ? query.kind === "pull_request" ? "is:pr" : "is:issue" : "(is:issue OR is:pr)");
2090
+ if (query.state && query.state !== "all") qualifiers.push(`state:${query.state}`);
2091
+ for (const [name, value] of [
2092
+ ["author", query.author],
2093
+ ["involves", query.involves],
2094
+ ["assignee", query.assignee]
2095
+ ]) if (value) qualifiers.push(`${name}:${value}`);
2096
+ for (const label of query.labels ?? []) qualifiers.push(`label:"${label}"`);
2097
+ if (query.since) qualifiers.push(`updated:>=${query.since.toISOString()}`);
2098
+ if (query.queryRaw) qualifiers.push(group(query.queryRaw));
2099
+ return qualifiers;
2100
+ }
2101
+ /**
2102
+ * GitHub requires a kind in issue search; naming both needs advanced search.
2103
+ * GitHub Enterprise Server may predate it, and GitHub App user access tokens
2104
+ * can't search both kinds at once, so those read issues, then pull requests.
2105
+ */
2106
+ async function searchThreadsPage(query) {
2107
+ if (!query.kind && (enterprise || auth.type === "token" && (await resolveToken(auth)).startsWith("ghu_"))) return phased(["issue", "pull_request"].map((kind) => (cursor) => searchIssuesPage({
2108
+ ...query,
2109
+ kind,
2110
+ cursor
2111
+ })), query.cursor);
2112
+ return searchIssuesPage(query);
2113
+ }
2114
+ async function searchIssuesPage(query) {
2115
+ const result = await fetcher.page("/search/issues", {
2116
+ query: {
2117
+ q: searchQualifiers(query).join(" "),
2118
+ advanced_search: query.kind ? void 0 : "true",
2119
+ sort: query.sort && query.sort !== "relevance" ? query.sort : void 0,
2120
+ order: query.direction,
2121
+ per_page: query.perPage
2122
+ },
2123
+ cursor: query.cursor,
2124
+ signal: query.signal,
2125
+ select: (body, next) => ({
2126
+ items: body.items,
2127
+ next
2128
+ })
2129
+ });
2130
+ return toPage(result, (raw) => {
2131
+ const repo = query.repo ?? repoRefFromApiUrl(instance, raw.repository_url);
2132
+ if (!repo) return;
2133
+ const isPull = Boolean(raw.pull_request);
2134
+ return toThread({
2135
+ forge: FORGE,
2136
+ instance,
2137
+ repo,
2138
+ kind: isPull ? "pull_request" : "issue",
2139
+ number: String(raw.number)
2140
+ }, raw);
2141
+ });
2142
+ }
2143
+ function milestonesPage(repo, listOptions = {}) {
2144
+ return list(`${repoPath(repo)}/milestones`, listOptions, (raw) => toMilestone(raw), { query: { state: listOptions.state ?? "open" } });
2145
+ }
2146
+ const REPO_SEARCH_SORTS = {
2147
+ updated: "updated",
2148
+ stars: "stars"
2149
+ };
2150
+ async function searchReposPage(query) {
2151
+ const qualifiers = [
2152
+ query.text,
2153
+ query.owner && `user:${query.owner}`,
2154
+ query.language && `language:${query.language}`,
2155
+ query.queryRaw
2156
+ ].filter(Boolean);
2157
+ const warnings = query.sort === "created" ? [{
2158
+ code: "sort_unsupported",
2159
+ message: "GitHub repository search cannot sort by creation time; sorted by relevance"
2160
+ }] : [];
2161
+ const result = await fetcher.page("/search/repositories", {
2162
+ query: {
2163
+ q: qualifiers.join(" "),
2164
+ sort: query.sort ? REPO_SEARCH_SORTS[query.sort] : void 0,
2165
+ order: query.direction,
2166
+ per_page: query.perPage
2167
+ },
2168
+ cursor: query.cursor,
2169
+ signal: query.signal,
2170
+ select: (body, next) => ({
2171
+ items: body.items,
2172
+ next
2173
+ })
2174
+ });
2175
+ return toPage(result, (raw) => toRepo(instance, raw), warnings);
2176
+ }
2177
+ const COMMIT_SEARCH_SORTS = {
2178
+ author_date: "author-date",
2179
+ committer_date: "committer-date"
2180
+ };
2181
+ async function searchCommitsPage(query) {
2182
+ const qualifiers = [
2183
+ query.text,
2184
+ query.repo && `repo:${query.repo.owner}/${query.repo.name}`,
2185
+ query.author && `author:${query.author}`,
2186
+ query.committer && `committer:${query.committer}`,
2187
+ query.since && `author-date:>=${query.since.toISOString()}`,
2188
+ query.until && `author-date:<=${query.until.toISOString()}`,
2189
+ query.queryRaw
2190
+ ].filter(Boolean);
2191
+ const result = await fetcher.page("/search/commits", {
2192
+ query: {
2193
+ q: qualifiers.join(" "),
2194
+ sort: query.sort && COMMIT_SEARCH_SORTS[query.sort],
2195
+ order: query.direction,
2196
+ per_page: query.perPage
2197
+ },
2198
+ cursor: query.cursor,
2199
+ signal: query.signal,
2200
+ select: (body, next) => ({
2201
+ items: body.items,
2202
+ next
2203
+ })
2204
+ });
2205
+ return toPage(result, (raw) => {
2206
+ const repo = query.repo ?? (raw.repository && toRepoRef(instance, raw.repository));
2207
+ return repo && toCommit(repo, raw);
2208
+ });
2209
+ }
2210
+ /** The excerpts only come with the text match media type. */
2211
+ async function searchCodePage(query) {
2212
+ const qualifiers = [
2213
+ query.text,
2214
+ query.repo ? `repo:${query.repo.owner}/${query.repo.name}` : query.owner && `user:${query.owner}`,
2215
+ query.queryRaw
2216
+ ].filter(Boolean);
2217
+ let incomplete = false;
2218
+ const result = await fetcher.page("/search/code", {
2219
+ query: {
2220
+ q: qualifiers.join(" "),
2221
+ per_page: query.perPage
2222
+ },
2223
+ headers: { accept: "application/vnd.github.text-match+json" },
2224
+ cursor: query.cursor,
2225
+ signal: query.signal,
2226
+ select: (body, next) => {
2227
+ const { items, incomplete_results } = body;
2228
+ incomplete = incomplete_results === true;
2229
+ return {
2230
+ items,
2231
+ next
2232
+ };
2233
+ }
2234
+ });
2235
+ return toPage(result, (raw) => ({
2236
+ repo: query.repo ?? toRepoRef(instance, raw.repository),
2237
+ path: raw.path,
2238
+ ref: new URL(raw.url).searchParams.get("ref") ?? void 0,
2239
+ fragments: (raw.text_matches ?? []).filter((match) => match.property === "content").map((match) => ({ text: match.fragment })),
2240
+ url: raw.html_url,
2241
+ raw
2242
+ }), incomplete ? [{
2243
+ code: "search_incomplete",
2244
+ message: "GitHub stopped the search before it finished; the page may be missing matches"
2245
+ }] : []);
2246
+ }
2247
+ function createInstallationsApi(app) {
2248
+ const appFetcher = createFetcher({
2249
+ baseUrl,
2250
+ authHeaders: async () => ({ authorization: `Bearer ${await app.appJwt()}` })
2251
+ });
2252
+ const auth = options.auth;
2253
+ function toInstallation(raw) {
2254
+ return {
2255
+ forge: FORGE,
2256
+ instance,
2257
+ id: String(raw.id),
2258
+ account: toActor(instance, raw.account),
2259
+ targetTypeRaw: raw.target_type,
2260
+ repositorySelectionRaw: raw.repository_selection,
2261
+ raw
2262
+ };
2263
+ }
2264
+ function provider(installation) {
2265
+ return derive({
2266
+ ...options,
2267
+ auth: {
2268
+ ...auth,
2269
+ installationId: installationId(installation)
2270
+ }
2271
+ }, app);
2272
+ }
2273
+ async function listPage(listOptions = {}) {
2274
+ return toPage(await appFetcher.page("/app/installations", {
2275
+ query: { per_page: listOptions.perPage },
2276
+ cursor: listOptions.cursor,
2277
+ signal: listOptions.signal
2278
+ }), toInstallation);
2279
+ }
2280
+ return {
2281
+ listPage,
2282
+ async get(installation, options) {
2283
+ return toInstallation((await appFetcher.json(`/app/installations/${installationId(installation)}`, { signal: options?.signal })).data);
2284
+ },
2285
+ token: (installation, options) => app.installationTokenDetails(installationId(installation), options),
2286
+ reposPage(installation, listOptions) {
2287
+ const id = installationId(installation);
2288
+ return reposPage(createFetcher({
2289
+ baseUrl,
2290
+ authHeaders: async () => ({ authorization: `Bearer ${await app.installationToken(id)}` })
2291
+ }), "/installation/repositories", true, listOptions);
2292
+ },
2293
+ provider,
2294
+ providers() {
2295
+ return forgeIterable(async function* () {
2296
+ for await (const installation of iteratePages(listPage)) yield {
2297
+ installation,
2298
+ provider: provider(installation)
2299
+ };
2300
+ });
2301
+ }
2302
+ };
2303
+ }
2304
+ async function readFile(repo, path, fileOptions = {}) {
2305
+ const { data } = await fetcher.json(`${repoPath(repo)}/contents/${path.split("/").map(encodeURIComponent).join("/")}`, {
2306
+ query: { ref: fileOptions.ref },
2307
+ signal: fileOptions.signal
2308
+ });
2309
+ const file = {
2310
+ path: data.path,
2311
+ sha: data.sha,
2312
+ size: data.size,
2313
+ url: data.html_url ?? void 0
2314
+ };
2315
+ const content = data.encoding === "base64" && data.content !== void 0 ? data.content : (await fetcher.json(`${repoPath(repo)}/git/blobs/${data.sha}`, { signal: fileOptions.signal })).data.content;
2316
+ return toFileContent(fromBase64(content), file, fileOptions, context);
2317
+ }
2318
+ async function treePage(repo, treeOptions = {}) {
2319
+ const ref = treeOptions.ref ?? "HEAD";
2320
+ const target = treeOptions.path ? `${ref}:${treeOptions.path.replace(/^\/|\/$/g, "")}` : ref;
2321
+ const { data, response } = await fetcher.json(`${repoPath(repo)}/git/trees/${encodeURIComponent(target)}`, {
2322
+ query: treeOptions.recursive ? { recursive: "1" } : {},
2323
+ signal: treeOptions.signal
2324
+ });
2325
+ const warnings = data.truncated ? [{
2326
+ code: "tree_truncated",
2327
+ message: "GitHub truncated the tree; read it directory by directory instead",
2328
+ subject: target
2329
+ }] : [];
2330
+ return toPage({
2331
+ data: data.tree,
2332
+ response,
2333
+ notModified: false
2334
+ }, toTreeEntry, warnings);
2335
+ }
2336
+ async function commitsPage(repo, query = {}) {
2337
+ return list(`${repoPath(repo)}/commits`, query, (raw) => toCommit(repo, raw), { query: {
2338
+ sha: query.ref,
2339
+ path: query.path,
2340
+ author: query.author,
2341
+ since: query.since?.toISOString(),
2342
+ until: query.until?.toISOString()
2343
+ } });
2344
+ }
2345
+ async function releasesPage(repo, listOptions = {}) {
2346
+ return list(`${repoPath(repo)}/releases`, listOptions, (raw) => toRelease(repo, raw));
2347
+ }
2348
+ async function writeRelease(repo, path, method, input, options) {
2349
+ const { data } = await fetcher.json(path, {
2350
+ method,
2351
+ json: {
2352
+ tag_name: input.tag,
2353
+ target_commitish: input.target,
2354
+ name: input.name,
2355
+ body: input.body,
2356
+ draft: input.draft,
2357
+ prerelease: input.prerelease
2358
+ },
2359
+ mapError: immutableReleaseIs422,
2360
+ signal: options?.signal
2361
+ });
2362
+ return toRelease(repo, data);
2363
+ }
2364
+ const ALERT_SOURCES = {
2365
+ dependency: {
2366
+ path: "dependabot/alerts",
2367
+ states: {
2368
+ open: "open",
2369
+ closed: "fixed,dismissed,auto_dismissed"
2370
+ },
2371
+ map: toDependabotAlert
2372
+ },
2373
+ code_scanning: {
2374
+ path: "code-scanning/alerts",
2375
+ states: {
2376
+ open: "open",
2377
+ closed: "closed"
2378
+ },
2379
+ map: toCodeScanningAlert
2380
+ },
2381
+ secret: {
2382
+ path: "secret-scanning/alerts",
2383
+ states: {
2384
+ open: "open",
2385
+ closed: "resolved"
2386
+ },
2387
+ map: toSecretScanningAlert
2388
+ }
2389
+ };
2390
+ async function alertsPage(repo, listOptions = {}) {
2391
+ const kinds = listOptions.kind ? [listOptions.kind] : Object.keys(ALERT_SOURCES);
2392
+ return phased(kinds.map((kind) => async (cursor) => {
2393
+ const source = ALERT_SOURCES[kind];
2394
+ const state = listOptions.state ?? "open";
2395
+ try {
2396
+ const result = await fetcher.page(`${repoPath(repo)}/${source.path}`, {
2397
+ query: {
2398
+ state: state === "all" ? void 0 : source.states[state],
2399
+ per_page: listOptions.perPage
2400
+ },
2401
+ cursor,
2402
+ signal: listOptions.signal
2403
+ });
2404
+ return toPage(result, (raw) => source.map(repo, raw));
2405
+ } catch (error) {
2406
+ if (!listOptions.kind && (error instanceof InsufficientScopeError || error instanceof NotFoundError)) return {
2407
+ items: [],
2408
+ warnings: [toWarning(error instanceof InsufficientScopeError ? "insufficient_scope" : "alerts_unavailable", error, kind)]
2409
+ };
2410
+ throw error;
2411
+ }
2412
+ }), listOptions.cursor);
2413
+ }
2414
+ /** The GraphQL node id of an issue, pull request or discussion. */
2415
+ async function nodeId(thread, options) {
2416
+ const ref = requireThread(thread, context);
2417
+ return ref.externalId ?? (await subscriptionNode(ref, options)).id;
2418
+ }
2419
+ function reactionPath(target) {
2420
+ if ("thread" in target) return target.thread.kind === "commit" ? `${repoPath(target.thread.repo)}/comments/${encodeURIComponent(target.id)}/reactions` : `${repoPath(target.thread.repo)}/issues/comments/${encodeURIComponent(target.id)}/reactions`;
2421
+ const ref = requireIssueOrPull(target, context, "react to");
2422
+ return `${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/reactions`;
2423
+ }
2424
+ /** The authenticated login, read once and reused; reactions are deleted by id, so removing one needs it. */
2425
+ const viewerLogin = memo(async () => (await fetcher.json("/user")).data.login);
2426
+ async function reactionsPage(target, listOptions = {}) {
2427
+ const thread = "thread" in target ? target.thread : target;
2428
+ if (thread.kind === "discussion") {
2429
+ const id = "thread" in target ? target.id : await nodeId(thread, listOptions);
2430
+ const [data, { toGraphQLActor, GRAPHQL_REACTIONS }] = await Promise.all([graphql("NODE_REACTIONS", {
2431
+ id,
2432
+ after: listOptions.cursor?.token ?? null
2433
+ }, listOptions), import("./graphql-D7uGzZTf.mjs")]);
2434
+ const reactions = data.node?.reactions;
2435
+ return {
2436
+ items: (reactions?.nodes ?? []).flatMap((raw) => {
2437
+ const actor = toGraphQLActor(instance, raw.user);
2438
+ return actor ? [{
2439
+ content: reactionContent(GRAPHQL_REACTIONS[raw.content] ?? raw.content),
2440
+ contentRaw: raw.content,
2441
+ actor,
2442
+ createdAt: toDate(raw.createdAt),
2443
+ raw
2444
+ }] : [];
2445
+ }),
2446
+ cursor: reactions?.pageInfo.hasNextPage && reactions.pageInfo.endCursor ? { token: reactions.pageInfo.endCursor } : void 0
2447
+ };
2448
+ }
2449
+ const result = await fetcher.page(reactionPath(target), {
2450
+ query: { per_page: listOptions.perPage },
2451
+ cursor: listOptions.cursor,
2452
+ signal: listOptions.signal
2453
+ });
2454
+ return toPage(result, (raw) => {
2455
+ const actor = toActor(instance, raw.user);
2456
+ return actor && {
2457
+ content: reactionContent(raw.content),
2458
+ contentRaw: raw.content,
2459
+ actor,
2460
+ createdAt: toDate(raw.created_at),
2461
+ raw
2462
+ };
2463
+ });
2464
+ }
2465
+ async function react(target, reaction, options) {
2466
+ const thread = "thread" in target ? target.thread : target;
2467
+ if (thread.kind === "discussion") {
2468
+ await graphql("ADD_REACTION", {
2469
+ id: "thread" in target ? target.id : await nodeId(thread, options),
2470
+ content: (await import("./graphql-D7uGzZTf.mjs")).REACTION_CONTENT[reaction]
2471
+ }, options);
2472
+ return;
2473
+ }
2474
+ await fetcher.raw(reactionPath(target), {
2475
+ method: "POST",
2476
+ json: { content: reaction },
2477
+ signal: options?.signal
2478
+ });
2479
+ }
2480
+ async function unreact(target, reaction, options) {
2481
+ const thread = "thread" in target ? target.thread : target;
2482
+ if (thread.kind === "discussion") {
2483
+ await graphql("REMOVE_REACTION", {
2484
+ id: "thread" in target ? target.id : await nodeId(thread, options),
2485
+ content: (await import("./graphql-D7uGzZTf.mjs")).REACTION_CONTENT[reaction]
2486
+ }, options);
2487
+ return;
2488
+ }
2489
+ const path = reactionPath(target);
2490
+ const { data } = await fetcher.json(path, {
2491
+ query: {
2492
+ content: reaction,
2493
+ per_page: 100
2494
+ },
2495
+ signal: options?.signal
2496
+ });
2497
+ const login = await viewerLogin();
2498
+ const mine = data.find((item) => item.content === reaction && item.user?.login === login);
2499
+ if (mine) await fetcher.raw(`${path}/${encodeURIComponent(mine.id)}`, {
2500
+ method: "DELETE",
2501
+ signal: options?.signal
2502
+ });
2503
+ }
2504
+ async function issuePatch(thread, json, action, signal) {
2505
+ const ref = requireIssueOrPull(thread, context, action);
2506
+ await fetcher.raw(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}`, {
2507
+ method: "PATCH",
2508
+ json,
2509
+ signal
2510
+ });
2511
+ }
2512
+ /** Check runs need an installation credential; a token writes a commit status instead. */
2513
+ const canWriteCheckRuns = auth.type === "app" && auth.installationId !== void 0;
2514
+ const STATUS_STATES = {
2515
+ pending: "pending",
2516
+ success: "success",
2517
+ failure: "failure",
2518
+ neutral: "success",
2519
+ unknown: "pending"
2520
+ };
2521
+ const CHECK_RUN_CONCLUSIONS = {
2522
+ success: "success",
2523
+ failure: "failure",
2524
+ neutral: "neutral"
2525
+ };
2526
+ async function report(repo, sha, input, options) {
2527
+ if (canWriteCheckRuns) {
2528
+ const conclusion = CHECK_RUN_CONCLUSIONS[input.state];
2529
+ const { data } = await fetcher.json(`${repoPath(repo)}/check-runs`, {
2530
+ method: "POST",
2531
+ json: {
2532
+ name: input.name,
2533
+ head_sha: sha,
2534
+ status: conclusion ? "completed" : "in_progress",
2535
+ conclusion,
2536
+ details_url: input.url,
2537
+ external_id: input.externalId,
2538
+ output: input.description ? {
2539
+ title: input.name,
2540
+ summary: input.description
2541
+ } : void 0
2542
+ },
2543
+ signal: options?.signal
2544
+ });
2545
+ return toCheckRun(repo, data);
2546
+ }
2547
+ const { data } = await fetcher.json(`${repoPath(repo)}/statuses/${sha}`, {
2548
+ method: "POST",
2549
+ json: {
2550
+ state: STATUS_STATES[input.state],
2551
+ context: input.name,
2552
+ description: input.description,
2553
+ target_url: input.url
2554
+ },
2555
+ signal: options?.signal
2556
+ });
2557
+ return toStatusCheck(repo, data);
2558
+ }
2559
+ async function runsPage(repo, query = {}) {
2560
+ const result = await fetcher.page(`${repoPath(repo)}/actions/runs`, {
2561
+ query: {
2562
+ branch: query.branch,
2563
+ status: query.state === "pending" ? "in_progress" : query.state === "success" ? "success" : query.state === "failure" ? "failure" : void 0,
2564
+ per_page: query.perPage
2565
+ },
2566
+ cursor: query.cursor,
2567
+ signal: query.signal,
2568
+ select: (body, next) => ({
2569
+ items: body.workflow_runs,
2570
+ next
2571
+ })
2572
+ });
2573
+ return toPage(result, (raw) => toWorkflowRun(repo, raw));
2574
+ }
2575
+ return {
2576
+ checks: {
2577
+ list: verb(true, (repo, sha, options) => headChecks(repo, sha, options?.signal)),
2578
+ report: verb(true, report),
2579
+ rerun: verb(true, async (ref, options) => {
2580
+ if (ref.type !== "check_run") throw new UnsupportedOperationError(`GitHub cannot re-run a ${ref.type}; only check runs can be re-requested`, context);
2581
+ try {
2582
+ await fetcher.raw(`${repoPath(ref.repo)}/check-runs/${encodeURIComponent(ref.id)}/rerequest`, {
2583
+ method: "POST",
2584
+ signal: options?.signal
2585
+ });
2586
+ } catch (error) {
2587
+ if (!(error instanceof ForgeApiError) || error.status !== 422) throw error;
2588
+ try {
2589
+ await fetcher.raw(`${repoPath(ref.repo)}/actions/jobs/${encodeURIComponent(ref.id)}/rerun`, {
2590
+ method: "POST",
2591
+ signal: options?.signal
2592
+ });
2593
+ } catch (rerunError) {
2594
+ throw rerunError instanceof NotFoundError ? error : rerunError;
2595
+ }
2596
+ }
2597
+ })
2598
+ },
2599
+ ci: {
2600
+ runsPage: verb(true, runsPage),
2601
+ run: verb(true, async (ref, options) => toWorkflowRun(ref.repo, (await fetcher.json(`${repoPath(ref.repo)}/actions/runs/${encodeURIComponent(ref.id)}`, { signal: options?.signal })).data)),
2602
+ jobsPage: verb(true, (ref, listOptions = {}) => list(`${repoPath(ref.repo)}/actions/runs/${encodeURIComponent(ref.id)}/jobs`, listOptions, (raw) => toWorkflowJob(ref, raw), { select: (body, next) => ({
2603
+ items: body.jobs,
2604
+ next
2605
+ }) })),
2606
+ log: verb(!anonymous, async (ref, options) => (await fetcher.stream(`${repoPath(ref.repo)}/actions/jobs/${encodeURIComponent(ref.id)}/logs`, { signal: options?.signal })).body)
2607
+ },
2608
+ contents: {
2609
+ file: verb(true, readFile),
2610
+ treePage: verb(true, treePage),
2611
+ branchesPage: verb(true, (repo, listOptions = {}) => list(`${repoPath(repo)}/branches`, listOptions, (raw) => toBranch(raw))),
2612
+ tagsPage: verb(true, (repo, listOptions = {}) => list(`${repoPath(repo)}/tags`, listOptions, toTag)),
2613
+ resolveRef: verb(true, async (repo, ref, options) => (await fetcher.json(`${repoPath(repo)}/commits/${encodeURIComponent(ref)}`, { signal: options?.signal })).data.sha),
2614
+ commitsPage: verb(true, commitsPage),
2615
+ commit: verb(true, async (repo, sha, options) => toCommit(repo, (await fetcher.json(`${repoPath(repo)}/commits/${sha}`, { signal: options?.signal })).data)),
2616
+ compare: verb(true, async (repo, base, head, options) => {
2617
+ const { data } = await fetcher.json(`${repoPath(repo)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`, { signal: options?.signal });
2618
+ return {
2619
+ base,
2620
+ head,
2621
+ aheadBy: data.ahead_by,
2622
+ behindBy: data.behind_by,
2623
+ mergeBaseSha: data.merge_base_commit?.sha,
2624
+ commits: (data.commits ?? []).map((raw) => toCommit(repo, raw)),
2625
+ files: (data.files ?? []).map(toChangedFile),
2626
+ raw: data
2627
+ };
2628
+ })
2629
+ },
2630
+ search: {
2631
+ threadsPage: verb(true, searchThreadsPage),
2632
+ reposPage: verb(true, searchReposPage),
2633
+ commitsPage: verb(true, searchCommitsPage),
2634
+ codePage: verb(!anonymous, searchCodePage),
2635
+ queryRaw: true
2636
+ },
2637
+ releases: {
2638
+ listPage: verb(true, releasesPage),
2639
+ get: verb(true, async (ref, options) => toRelease(ref.repo, (await fetcher.json(`${repoPath(ref.repo)}/releases/${encodeURIComponent(ref.id)}`, { signal: options?.signal })).data)),
2640
+ getByTag: verb(true, async (repo, tag, options) => toRelease(repo, (await fetcher.json(`${repoPath(repo)}/releases/tags/${encodeURIComponent(tag)}`, { signal: options?.signal })).data)),
2641
+ downloadAsset: verb(true, async (ref, downloadOptions = {}) => (await fetcher.stream(`${repoPath(ref.repo)}/releases/assets/${encodeURIComponent(ref.id)}`, {
2642
+ headers: { accept: "application/octet-stream" },
2643
+ signal: downloadOptions.signal
2644
+ })).body),
2645
+ latest: verb(true, async (repo, options) => {
2646
+ try {
2647
+ return toRelease(repo, (await fetcher.json(`${repoPath(repo)}/releases/latest`, { signal: options?.signal })).data);
2648
+ } catch (error) {
2649
+ if (error instanceof NotFoundError) return;
2650
+ throw error;
2651
+ }
2652
+ }),
2653
+ create: verb(true, (repo, input, options) => writeRelease(repo, `${repoPath(repo)}/releases`, "POST", input, options)),
2654
+ update: verb(true, (ref, update, options) => writeRelease(ref.repo, `${repoPath(ref.repo)}/releases/${encodeURIComponent(ref.id)}`, "PATCH", update, options))
2655
+ },
2656
+ securityAlerts: {
2657
+ kinds: {
2658
+ dependency: !anonymous && (enterprise ? ({ version }) => versionAtLeast(version, GHES_DEPENDABOT_ALERTS) && "unverified" : true),
2659
+ code_scanning: !anonymous,
2660
+ secret: !anonymous
2661
+ },
2662
+ listPage: alertsPage
2663
+ },
2664
+ traits: {
2665
+ eventKinds: "native",
2666
+ authKinds: [
2667
+ "token",
2668
+ "app",
2669
+ "anonymous"
2670
+ ],
2671
+ limits: {
2672
+ bodyLength: 65536,
2673
+ commentLength: 65536,
2674
+ labelLength: 50
2675
+ }
2676
+ },
2677
+ probeVersion: enterprise ? async () => (await fetcher.json("/meta")).data.installed_version : void 0,
2678
+ installations: credentials ? verb(true, createInstallationsApi(credentials)) : void 0,
2679
+ web: githubShapedWeb(enterprise ? baseUrl.replace(/\/api\/v3$/, "") : `https://${webHost(host)}`, {
2680
+ pull: "pull",
2681
+ discussions: true,
2682
+ commentFragment: "issuecomment-",
2683
+ file: (at) => `/blob/${encodeURIComponent(at)}`,
2684
+ lineFragment: (line) => `L${line}`,
2685
+ reserved: GITHUB_RESERVED_PATHS
2686
+ }),
2687
+ webhooks: {
2688
+ listPage: verb(true, (target, listOptions = {}) => list(hooksPath(target), listOptions, (raw) => toWebhook(target, raw), { mapError: scopeIs404 })),
2689
+ create: verb(true, async (target, input, options) => toWebhook(target, (await fetcher.json(hooksPath(target), {
2690
+ method: "POST",
2691
+ json: {
2692
+ name: "web",
2693
+ active: input.active ?? true,
2694
+ events: nativeEventsFor(GITHUB_NATIVE_EVENTS, input.events, input.nativeEvents),
2695
+ config: hookConfig(input)
2696
+ },
2697
+ mapError: scopeIs404,
2698
+ signal: options?.signal
2699
+ })).data)),
2700
+ update: verb(true, async (ref, update, options) => toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${encodeURIComponent(ref.id)}`, {
2701
+ method: "PATCH",
2702
+ json: {
2703
+ ...update.active === void 0 ? {} : { active: update.active },
2704
+ ...update.events || update.nativeEvents ? { events: nativeEventsFor(GITHUB_NATIVE_EVENTS, update.events, update.nativeEvents) } : {},
2705
+ ...update.url || update.secret || update.contentType ? { config: hookConfig(update) } : {}
2706
+ },
2707
+ mapError: scopeIs404,
2708
+ signal: options?.signal
2709
+ })).data)),
2710
+ delete: verb(true, async (ref, options) => {
2711
+ await fetcher.raw(`${hooksPath(ref.target)}/${encodeURIComponent(ref.id)}`, {
2712
+ method: "DELETE",
2713
+ mapError: scopeIs404,
2714
+ signal: options?.signal
2715
+ });
2716
+ }),
2717
+ rotateSecret: verb(true, async (ref, secret, options) => {
2718
+ const current = toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${encodeURIComponent(ref.id)}`, {
2719
+ mapError: scopeIs404,
2720
+ signal: options?.signal
2721
+ })).data);
2722
+ const { data } = await fetcher.json(`${hooksPath(ref.target)}/${encodeURIComponent(ref.id)}`, {
2723
+ method: "PATCH",
2724
+ json: { config: hookConfig({
2725
+ url: current.url,
2726
+ contentType: current.contentType,
2727
+ secret
2728
+ }) },
2729
+ mapError: scopeIs404,
2730
+ signal: options?.signal
2731
+ });
2732
+ return toWebhook(ref.target, data);
2733
+ }),
2734
+ deliveriesPage: verb(true, (ref, listOptions = {}) => list(`${hooksPath(ref.target)}/${encodeURIComponent(ref.id)}/deliveries`, listOptions, (raw) => toWebhookDelivery(ref, raw), { mapError: scopeIs404 })),
2735
+ redeliver: verb(true, async (ref, options) => {
2736
+ await fetcher.raw(`${hooksPath(ref.hook.target)}/${encodeURIComponent(ref.hook.id)}/deliveries/${encodeURIComponent(ref.id)}/attempts`, {
2737
+ method: "POST",
2738
+ mapError: scopeIs404,
2739
+ signal: options?.signal
2740
+ });
2741
+ })
2742
+ },
2743
+ scopes: githubScopesFor,
2744
+ users: {
2745
+ get: verb(true, async (login, options) => toUser(instance, (await fetcher.json(`/users/${encodeURIComponent(login)}`, { signal: options?.signal })).data)),
2746
+ me: verb(auth.type === "token", async (options) => toUser(instance, (await fetcher.json("/user", { signal: options?.signal })).data))
2747
+ },
2748
+ repos: {
2749
+ get: verb(true, async (ref, options) => {
2750
+ return toRepo(instance, (await fetcher.json(repoPath(ref), { signal: options?.signal })).data);
2751
+ }),
2752
+ listPage: verb(auth.type === "token" || auth.type === "app" && auth.installationId !== void 0, async (listOptions = {}) => {
2753
+ if (auth.type === "app") {
2754
+ if (auth.installationId === void 0) throw new UnsupportedOperationError("An app without an installation has no repositories; use installations.repos()", context);
2755
+ return reposPage(fetcher, "/installation/repositories", true, listOptions);
2756
+ }
2757
+ return reposPage(fetcher, "/user/repos", false, listOptions);
2758
+ }),
2759
+ labelsPage: verb(true, (repo, listOptions = {}) => list(`${repoPath(repo)}/labels`, listOptions, toLabel)),
2760
+ createLabel: verb(true, async (repo, label, options) => toLabel((await fetcher.json(`${repoPath(repo)}/labels`, {
2761
+ method: "POST",
2762
+ json: {
2763
+ name: label.name,
2764
+ color: label.colour,
2765
+ description: label.description
2766
+ },
2767
+ signal: options?.signal
2768
+ })).data)),
2769
+ milestonesPage: verb(true, milestonesPage),
2770
+ collaboratorsPage: verb(!anonymous, (repo, listOptions = {}) => list(`${repoPath(repo)}/collaborators`, listOptions, (raw) => toCollaborator(instance, raw))),
2771
+ permissionFor: verb(!anonymous, async (repo, actor, options) => {
2772
+ const { data } = await fetcher.json(`${repoPath(repo)}/collaborators/${encodeURIComponent(actorLogin(actor))}/permission`, { signal: options?.signal });
2773
+ return toRole({
2774
+ role_name: data.role_name ?? (data.permission === "none" ? void 0 : data.permission),
2775
+ permissions: data.user?.permissions
2776
+ });
2777
+ }),
2778
+ addCollaborator: verb(true, async (repo, actor, role, options) => {
2779
+ await fetcher.raw(`${repoPath(repo)}/collaborators/${encodeURIComponent(actorLogin(actor))}`, {
2780
+ method: "PUT",
2781
+ json: { permission: role },
2782
+ signal: options?.signal
2783
+ });
2784
+ }),
2785
+ assignableUsersPage: verb(true, (repo, listOptions = {}) => list(`${repoPath(repo)}/assignees`, listOptions, (raw) => toActor(instance, raw))),
2786
+ reviewerCandidatesPage: verb(!anonymous && "emulated", async (thread, listOptions = {}) => {
2787
+ const ref = requireThread(thread, context);
2788
+ const page = await fetcher.page(`${repoPath(ref.repo)}/collaborators`, {
2789
+ query: { per_page: listOptions.perPage },
2790
+ cursor: listOptions.cursor,
2791
+ signal: listOptions.signal
2792
+ });
2793
+ const author = (await get(ref, listOptions)).author?.login;
2794
+ return toPage(page, (raw) => raw.login === author ? void 0 : toActor(instance, raw));
2795
+ })
2796
+ },
2797
+ notifications: {
2798
+ listPage: verb(true, notificationPage),
2799
+ markRead: verb(true, async (ref, options) => {
2800
+ await fetcher.raw(`/notifications/threads/${encodeURIComponent(ref.id)}`, {
2801
+ method: "PATCH",
2802
+ signal: options?.signal
2803
+ });
2804
+ }),
2805
+ markDone: verb(enterprise ? ({ version }) => versionAtLeast(version, GHES_MARK_DONE) : true, async (ref, options) => {
2806
+ await fetcher.raw(`/notifications/threads/${encodeURIComponent(ref.id)}`, {
2807
+ method: "DELETE",
2808
+ signal: options?.signal
2809
+ });
2810
+ }),
2811
+ unsubscribe: verb(true, async (ref, options) => {
2812
+ await fetcher.raw(`/notifications/threads/${encodeURIComponent(ref.id)}/subscription`, {
2813
+ method: "DELETE",
2814
+ signal: options?.signal
2815
+ });
2816
+ }),
2817
+ markAllRead: verb(true, async (bulk = {}) => {
2818
+ await fetcher.raw(bulk.repo ? `${repoPath(bulk.repo)}/notifications` : "/notifications", {
2819
+ method: "PUT",
2820
+ ...bulk.before ? { json: { last_read_at: bulk.before.toISOString() } } : {},
2821
+ signal: bulk.signal
2822
+ });
2823
+ })
2824
+ },
2825
+ threads: {
2826
+ get: perKind({
2827
+ issue: true,
2828
+ pull_request: true,
2829
+ discussion: !anonymous,
2830
+ commit: true
2831
+ }, get),
2832
+ getMany: verb(true, getMany),
2833
+ listPage: perKind({
2834
+ ...ISSUE_LIKE,
2835
+ discussion: !anonymous
2836
+ }, listPage),
2837
+ eventsPage: verb(true, async (thread, listOptions = {}) => {
2838
+ const ref = requireThread(thread, context);
2839
+ if (ref.kind === "discussion") {
2840
+ const [page, { toDiscussionCommentEvent }] = await Promise.all([discussionCommentPage(ref, listOptions), import("./graphql-D7uGzZTf.mjs")]);
2841
+ return {
2842
+ ...page,
2843
+ items: page.items.map(({ comment, isReply }) => toDiscussionCommentEvent(ref, comment, isReply))
2844
+ };
2845
+ }
2846
+ if (ref.kind === "commit") return list(`${repoPath(ref.repo)}/commits/${encodeURIComponent(ref.number)}/comments`, listOptions, (comment) => toEvent(instance, ref, {
2847
+ ...comment,
2848
+ event: "commented"
2849
+ }));
2850
+ return list(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/timeline`, listOptions, (entry) => toEvent(instance, ref, entry));
2851
+ }),
2852
+ commentsPage: perKind({
2853
+ issue: true,
2854
+ pull_request: true,
2855
+ discussion: !anonymous,
2856
+ commit: true
2857
+ }, commentsPage),
2858
+ reviewsPage: verb(true, reviewsPage),
2859
+ filesPage: verb(true, (ref, listOptions = {}) => list(`${threadPath(requirePull(ref, "read for changed files"))}/files`, listOptions, toChangedFile)),
2860
+ commitsPage: verb(true, (ref, listOptions = {}) => list(`${threadPath(requirePull(ref, "read for commits"))}/commits`, listOptions, (raw) => toCommit(ref.repo, raw))),
2861
+ createReview: verb(true, createReview),
2862
+ submitReview: verb(true, async (ref, event, body, options) => {
2863
+ const pull = requirePull(ref.thread, "reviewed");
2864
+ const { data } = await fetcher.json(`${threadPath(pull)}/reviews/${encodeURIComponent(ref.id)}/events`, {
2865
+ method: "POST",
2866
+ json: {
2867
+ event: REVIEW_EVENTS[event],
2868
+ body
2869
+ },
2870
+ signal: options?.signal
2871
+ });
2872
+ return toReview(pull, data, []);
2873
+ }),
2874
+ reviewThreads: verb(true, {
2875
+ resolveReviewThread: async (_ref, id, options) => {
2876
+ await graphql("RESOLVE_REVIEW_THREAD", { id }, options);
2877
+ },
2878
+ unresolveReviewThread: async (_ref, id, options) => {
2879
+ await graphql("UNRESOLVE_REVIEW_THREAD", { id }, options);
2880
+ }
2881
+ }),
2882
+ checks: perKind({ pull_request: true }, async (thread, options) => {
2883
+ const ref = requireThread(thread, context);
2884
+ const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
2885
+ return data.head?.sha ? headChecks(ref.repo, data.head.sha, options?.signal) : { items: [] };
2886
+ }),
2887
+ comment: perKind({
2888
+ issue: true,
2889
+ pull_request: true,
2890
+ discussion: true,
2891
+ commit: true
2892
+ }, async (thread, body, options) => {
2893
+ const ref = requireThread(thread, context);
2894
+ if (ref.kind === "discussion") {
2895
+ const data = await graphql("ADD_DISCUSSION_COMMENT", {
2896
+ id: await discussionId(ref, options),
2897
+ body
2898
+ }, options);
2899
+ return (await import("./graphql-D7uGzZTf.mjs")).toDiscussionComment(ref, data.addDiscussionComment.comment);
2900
+ }
2901
+ const path = ref.kind === "commit" ? `${repoPath(ref.repo)}/commits/${encodeURIComponent(ref.number)}/comments` : `${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/comments`;
2902
+ const { data } = await fetcher.json(path, {
2903
+ method: "POST",
2904
+ json: { body },
2905
+ signal: options?.signal
2906
+ });
2907
+ return toComment(ref, data);
2908
+ }),
2909
+ editComment: perKind({
2910
+ issue: true,
2911
+ pull_request: true,
2912
+ discussion: true,
2913
+ commit: true
2914
+ }, async (ref, body, options) => {
2915
+ if (ref.thread.kind === "discussion") {
2916
+ const data = await graphql("UPDATE_DISCUSSION_COMMENT", {
2917
+ id: ref.id,
2918
+ body
2919
+ }, options);
2920
+ return (await import("./graphql-D7uGzZTf.mjs")).toDiscussionComment(ref.thread, data.updateDiscussionComment.comment);
2921
+ }
2922
+ const { data } = await fetcher.json(commentPath(ref), {
2923
+ method: "PATCH",
2924
+ json: { body },
2925
+ signal: options?.signal
2926
+ });
2927
+ return toComment(ref.thread, data);
2928
+ }),
2929
+ deleteComment: perKind({
2930
+ issue: true,
2931
+ pull_request: true,
2932
+ discussion: true,
2933
+ commit: true
2934
+ }, async (ref, options) => {
2935
+ if (ref.thread.kind === "discussion") {
2936
+ await graphql("DELETE_DISCUSSION_COMMENT", { id: ref.id }, options);
2937
+ return;
2938
+ }
2939
+ await fetcher.raw(commentPath(ref), {
2940
+ method: "DELETE",
2941
+ signal: options?.signal
2942
+ });
2943
+ }),
2944
+ create: perKind({
2945
+ issue: true,
2946
+ pull_request: true
2947
+ }, async (repo, input, options) => {
2948
+ const assignees = input.assignees?.map(actorLogin);
2949
+ if (input.kind === "issue") {
2950
+ const { data } = await fetcher.json(`${repoPath(repo)}/issues`, {
2951
+ method: "POST",
2952
+ json: {
2953
+ title: input.title,
2954
+ body: input.body,
2955
+ labels: input.labels,
2956
+ assignees
2957
+ },
2958
+ signal: options?.signal
2959
+ });
2960
+ return toThread({
2961
+ forge: FORGE,
2962
+ instance,
2963
+ repo,
2964
+ kind: "issue",
2965
+ number: String(data.number)
2966
+ }, data);
2967
+ }
2968
+ if (input.kind !== "pull_request") throw new UnsupportedOperationError("GitHub discussions cannot be created through this API yet", context);
2969
+ if (!input.head || !input.base) throw new UnsupportedOperationError("Creating a pull request needs head and base branches", context);
2970
+ const { data: pull } = await fetcher.json(`${repoPath(repo)}/pulls`, {
2971
+ method: "POST",
2972
+ json: {
2973
+ title: input.title,
2974
+ body: input.body,
2975
+ head: input.head,
2976
+ base: input.base,
2977
+ draft: input.draft
2978
+ },
2979
+ signal: options?.signal
2980
+ });
2981
+ const ref = {
2982
+ forge: FORGE,
2983
+ instance,
2984
+ repo,
2985
+ kind: "pull_request",
2986
+ number: String(pull.number)
2987
+ };
2988
+ if (!input.labels?.length && !assignees?.length) return toThread(ref, pull);
2989
+ const { data: issue } = await fetcher.json(`${repoPath(repo)}/issues/${encodeURIComponent(pull.number)}`, {
2990
+ method: "PATCH",
2991
+ json: {
2992
+ labels: input.labels,
2993
+ assignees
2994
+ },
2995
+ signal: options?.signal
2996
+ });
2997
+ return toThread(ref, {
2998
+ ...pull,
2999
+ labels: issue.labels,
3000
+ assignees: issue.assignees
3001
+ });
3002
+ }),
3003
+ update: perKind({
3004
+ issue: true,
3005
+ pull_request: true,
3006
+ discussion: true
3007
+ }, async (thread, input, options) => {
3008
+ const ref = requireThread(thread, context);
3009
+ if (ref.kind === "discussion") {
3010
+ const data = await graphql("UPDATE_DISCUSSION", {
3011
+ id: await discussionId(ref, options),
3012
+ title: input.title,
3013
+ body: input.body
3014
+ }, options);
3015
+ return (await import("./graphql-D7uGzZTf.mjs")).toDiscussionThread(ref, data.updateDiscussion.discussion);
3016
+ }
3017
+ const target = requireIssueOrPull(ref, context, "update");
3018
+ const { data } = await fetcher.json(threadPath(target), {
3019
+ method: "PATCH",
3020
+ json: input,
3021
+ signal: options?.signal
3022
+ });
3023
+ return toThread(target, data);
3024
+ }),
3025
+ addLabels: perKind({
3026
+ issue: true,
3027
+ pull_request: true
3028
+ }, async (thread, labels, options) => {
3029
+ const ref = requireIssueOrPull(thread, context, "label");
3030
+ await fetcher.raw(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/labels`, {
3031
+ method: "POST",
3032
+ json: { labels },
3033
+ signal: options?.signal
3034
+ });
3035
+ }),
3036
+ removeLabels: perKind({
3037
+ issue: true,
3038
+ pull_request: true
3039
+ }, async (thread, labels, options) => {
3040
+ const ref = requireIssueOrPull(thread, context, "label");
3041
+ for (const label of labels) await fetcher.raw(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/labels/${encodeURIComponent(label)}`, {
3042
+ method: "DELETE",
3043
+ signal: options?.signal
3044
+ });
3045
+ }),
3046
+ setMilestone: perKind({
3047
+ issue: true,
3048
+ pull_request: true
3049
+ }, async (thread, milestone, options) => issuePatch(thread, { milestone: milestone === void 0 ? null : await milestoneId(milestone, (page) => milestonesPage(requireThread(thread, context).repo, page), context, options) }, "set the milestone of", options?.signal)),
3050
+ reactions: perKind({
3051
+ issue: true,
3052
+ pull_request: true,
3053
+ discussion: true
3054
+ }, {
3055
+ react,
3056
+ unreact
3057
+ }),
3058
+ reactionsPage: perKind({
3059
+ issue: true,
3060
+ pull_request: true,
3061
+ discussion: !anonymous
3062
+ }, reactionsPage),
3063
+ transfer: verb(true, async (thread, repo, options) => {
3064
+ const ref = requireThread(thread, context);
3065
+ if (ref.kind !== "issue") throw new UnsupportedOperationError("GitHub transfers issues only", context);
3066
+ const target = await graphql("REPOSITORY_ID", {
3067
+ owner: repo.owner,
3068
+ name: repo.name
3069
+ }, options);
3070
+ if (!target.repository) throw new NotFoundError(`Repository ${repo.owner}/${repo.name} not found`, 404, "", context);
3071
+ const data = await graphql("TRANSFER_ISSUE", {
3072
+ issue: await nodeId(ref, options),
3073
+ repo: target.repository.id
3074
+ }, options);
3075
+ return {
3076
+ forge: FORGE,
3077
+ instance,
3078
+ repo,
3079
+ kind: "issue",
3080
+ number: String(data.transferIssue.issue.number),
3081
+ externalId: data.transferIssue.issue.id
3082
+ };
3083
+ }),
3084
+ markDuplicate: verb(true, async (thread, canonical, options) => {
3085
+ await graphql("MARK_DUPLICATE", {
3086
+ canonical: await nodeId(canonical, options),
3087
+ duplicate: await nodeId(thread, options)
3088
+ }, options);
3089
+ }),
3090
+ setLabels: perKind({
3091
+ issue: true,
3092
+ pull_request: true
3093
+ }, async (thread, labels, options) => {
3094
+ const ref = requireIssueOrPull(thread, context, "label");
3095
+ await fetcher.raw(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}/labels`, {
3096
+ method: "PUT",
3097
+ json: { labels },
3098
+ signal: options?.signal
3099
+ });
3100
+ }),
3101
+ setAssignees: perKind({
3102
+ issue: true,
3103
+ pull_request: true
3104
+ }, async (thread, assignees, options) => {
3105
+ const ref = requireIssueOrPull(thread, context, "assign");
3106
+ await fetcher.raw(`${repoPath(ref.repo)}/issues/${encodeURIComponent(ref.number)}`, {
3107
+ method: "PATCH",
3108
+ json: { assignees: assignees.map(actorLogin) },
3109
+ signal: options?.signal
3110
+ });
3111
+ }),
3112
+ requestReview: perKind({ pull_request: true }, async (thread, reviewers, options) => {
3113
+ const ref = requireThread(thread, context);
3114
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only pull requests have reviewers", context);
3115
+ const teams = reviewers.filter((reviewer) => typeof reviewer !== "string" && reviewer.typeRaw === "Team").map(actorLogin);
3116
+ const users = reviewers.filter((reviewer) => typeof reviewer === "string" || reviewer.typeRaw !== "Team").map(actorLogin);
3117
+ await fetcher.raw(`${threadPath(ref)}/requested_reviewers`, {
3118
+ method: "POST",
3119
+ json: {
3120
+ reviewers: users,
3121
+ ...teams.length ? { team_reviewers: teams } : {}
3122
+ },
3123
+ signal: options?.signal
3124
+ });
3125
+ }),
3126
+ close: perKind(ISSUE_LIKE, (ref, options_) => setState(ref, "closed", options_)),
3127
+ reopen: perKind({
3128
+ issue: true,
3129
+ pull_request: true,
3130
+ discussion: true
3131
+ }, (ref, options) => setState(ref, "open", options)),
3132
+ merge: verb(true, merge),
3133
+ subscriptions: perKind(ISSUE_LIKE, {
3134
+ subscription: async (thread, options) => {
3135
+ const { state } = await subscriptionNode(thread, options);
3136
+ return state === "SUBSCRIBED" ? "subscribed" : state === "IGNORED" ? "ignored" : "none";
3137
+ },
3138
+ subscribe: (thread, options) => setSubscription(thread, "SUBSCRIBED", options),
3139
+ unsubscribe: (thread, options) => setSubscription(thread, "UNSUBSCRIBED", options)
3140
+ })
3141
+ }
3142
+ };
3143
+ }
3144
+ const GITHUB = {
3145
+ forge: FORGE,
3146
+ baseUrl: "https://api.github.com",
3147
+ anonymous: true,
3148
+ instance: webHost,
3149
+ headers: {
3150
+ "accept": "application/vnd.github+json",
3151
+ "x-github-api-version": "2022-11-28"
3152
+ },
3153
+ prepare: ({ options, baseUrl, headers, state }) => options.auth?.type === "app" ? state ?? createAppCredentials(options.auth, {
3154
+ baseUrl,
3155
+ fetch: options.fetch,
3156
+ timeout: options.timeout,
3157
+ headers
3158
+ }) : void 0,
3159
+ authHeaders: ({ options, state }) => createAuthHeaders(options.auth, state),
3160
+ setup: setupGitHub
3161
+ };
3162
+ /** Creates a GitHub provider for github.com or a GitHub Enterprise Server instance. */
3163
+ const github = /* @__PURE__ */ defineForgeProvider({
3164
+ ...GITHUB,
3165
+ webhooks: githubWebhooks
3166
+ });
3167
+ /** `github()` without webhook ingestion, for bundles that never receive a delivery. */
3168
+ const githubLite = /* @__PURE__ */ defineForgeProvider(GITHUB);
3169
+ /**
3170
+ * Token scopes and fine-grained permissions GitHub documents per endpoint
3171
+ * group. Classic scopes are the ones an OAuth consent screen asks for; the
3172
+ * permissions are the fine-grained token and app equivalents.
3173
+ */
3174
+ function githubScopesFor(verb) {
3175
+ const [group = "", name = ""] = verb.split(".");
3176
+ if (group === "webhooks") return name === "verify" || name === "ingest" ? {} : {
3177
+ token: ["admin:repo_hook", "admin:org_hook"],
3178
+ permissions: { webhooks: "admin" }
3179
+ };
3180
+ if (group === "notifications") return {
3181
+ token: ["notifications"],
3182
+ permissions: { notifications: name.startsWith("mark") || name === "unsubscribe" ? "write" : "read" }
3183
+ };
3184
+ if (group === "securityAlerts") return {
3185
+ token: ["security_events", "repo"],
3186
+ permissions: {
3187
+ vulnerability_alerts: "read",
3188
+ secret_scanning_alerts: "read",
3189
+ code_scanning_alerts: "read"
3190
+ }
3191
+ };
3192
+ if (group === "installations") return { note: "App credentials: an app JWT signed with the app private key" };
3193
+ if (group === "users" || group === "search") return {
3194
+ token: ["read:user"],
3195
+ permissions: {}
3196
+ };
3197
+ if (group === "ci" || group === "checks" && name !== "report") return {
3198
+ token: ["repo"],
3199
+ permissions: {
3200
+ actions: "read",
3201
+ checks: name === "rerun" ? "write" : "read"
3202
+ }
3203
+ };
3204
+ if (verb === "checks.report") return {
3205
+ token: ["repo:status"],
3206
+ permissions: {
3207
+ checks: "write",
3208
+ statuses: "write"
3209
+ }
3210
+ };
3211
+ const write = (/* @__PURE__ */ new Set([
3212
+ "comment",
3213
+ "upsertComment",
3214
+ "editComment",
3215
+ "deleteComment",
3216
+ "create",
3217
+ "update",
3218
+ "close",
3219
+ "reopen",
3220
+ "setLabels",
3221
+ "addLabels",
3222
+ "removeLabels",
3223
+ "setMilestone",
3224
+ "react",
3225
+ "unreact",
3226
+ "setAssignees",
3227
+ "requestReview",
3228
+ "merge",
3229
+ "approveAndMerge",
3230
+ "createReview",
3231
+ "submitReview",
3232
+ "approve",
3233
+ "transfer",
3234
+ "markDuplicate",
3235
+ "resolveReviewThread",
3236
+ "unresolveReviewThread",
3237
+ "createLabel",
3238
+ "addCollaborator",
3239
+ "subscribe",
3240
+ "unsubscribe"
3241
+ ])).has(name);
3242
+ if (group === "threads") return {
3243
+ token: ["repo", "public_repo"],
3244
+ permissions: {
3245
+ issues: write ? "write" : "read",
3246
+ pull_requests: write ? "write" : "read"
3247
+ }
3248
+ };
3249
+ if (group === "repos" || group === "contents" || group === "releases") return {
3250
+ token: ["repo", "public_repo"],
3251
+ permissions: {
3252
+ contents: write ? "write" : "read",
3253
+ metadata: "read"
3254
+ }
3255
+ };
3256
+ return {};
3257
+ }
3258
+ //#endregion
3259
+ export { FORGE as i, githubLite as n, githubScopesFor as r, github as t };