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.
- package/LICENCE +21 -0
- package/README.md +72 -0
- package/dist/azure-devops/index.d.mts +2 -0
- package/dist/azure-devops/index.mjs +1 -0
- package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
- package/dist/bitbucket/index.d.mts +2 -0
- package/dist/bitbucket/index.mjs +1 -0
- package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
- package/dist/crypto-CQNEyYHD.mjs +156 -0
- package/dist/cursor-origin/index.d.mts +2 -0
- package/dist/cursor-origin/index.mjs +1 -0
- package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
- package/dist/define-B2_4ob2i.d.mts +307 -0
- package/dist/define-D3yEXtKK.mjs +2276 -0
- package/dist/env.d.mts +46 -0
- package/dist/env.mjs +283 -0
- package/dist/fake/index.d.mts +227 -0
- package/dist/fake/index.mjs +1001 -0
- package/dist/fetch-D4pSRvvA.d.mts +1723 -0
- package/dist/forgejo/index.d.mts +2 -0
- package/dist/forgejo/index.mjs +1 -0
- package/dist/forgejo-BCefbHUN.mjs +2005 -0
- package/dist/gitea/index.d.mts +2 -0
- package/dist/gitea/index.mjs +22 -0
- package/dist/gitee/index.d.mts +2 -0
- package/dist/gitee/index.mjs +1 -0
- package/dist/gitee-CA-f7Rs8.mjs +1272 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.mjs +1 -0
- package/dist/github-B0umL8_f.mjs +3259 -0
- package/dist/gitlab/index.d.mts +2 -0
- package/dist/gitlab/index.mjs +1 -0
- package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
- package/dist/graphql-D7uGzZTf.mjs +366 -0
- package/dist/index-BQ6VS31s.d.mts +32 -0
- package/dist/index-C-jz9Mo3.d.mts +16 -0
- package/dist/index-C4fYzmK3.d.mts +102 -0
- package/dist/index-CQiIX4SH.d.mts +27 -0
- package/dist/index-CTj5gQzl.d.mts +35 -0
- package/dist/index-CliBWzOF.d.mts +23 -0
- package/dist/index-Cz_y-t3d.d.mts +36 -0
- package/dist/index-DEt25E1w.d.mts +77 -0
- package/dist/index-DIuS0EOy.d.mts +43 -0
- package/dist/index-uSpsMCrC.d.mts +26 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +13 -0
- package/dist/kit.d.mts +183 -0
- package/dist/kit.mjs +4 -0
- package/dist/provider-CoZ7cCxp.d.mts +1439 -0
- package/dist/provider-Ed1YhkQN.mjs +169 -0
- package/dist/pushin/index.d.mts +2 -0
- package/dist/pushin/index.mjs +1 -0
- package/dist/pushin-CkxgJ2cU.mjs +343 -0
- package/dist/schema/Actor.json +96 -0
- package/dist/schema/AlertSupport.json +49 -0
- package/dist/schema/AuthKind.json +15 -0
- package/dist/schema/Branch.json +31 -0
- package/dist/schema/ChangedFile.json +58 -0
- package/dist/schema/Check.json +169 -0
- package/dist/schema/CheckRef.json +114 -0
- package/dist/schema/CheckState.json +14 -0
- package/dist/schema/ChecksSummary.json +34 -0
- package/dist/schema/CiJob.json +186 -0
- package/dist/schema/CiJobRef.json +132 -0
- package/dist/schema/CiRun.json +234 -0
- package/dist/schema/CiRunRef.json +103 -0
- package/dist/schema/CodeFragment.json +19 -0
- package/dist/schema/CodeMatch.json +128 -0
- package/dist/schema/Collaborator.json +169 -0
- package/dist/schema/Comment.json +336 -0
- package/dist/schema/CommentRef.json +158 -0
- package/dist/schema/Commit.json +307 -0
- package/dist/schema/CommitRef.json +104 -0
- package/dist/schema/CommitSignature.json +118 -0
- package/dist/schema/Comparison.json +355 -0
- package/dist/schema/Cursor.json +21 -0
- package/dist/schema/EventAction.json +33 -0
- package/dist/schema/EventDetail.json +649 -0
- package/dist/schema/EventKind.json +26 -0
- package/dist/schema/FileContent.json +73 -0
- package/dist/schema/FileStatus.json +15 -0
- package/dist/schema/ForgeCapabilities.json +737 -0
- package/dist/schema/ForgeEvent.json +789 -0
- package/dist/schema/ForgeInstance.json +7 -0
- package/dist/schema/ForgeKind.json +32 -0
- package/dist/schema/ForgeWarning.json +49 -0
- package/dist/schema/ForgeWarningCode.json +7 -0
- package/dist/schema/GetManyResult.json +699 -0
- package/dist/schema/Installation.json +131 -0
- package/dist/schema/InstallationToken.json +31 -0
- package/dist/schema/KnownForgeKind.json +19 -0
- package/dist/schema/Label.json +22 -0
- package/dist/schema/MergeMethod.json +14 -0
- package/dist/schema/Milestone.json +40 -0
- package/dist/schema/Notification.json +498 -0
- package/dist/schema/NotificationReason.json +28 -0
- package/dist/schema/NotificationRef.json +60 -0
- package/dist/schema/NotificationSubject.json +376 -0
- package/dist/schema/PerKind.json +49 -0
- package/dist/schema/PullBranches.json +125 -0
- package/dist/schema/PushCommit.json +24 -0
- package/dist/schema/Reaction.json +148 -0
- package/dist/schema/ReactionContent.json +17 -0
- package/dist/schema/ReactionSummary.json +70 -0
- package/dist/schema/Release.json +286 -0
- package/dist/schema/ReleaseAsset.json +169 -0
- package/dist/schema/ReleaseAssetRef.json +138 -0
- package/dist/schema/ReleaseRef.json +107 -0
- package/dist/schema/Repo.json +354 -0
- package/dist/schema/RepoFeatures.json +35 -0
- package/dist/schema/RepoPermissions.json +35 -0
- package/dist/schema/RepoRef.json +77 -0
- package/dist/schema/RepoRole.json +15 -0
- package/dist/schema/RepoVisibility.json +12 -0
- package/dist/schema/Review.json +371 -0
- package/dist/schema/ReviewComment.json +280 -0
- package/dist/schema/ReviewCommentInput.json +34 -0
- package/dist/schema/ReviewRef.json +158 -0
- package/dist/schema/ReviewState.json +15 -0
- package/dist/schema/Reviewer.json +133 -0
- package/dist/schema/SecurityAlert.json +219 -0
- package/dist/schema/SecurityAlertKind.json +14 -0
- package/dist/schema/SecurityAlertRef.json +122 -0
- package/dist/schema/SecuritySeverity.json +14 -0
- package/dist/schema/SourceKind.json +12 -0
- package/dist/schema/SubscriptionState.json +12 -0
- package/dist/schema/Support.json +23 -0
- package/dist/schema/Tag.json +25 -0
- package/dist/schema/TextLimits.json +18 -0
- package/dist/schema/Thread.json +653 -0
- package/dist/schema/ThreadKind.json +14 -0
- package/dist/schema/ThreadRef.json +131 -0
- package/dist/schema/ThreadStack.json +146 -0
- package/dist/schema/ThreadState.json +13 -0
- package/dist/schema/TreeEntry.json +46 -0
- package/dist/schema/TreeEntryType.json +13 -0
- package/dist/schema/UpsertCommentResult.json +355 -0
- package/dist/schema/User.json +123 -0
- package/dist/schema/Webhook.json +188 -0
- package/dist/schema/WebhookDeliveryRecord.json +173 -0
- package/dist/schema/WebhookDeliveryRef.json +135 -0
- package/dist/schema/WebhookRef.json +105 -0
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.mjs +3219 -0
- package/dist/tangled/index.d.mts +2 -0
- package/dist/tangled/index.mjs +1 -0
- package/dist/tangled-ChdPmiv-.mjs +1570 -0
- package/dist/testing/index.d.mts +123 -0
- package/dist/testing/index.mjs +242 -0
- package/dist/webhooks-rn9-8lxT.mjs +70 -0
- package/package.json +115 -3
- 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 };
|