@@ -37,9 +37,29 @@ trait PullRequestService {
37
37
/** Pull Request HTTP service */
38
38
val prService = HttpService {
39
39
case request @ POST -> Root =>
40
- request.as(jsonOf[Issue ]).flatMap(checkPullRequest)
40
+ val githubEvent =
41
+ request.headers
42
+ .get(CaseInsensitiveString (" X-GitHub-Event" ))
43
+ .map(_.value).getOrElse(" " )
44
+
45
+ githubEvent match {
46
+ case " pull_request" =>
47
+ request.as(jsonOf[Issue ]).flatMap(checkPullRequest)
48
+
49
+ case " issue_comment" =>
50
+ request.as(jsonOf[IssueComment ]).flatMap(respondToComment)
51
+
52
+ case " " =>
53
+ BadRequest (" Missing header: X-Github-Event" )
54
+
55
+ case event =>
56
+ BadRequest (" Unsupported event: $event" )
57
+
58
+ }
41
59
}
42
60
61
+ private [this ] val droneContext = " continuous-integration/drone/pr"
62
+
43
63
private final case class CLASignature (
44
64
user : String ,
45
65
signed : Boolean ,
@@ -148,7 +168,7 @@ trait PullRequestService {
148
168
}
149
169
150
170
/** Ordered from earliest to latest */
151
- def getCommits (issueNbr : Int , httpClient : Client ): Task [List [Commit ]] = {
171
+ def getCommits (issueNbr : Int )( implicit httpClient : Client ): Task [List [Commit ]] = {
152
172
def makeRequest (url : String ): Task [List [Commit ]] =
153
173
for {
154
174
res <- httpClient.fetch(get(url)) { res =>
@@ -246,10 +266,10 @@ trait PullRequestService {
246
266
}
247
267
248
268
def checkFreshPR (issue : Issue ): Task [Response ] = {
249
- val httpClient = PooledHttp1Client ()
269
+ implicit val httpClient = PooledHttp1Client ()
250
270
251
271
for {
252
- commits <- getCommits(issue.number, httpClient )
272
+ commits <- getCommits(issue.number)
253
273
statuses <- checkCLA(commits, httpClient)
254
274
255
275
(validStatuses, invalidStatuses) = statuses.partition(_.isValid)
@@ -280,9 +300,31 @@ trait PullRequestService {
280
300
private def extractCommitSha (status : StatusResponse ): Task [String ] =
281
301
Task .delay(status.sha)
282
302
303
+ def startBuild (commit : Commit )(implicit client : Client ): Task [Drone .Build ] = {
304
+ def pendingStatus (targetUrl : String ): Status =
305
+ Status (" pending" , targetUrl, " build restarted by bot" , droneContext)
306
+
307
+ def filterStatuses (xs : List [StatusResponse ]): Task [Int ] =
308
+ xs.filter { status =>
309
+ (status.state == " failure" || status.state == " success" ) &&
310
+ status.context == droneContext
311
+ }
312
+ .map(status => Task .now(status.target_url.split('/' ).last.toInt))
313
+ .headOption
314
+ .getOrElse(Task .fail(new NoSuchElementException (" Couldn't find drone build for PR" )))
315
+
316
+ for {
317
+ statuses <- getStatus(commit, client)
318
+ failed <- filterStatuses(statuses)
319
+ build <- Drone .startBuild(failed, droneToken)
320
+ setStatusReq <- post(statusUrl(commit.sha)).withAuth(githubUser, githubToken)
321
+ newStatus = pendingStatus(s " http://dotty-ci.epfl.ch/lampepfl/dotty/ $failed" ).asJson
322
+ _ <- client.expect(setStatusReq.withBody(newStatus))(jsonOf[StatusResponse ])
323
+ } yield build
324
+ }
325
+
283
326
def cancelBuilds (commits : List [Commit ])(implicit client : Client ): Task [Boolean ] =
284
327
Task .gatherUnordered {
285
- val droneContext = " continuous-integration/drone/pr"
286
328
commits.map { commit =>
287
329
for {
288
330
statuses <- getStatus(commit, client)
@@ -295,10 +337,10 @@ trait PullRequestService {
295
337
.map(xs => xs.foldLeft(true )(_ == _))
296
338
297
339
def checkSynchronize (issue : Issue ): Task [Response ] = {
298
- val httpClient = PooledHttp1Client ()
340
+ implicit val httpClient = PooledHttp1Client ()
299
341
300
342
for {
301
- commits <- getCommits(issue.number, httpClient )
343
+ commits <- getCommits(issue.number)
302
344
statuses <- checkCLA(commits, httpClient)
303
345
invalid = statuses.filterNot(_.isValid)
304
346
_ <- sendStatuses(invalid, httpClient)
@@ -318,8 +360,80 @@ trait PullRequestService {
318
360
319
361
def checkPullRequest (issue : Issue ): Task [Response ] =
320
362
issue.action match {
321
- case " opened" => checkFreshPR(issue)
322
- case " synchronize" => checkSynchronize(issue)
323
- case action => BadRequest (s " Unhandled action: $action" )
363
+ case Some (" opened" ) => checkFreshPR(issue)
364
+ case Some (" synchronize" ) => checkSynchronize(issue)
365
+ case Some (action) => BadRequest (s " Unhandled action: $action" )
366
+ case None => BadRequest (" Cannot check pull request, missing action field" )
367
+ }
368
+
369
+ def restartCI (issue : Issue ): Task [Response ] = {
370
+ implicit val client = PooledHttp1Client ()
371
+
372
+ def restartedComment : Comment = {
373
+ import scala .util .Random
374
+ val answers = Array (
375
+ " Okidokey, boss! :clap:" ,
376
+ " You got it, homie! :pray:" ,
377
+ " No problem, big shot! :punch:" ,
378
+ " Sure thing, I got your back! :heart:" ,
379
+ " No WAY! :-1: ...wait, don't fire me please! There, I did it! :tada:"
380
+ )
381
+
382
+ Comment (Author (None ), answers(Random .nextInt(answers.length)))
324
383
}
384
+
385
+ for {
386
+ commits <- getCommits(issue.number)
387
+ latest = commits.last
388
+ _ <- cancelBuilds(latest :: Nil )
389
+ _ <- startBuild(latest)
390
+ req <- post(issueCommentsUrl(issue.number)).withAuth(githubUser, githubToken)
391
+ _ <- client.fetch(req.withBody(restartedComment.asJson))(Task .now)
392
+ res <- Ok (" Replied to request for CI restart" )
393
+ } yield res
394
+ }
395
+
396
+ def cannotUnderstand (line : String , issueComment : IssueComment ): Task [Response ] = {
397
+ implicit val client = PooledHttp1Client ()
398
+ val comment = Comment (Author (None ), {
399
+ s """ Hey, sorry - I could not understand what you meant by:
400
+ |
401
+ |> $line
402
+ |
403
+ |I'm just a dumb bot after all :cry:
404
+ |
405
+ |I mostly understand when your mention contains these words:
406
+ |
407
+ |- (re)check (the) cla
408
+ |- recheck
409
+ |- restart drone
410
+ |
411
+ |Maybe if you want to make me smarter, you could open a PR? :heart_eyes:
412
+ | """ .stripMargin
413
+ })
414
+ for {
415
+ req <- post(issueCommentsUrl(issueComment.issue.number)).withAuth(githubUser, githubToken)
416
+ _ <- client.fetch(req.withBody(comment.asJson))(Task .now)
417
+ res <- Ok (" Delivered could not understand comment" )
418
+ } yield res
419
+ }
420
+
421
+ def extractMention (body : String ): Option [String ] =
422
+ body.lines.find(_.startsWith(" @dotty-bot:" ))
423
+
424
+ /** TODO: The implementation here could be quite elegant if we used a trie instead */
425
+ def interpretMention (line : String , issueComment : IssueComment ): Task [Response ] = {
426
+ val loweredLine = line.toLowerCase
427
+ if (loweredLine.contains(" check cla" ) || loweredLine.contains(" check the cla" ))
428
+ checkSynchronize(issueComment.issue)
429
+ else if (loweredLine.contains(" recheck" ) || loweredLine.contains(" restart drone" ))
430
+ restartCI(issueComment.issue)
431
+ else
432
+ cannotUnderstand(line, issueComment)
433
+ }
434
+
435
+ def respondToComment (issueComment : IssueComment ): Task [Response ] =
436
+ extractMention(issueComment.comment.body)
437
+ .map(interpretMention(_, issueComment))
438
+ .getOrElse(Ok (" Nothing to do here, move along!" ))
325
439
}
0 commit comments