This repository has been archived by the owner on Jan 21, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathwebhook.ts
352 lines (313 loc) · 10.8 KB
/
webhook.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import { IssueCommentCreatedEvent } from "@octokit/webhooks-types/schema"
import { Mutex } from "async-mutex"
import path from "path"
import { Probot } from "probot"
import {
botMentionPrefix,
defaultTryRuntimeGetCommandOptions,
} from "./constants"
import {
getPullRequestTaskHandle,
getRegisterPullRequestHandle,
queue,
} from "./executor"
import {
createComment,
ExtendedOctokit,
getOctokit,
getPostPullRequestResult,
getPullRequestHandleId,
isOrganizationMember,
updateComment,
} from "./github"
import { Logger } from "./logger"
import {
PullRequestError,
PullRequestTask,
State,
WebhookEvents,
} from "./types"
import { displayCommand, getCommand, getLines, getParsedArgs } from "./utils"
type WebhookEventPayload<E extends WebhookEvents> =
E extends "issue_comment.created" ? IssueCommentCreatedEvent : never
type WebhookHandler<E extends WebhookEvents> = (
logger: Logger,
octokit: ExtendedOctokit,
event: WebhookEventPayload<E>,
) => Promise<PullRequestError | void>
export const setupEvent = function <E extends WebhookEvents>(
bot: Probot,
eventName: E,
handler: WebhookHandler<E>,
logger: Logger,
) {
bot.on(eventName, async function (event) {
logger.debug(event, `Got event for ${eventName}`)
const installationId: number | undefined =
"installation" in event.payload
? event.payload.installation?.id
: undefined
const octokit = getOctokit(await bot.auth(installationId))
try {
const result = await handler(
logger.child({ event: eventName, eventId: event.id }),
octokit,
event.payload as WebhookEventPayload<E>,
)
if (result instanceof PullRequestError) {
const {
params: { pull_number, ...params },
comment: { body, commentId, requester },
} = result
const sharedCommentParams = {
...params,
issue_number: pull_number,
body: `${requester ? `@${requester} ` : ""}${body}`,
}
if (commentId) {
await updateComment(octokit, {
...sharedCommentParams,
comment_id: commentId,
})
} else {
await createComment(octokit, sharedCommentParams)
}
}
} catch (error) {
logger.fatal(error, "Exception caught in webhook handler")
}
})
}
// Mutexes are used so that payloads are handled one-at-a-time; this is will
// not get the bot stuck until the command finishes, however, since queueing
// should be asynchronous
const mutex = new Mutex()
export const getWebhooksHandlers = function (state: State) {
const {
logger,
version,
allowedOrganizations,
repositoryCloneDirectory,
nodesAddresses,
} = state
const isRequesterAllowed = async function (
octokit: ExtendedOctokit,
username: string,
) {
for (const organizationId of allowedOrganizations) {
if (
await isOrganizationMember({
organizationId,
username,
octokit,
logger,
})
) {
return true
}
}
return false
}
const onIssueCommentCreated: WebhookHandler<"issue_comment.created"> =
async function (eventName, octokit, payload) {
// Note: async-mutex implements a "fair mutex" which means requests will be
// queued in the same order as they're received; if changing to a different
// library then verify that this aspect is maintained.
await mutex.runExclusive(async function () {
const { issue, comment, repository, installation } = payload
if (!("pull_request" in issue)) {
logger.debug(
payload,
`Skipping payload in ${eventName} because it's not from a pull request`,
)
return
}
const requester = comment.user?.login
if (!requester) {
logger.debug(payload, "Skipping payload because it has no requester")
return
}
if (payload.action !== "created") {
logger.debug(
payload,
"Skipping payload because it's not for created comments",
)
return
}
if (comment.user?.type !== "User") {
logger.debug(
payload,
`Skipping payload because comment.user.type (${comment.user?.type}) is not "User"`,
)
return
}
const repo = repository.name
const owner = repository.owner.login
const pullNumber = issue.number
const prParams = { owner, repo, pull_number: pullNumber }
const commentParams = { owner, repo, issue_number: pullNumber }
const handleId = getPullRequestHandleId(prParams)
let commentId: number | undefined = undefined
const getError = function (body: string) {
return new PullRequestError(prParams, { body, requester, commentId })
}
try {
const commandLine = getLines(comment.body).find(function (line) {
return line.includes(botMentionPrefix)
})
if (!commandLine) {
return
}
if (!(await isRequesterAllowed(octokit, requester))) {
return getError(
"Requester could not be detected as a member of an allowed organization.",
)
}
const { execPath: botMention, ...command } = getCommand(
commandLine,
defaultTryRuntimeGetCommandOptions,
)
if (botMention !== botMentionPrefix) {
return
}
const [subCommand, ...otherArgs] = command.args
switch (subCommand) {
case "queue": {
const installationId = installation?.id
if (!installationId) {
return getError(
"Github Installation ID was not found in webhook payload",
)
}
if (getPullRequestTaskHandle(handleId) !== undefined) {
return getError(
"try-runtime is already being executed for this pull request",
)
}
const prResponse = await octokit.pulls.get(prParams)
if (prResponse.status !== 200) {
return getError(
`When trying to fetch the pull request, Github API responded with unexpected status ${
prResponse.status
}\n(${JSON.stringify(prResponse.data)})`,
)
}
const contributor = prResponse.data.head?.user?.login
if (!contributor) {
return getError(
`Failed to get branch owner from the Github API`,
)
}
const branch = prResponse.data.head?.ref
if (!branch) {
return getError(`Failed to get branch name from the Github API`)
}
const commentBody =
`Preparing try-runtime command for branch: \`${branch}\`. Comment will be updated.\n\n`.trim()
const commentCreationResponse = await createComment(octokit, {
...commentParams,
body: commentBody,
})
if (commentCreationResponse.status !== 201) {
return getError(
`When trying to create a comment in the pull request, Github API responded with unexpected status ${
prResponse.status
}\n(${JSON.stringify(commentCreationResponse.data)})`,
)
}
commentId = commentCreationResponse.id
const parsedArgs = getParsedArgs(nodesAddresses, otherArgs)
if (typeof parsedArgs === "string") {
return getError(parsedArgs)
}
const execPath = "cargo"
const args = [
"run",
// application requirement: always run the command in release mode
// see https://github.com/paritytech/try-runtime-bot/issues/26#issue-1049555966
"--release",
// "--quiet" should be kept so that the output doesn't get
// polluted with a bunch of compilation stuff; bear in mind the
// output is posted on Github comments which have limited
// character count
"--quiet",
"--features=try-runtime",
"try-runtime",
...parsedArgs,
]
const taskData: PullRequestTask = {
...prParams,
tag: "PullRequestTask",
handleId,
requester,
execPath,
args,
env: command.env,
commentId,
installationId,
gitRef: { owner, repo, contributor, branch },
version,
commandDisplay: displayCommand({
execPath,
args,
secretsToHide: [],
}),
timesRequeued: 0,
timesRequeuedSnapshotBeforeExecution: 0,
timesExecuted: 0,
repoPath: path.join(repositoryCloneDirectory, repo),
}
const message = await queue({
taskData,
onResult: getPostPullRequestResult({
taskData,
octokit,
state,
}),
state,
registerHandle: getRegisterPullRequestHandle(taskData),
})
await updateComment(octokit, {
...commentParams,
comment_id: commentId,
body: `${commentBody}\n${message}`,
})
break
}
case "cancel": {
const cancelItem = getPullRequestTaskHandle(handleId)
if (cancelItem === undefined) {
return getError(`No command is running for this pull request`)
}
const {
cancel,
task: { commentId },
} = cancelItem
await cancel()
await updateComment(octokit, {
...commentParams,
comment_id: commentId,
body: `@${requester} command was cancelled`.trim(),
})
break
}
default: {
return getError(`Unknown sub-command ${subCommand}`)
}
}
} catch (error) {
const cancelHandle = getPullRequestTaskHandle(handleId)
if (cancelHandle !== undefined) {
const { cancel } = cancelHandle
await cancel()
}
return getError(
`Exception caught in webhook handler\n${error.toString()}: ${
error.stack
}`,
)
}
})
}
return { onIssueCommentCreated }
}