| // Copyright 2020 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert'; |
| |
| import 'package:appengine/appengine.dart'; |
| import 'package:cocoon_service/src/foundation/github_checks_util.dart'; |
| import 'package:cocoon_service/src/model/github/checks.dart'; |
| import 'package:cocoon_service/src/model/luci/buildbucket.dart'; |
| import 'package:github/github.dart' as github; |
| |
| import '../../cocoon_service.dart'; |
| import '../model/luci/push_message.dart' as push_message; |
| import 'luci_build_service.dart'; |
| |
| /// Controls triggering builds and updating their status in the Github UI. |
| class GithubChecksService { |
| GithubChecksService(this.config, {GithubChecksUtil githubChecksUtil}) |
| : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil(); |
| |
| Config config; |
| GithubChecksUtil githubChecksUtil; |
| Logging log; |
| |
| static Set<github.CheckRunConclusion> failedStatesSet = |
| <github.CheckRunConclusion>{ |
| github.CheckRunConclusion.cancelled, |
| github.CheckRunConclusion.failure, |
| }; |
| |
| // This method has to be called before calling any other methods. |
| void setLogger(Logging log) { |
| this.log = log; |
| } |
| |
| /// Takes a [CheckSuiteEvent] and trigger all the relevant builds if this is a |
| /// new commit or only failed builds if the event was generated by a click on |
| /// the re-run all button in the Github UI. |
| /// Relevant API docs: |
| /// https://docs.github.com/en/rest/reference/checks#create-a-check-suite |
| /// https://docs.github.com/en/rest/reference/checks#rerequest-a-check-suite |
| Future<void> handleCheckSuite(CheckSuiteEvent checkSuiteEvent, |
| LuciBuildService luciBuilderService) async { |
| final github.RepositorySlug slug = checkSuiteEvent.repository.slug(); |
| final github.GitHub gitHubClient = |
| await config.createGitHubClient(slug.owner, slug.name); |
| final github.PullRequest pullRequest = |
| checkSuiteEvent.checkSuite.pullRequests[0]; |
| final int pullRequestNumber = pullRequest.number; |
| final String commitSha = checkSuiteEvent.checkSuite.headSha; |
| switch (checkSuiteEvent.action) { |
| case 'requested': |
| // Trigger all try builders. |
| await luciBuilderService.scheduleBuilds( |
| prNumber: pullRequestNumber, |
| commitSha: commitSha, |
| slug: checkSuiteEvent.repository.slug(), |
| checkSuiteEvent: checkSuiteEvent, |
| ); |
| break; |
| |
| case 'rerequested': |
| // Trigger only the builds that failed. |
| final List<Build> builds = await luciBuilderService.failedBuilds( |
| slug, pullRequestNumber, commitSha); |
| final Map<String, github.CheckRun> checkRuns = |
| await githubChecksUtil.allCheckRuns( |
| gitHubClient, |
| checkSuiteEvent, |
| ); |
| |
| for (Build build in builds) { |
| final github.CheckRun checkRun = checkRuns[build.builderId.builder]; |
| await luciBuilderService.rescheduleUsingCheckSuiteEvent( |
| checkSuiteEvent, |
| checkRun, |
| ); |
| } |
| break; |
| } |
| } |
| |
| /// Reschedules a failed build using a [CheckRunEvent]. The CheckRunEvent is |
| /// generated when someone clicks the re-run button from a failed build from |
| /// the Github UI. |
| /// Relevant APIs: |
| /// https://developer.github.com/v3/checks/runs/#check-runs-and-requested-actions |
| Future<void> handleCheckRun( |
| CheckRunEvent checkRunEvent, LuciBuildService luciBuildService) async { |
| switch (checkRunEvent.action) { |
| case 'rerequested': |
| final String builderName = checkRunEvent.checkRun.name; |
| final bool success = |
| await luciBuildService.rescheduleUsingCheckRunEvent(checkRunEvent); |
| log.debug('BuilderName: $builderName State: $success'); |
| } |
| } |
| |
| /// Updates the Github build status using a [BuildPushMessage] sent by LUCI in |
| /// a pub/sub notification. |
| /// Relevant APIs: |
| /// https://docs.github.com/en/rest/reference/checks#update-a-check-run |
| Future<bool> updateCheckStatus( |
| push_message.BuildPushMessage buildPushMessage, |
| LuciBuildService luciBuildService, |
| github.RepositorySlug slug, |
| ) async { |
| final github.GitHub gitHubClient = |
| await config.createGitHubClient(slug.owner, slug.name); |
| final push_message.Build build = buildPushMessage.build; |
| if (buildPushMessage.userData.isEmpty) { |
| return false; |
| } |
| final Map<String, dynamic> userData = |
| jsonDecode(buildPushMessage.userData) as Map<String, dynamic>; |
| if (!userData.containsKey('check_run_id') || |
| !userData.containsKey('repo_owner') || |
| !userData.containsKey('repo_name')) { |
| log.error( |
| 'UserData did not contain check_run_id,' |
| 'repo_owner, or repo_name: $userData', |
| ); |
| return false; |
| } |
| final github.CheckRun checkRun = await githubChecksUtil.getCheckRun( |
| gitHubClient, |
| slug, |
| userData['check_run_id'] as int, |
| ); |
| final github.CheckRunStatus status = statusForResult(build.status); |
| final github.CheckRunConclusion conclusion = |
| (buildPushMessage.build.result != null) |
| ? conclusionForResult(buildPushMessage.build.result) |
| : null; |
| // Do not override url for completed status. |
| final String url = status == github.CheckRunStatus.completed |
| ? checkRun.detailsUrl |
| : buildPushMessage.build.url; |
| github.CheckRunOutput output; |
| // If status has completed with failure then provide more details. |
| if (status == github.CheckRunStatus.completed && |
| failedStatesSet.contains(conclusion)) { |
| final Build build = await luciBuildService.getBuildById( |
| buildPushMessage.build.id, |
| fields: 'id,builder,summaryMarkdown'); |
| output = github.CheckRunOutput( |
| title: checkRun.name, summary: build.summaryMarkdown); |
| } |
| await githubChecksUtil.updateCheckRun( |
| gitHubClient, |
| slug, |
| checkRun, |
| status: status, |
| conclusion: conclusion, |
| detailsUrl: url, |
| output: output, |
| ); |
| return true; |
| } |
| |
| /// Transforms a [push_message.Result] to a [github.CheckRunConclusion]. |
| /// Relevant APIs: |
| /// https://developer.github.com/v3/checks/runs/#check-runs |
| github.CheckRunConclusion conclusionForResult(push_message.Result result) { |
| switch (result) { |
| case push_message.Result.canceled: |
| return github.CheckRunConclusion.cancelled; |
| case push_message.Result.failure: |
| return github.CheckRunConclusion.failure; |
| case push_message.Result.success: |
| return github.CheckRunConclusion.success; |
| } |
| throw StateError('unreachable'); |
| } |
| |
| /// Transforms a [ush_message.Status] to a [github.CheckRunStatus]. |
| /// Relevant APIs: |
| /// https://developer.github.com/v3/checks/runs/#check-runs |
| github.CheckRunStatus statusForResult(push_message.Status status) { |
| switch (status) { |
| case push_message.Status.completed: |
| return github.CheckRunStatus.completed; |
| case push_message.Status.scheduled: |
| return github.CheckRunStatus.queued; |
| case push_message.Status.started: |
| return github.CheckRunStatus.inProgress; |
| } |
| throw StateError('unreachable'); |
| } |
| } |