| // 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/request_handling/exceptions.dart'; |
| import 'package:github/github.dart' as github; |
| import 'package:meta/meta.dart'; |
| |
| import '../../cocoon_service.dart'; |
| import '../model/appengine/service_account_info.dart'; |
| import '../model/luci/buildbucket.dart'; |
| import '../model/luci/push_message.dart' as push_message; |
| import 'buildbucket.dart'; |
| |
| /// Class to interact with LUCI buildbucket to get, trigger |
| /// and cancel builds for github repos. It uses [config.luciTryBuilders] to |
| /// get the list of available builders. |
| class LuciBuildService { |
| LuciBuildService(this.config, this.buildBucketClient, this.serviceAccount, |
| {GithubChecksUtil githubChecksUtil}) |
| : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil(); |
| |
| BuildBucketClient buildBucketClient; |
| Config config; |
| ServiceAccountInfo serviceAccount; |
| Logging log; |
| GithubChecksUtil githubChecksUtil; |
| |
| static const Set<Status> failStatusSet = <Status>{ |
| Status.canceled, |
| Status.failure, |
| Status.infraFailure |
| }; |
| |
| /// Sets the appengine [log] used by this class to log debug and error |
| /// messages. This method has to be called before any other method in this |
| /// class. |
| void setLogger(Logging log) { |
| this.log = log; |
| } |
| |
| /// Returns a map of the BuildBucket builds for a given Github [slug] |
| /// [prNumber] and [commitSha] using the [builderName] as key and [Build] |
| /// as value. |
| Future<Map<String, Build>> buildsForRepositoryAndPr( |
| github.RepositorySlug slug, |
| int prNumber, |
| String commitSha, |
| ) async { |
| final BatchResponse batch = |
| await buildBucketClient.batch(BatchRequest(requests: <Request>[ |
| Request( |
| searchBuilds: SearchBuildsRequest( |
| predicate: BuildPredicate( |
| builderId: const BuilderId( |
| project: 'flutter', |
| bucket: 'try', |
| ), |
| createdBy: serviceAccount.email, |
| tags: <String, List<String>>{ |
| 'buildset': <String>['pr/git/$prNumber'], |
| 'github_link': <String>[ |
| 'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber' |
| ], |
| 'user_agent': const <String>['flutter-cocoon'], |
| }, |
| ), |
| ), |
| ), |
| Request( |
| searchBuilds: SearchBuildsRequest( |
| predicate: BuildPredicate( |
| builderId: const BuilderId( |
| project: 'flutter', |
| bucket: 'try', |
| ), |
| tags: <String, List<String>>{ |
| 'buildset': <String>['pr/git/$prNumber'], |
| 'user_agent': const <String>['recipe'], |
| }, |
| ), |
| ), |
| ), |
| ])); |
| final Iterable<Build> builds = batch.responses |
| .map((Response response) => response.searchBuilds) |
| .expand( |
| (SearchBuildsResponse response) => response.builds ?? <Build>[]); |
| return Map<String, Build>.fromIterable(builds, |
| key: (dynamic b) => b.builderId.builder as String, |
| value: (dynamic b) => b as Build); |
| } |
| |
| /// Schedules BuildBucket builds for a given [prNumber], [commitSha] |
| /// and Github [slug]. |
| Future<bool> scheduleBuilds({ |
| @required int prNumber, |
| @required String commitSha, |
| @required github.RepositorySlug slug, |
| CheckSuiteEvent checkSuiteEvent, |
| }) async { |
| assert(prNumber != null); |
| assert(commitSha != null); |
| assert(slug != null); |
| final github.GitHub githubClient = |
| await config.createGitHubClient(slug.owner, slug.name); |
| if (!config.githubPresubmitSupportedRepo(slug.name)) { |
| throw BadRequestException( |
| 'Repository ${slug.name} is not supported by this service.'); |
| } |
| |
| final Map<String, Build> builds = await buildsForRepositoryAndPr( |
| slug, |
| prNumber, |
| commitSha, |
| ); |
| if (builds != null && |
| builds.values.any((Build build) { |
| return build.status == Status.scheduled || |
| build.status == Status.started; |
| })) { |
| log.error( |
| 'Either builds are empty or they are already scheduled or started. ' |
| 'PR: $prNumber, Commit: $commitSha, Owner: ${slug.owner} ' |
| 'Repo: ${slug.name}'); |
| return false; |
| } |
| |
| final List<Map<String, dynamic>> builders = config.luciTryBuilders; |
| final List<String> builderNames = builders |
| .where((Map<String, dynamic> builder) => builder['repo'] == slug.name) |
| .map<String>( |
| (Map<String, dynamic> builder) => builder['name'] as String) |
| .toList(); |
| if (builderNames.isEmpty) { |
| throw InternalServerError('${slug.name} does not have any builders'); |
| } |
| |
| final List<Request> requests = <Request>[]; |
| for (String builder in builderNames) { |
| final BuilderId builderId = BuilderId( |
| project: 'flutter', |
| bucket: 'try', |
| builder: builder, |
| ); |
| final Map<String, dynamic> userData = <String, dynamic>{ |
| 'repo_owner': slug.owner, |
| 'repo_name': slug.name, |
| 'user_agent': 'flutter-cocoon', |
| }; |
| if (checkSuiteEvent != null) { |
| final github.CheckRun checkRun = |
| await githubClient.checks.checkRuns.createCheckRun( |
| checkSuiteEvent.repository.slug(), |
| name: builder, |
| headSha: commitSha, |
| ); |
| userData['check_suite_id'] = checkSuiteEvent.checkSuite.id; |
| userData['check_run_id'] = checkRun.id; |
| } |
| requests.add( |
| Request( |
| scheduleBuild: ScheduleBuildRequest( |
| builderId: builderId, |
| tags: <String, List<String>>{ |
| 'buildset': <String>['pr/git/$prNumber', 'sha/git/$commitSha'], |
| 'user_agent': const <String>['flutter-cocoon'], |
| 'github_link': <String>[ |
| 'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber' |
| ], |
| }, |
| properties: <String, String>{ |
| 'git_url': 'https://github.com/${slug.owner}/${slug.name}', |
| 'git_ref': 'refs/pull/$prNumber/head', |
| }, |
| notify: NotificationConfig( |
| pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds', |
| userData: json.encode(userData), |
| ), |
| ), |
| ), |
| ); |
| } |
| await buildBucketClient.batch(BatchRequest(requests: requests)); |
| return true; |
| } |
| |
| /// Cancels all the current builds for a given [repositoryName], [prNumber] |
| /// and [commitSha] adding a message for the cancelation reason. |
| Future<void> cancelBuilds(github.RepositorySlug slug, int prNumber, |
| String commitSha, String reason) async { |
| if (!config.githubPresubmitSupportedRepo(slug.name)) { |
| throw BadRequestException( |
| 'This service does not support repository ${slug.name}'); |
| } |
| final Map<String, Build> builds = await buildsForRepositoryAndPr( |
| slug, |
| prNumber, |
| commitSha, |
| ); |
| if (builds == null || |
| !builds.values.any((Build build) { |
| return build.status == Status.scheduled || |
| build.status == Status.started; |
| })) { |
| return; |
| } |
| final List<Request> requests = <Request>[]; |
| for (Build build in builds.values) { |
| requests.add( |
| Request( |
| cancelBuild: |
| CancelBuildRequest(id: build.id, summaryMarkdown: reason), |
| ), |
| ); |
| } |
| await buildBucketClient.batch(BatchRequest(requests: requests)); |
| } |
| |
| /// Gets a list of failed builds for a given [repositoryName], [prNumber] and |
| /// [commitSha]. |
| Future<List<Build>> failedBuilds( |
| github.RepositorySlug slug, |
| int prNumber, |
| String commitSha, |
| ) async { |
| final Map<String, Build> builds = |
| await buildsForRepositoryAndPr(slug, prNumber, commitSha); |
| final List<String> builderNames = config.luciTryBuilders |
| .map((Map<String, dynamic> entry) => entry['name'] as String) |
| .toList(); |
| // Return only builds that exist in the configuration file. |
| return builds.values |
| .where((Build build) => |
| failStatusSet.contains(build.status) && |
| builderNames.contains(build.builderId.builder)) |
| .toList(); |
| } |
| |
| /// Sends a [BuildBucket.scheduleBuild] the buildset, user_agent, and |
| /// github_link tags are applied to match the original build. The build |
| /// properties from the original build are also preserved. |
| Future<bool> rescheduleBuild({ |
| @required String commitSha, |
| @required String builderName, |
| @required push_message.BuildPushMessage buildPushMessage, |
| }) async { |
| // Ensure we are using V2 bucket name istead of V1. |
| // V1 bucket name is "luci.flutter.prod" while the api |
| // is expecting just the last part after "."(prod). |
| final String bucketName = buildPushMessage.build.bucket.split('.').last; |
| final Map<String, dynamic> userData = |
| jsonDecode(buildPushMessage.userData) as Map<String, dynamic>; |
| await buildBucketClient.scheduleBuild(ScheduleBuildRequest( |
| builderId: BuilderId( |
| project: buildPushMessage.build.project, |
| bucket: bucketName, |
| builder: builderName, |
| ), |
| tags: <String, List<String>>{ |
| 'buildset': buildPushMessage.build.tagsByName('buildset'), |
| 'user_agent': buildPushMessage.build.tagsByName('user_agent'), |
| 'github_link': buildPushMessage.build.tagsByName('github_link'), |
| }, |
| properties: (buildPushMessage.build.buildParameters['properties'] |
| as Map<String, dynamic>) |
| .cast<String, String>(), |
| notify: NotificationConfig( |
| pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds', |
| userData: json.encode(userData), |
| ), |
| )); |
| return true; |
| } |
| |
| /// Sends a [BuildBucket.scheduleBuild] request using [CheckRunEvent]. It |
| /// returns [true] if it is able to send the scheduleBuildRequest or [false] |
| /// if not. |
| Future<bool> rescheduleUsingCheckRunEvent(CheckRunEvent checkRunEvent) async { |
| final github.RepositorySlug slug = checkRunEvent.repository.slug(); |
| final Map<String, dynamic> userData = <String, dynamic>{}; |
| final github.PullRequest pr = checkRunEvent.checkRun.pullRequests[0]; |
| final github.GitHub gitHubClient = |
| await config.createGitHubClient(slug.owner, slug.name); |
| final github.CheckRun githubCheckRun = |
| await githubChecksUtil.createCheckRun( |
| gitHubClient, |
| slug, |
| checkRunEvent.checkRun.name, |
| pr.head.sha, |
| ); |
| userData['check_suite_id'] = checkRunEvent.checkRun.checkSuite.id; |
| userData['check_run_id'] = githubCheckRun.id; |
| userData['repo_owner'] = slug.owner; |
| userData['repo_name'] = slug.name; |
| userData['user_agent'] = 'flutter-cocoon'; |
| await buildBucketClient.scheduleBuild(ScheduleBuildRequest( |
| builderId: BuilderId( |
| project: 'flutter', |
| bucket: 'try', |
| builder: checkRunEvent.checkRun.name, |
| ), |
| tags: <String, List<String>>{ |
| 'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'], |
| 'user_agent': const <String>['flutter-cocoon'], |
| 'github_link': <String>[ |
| 'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}' |
| ], |
| }, |
| properties: <String, String>{ |
| 'git_url': 'https://github.com/${slug.owner}/${slug.name}', |
| 'git_ref': 'refs/pull/${pr.number}/head', |
| }, |
| notify: NotificationConfig( |
| pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds', |
| userData: json.encode(userData), |
| ), |
| )); |
| return true; |
| } |
| |
| /// Sends a [BuildBucket.scheduleBuild] request using [CheckSuiteEvent], |
| /// [gitgub.CheckRun] and [RepositorySlug]. It returns [true] if it is able to |
| /// send the scheduleBuildRequest or [false] if not. |
| Future<bool> rescheduleUsingCheckSuiteEvent( |
| CheckSuiteEvent checkSuiteEvent, github.CheckRun checkRun) async { |
| final github.RepositorySlug slug = checkSuiteEvent.repository.slug(); |
| final Map<String, dynamic> userData = <String, dynamic>{}; |
| final github.PullRequest pr = checkSuiteEvent.checkSuite.pullRequests[0]; |
| final github.GitHub gitHubClient = |
| await config.createGitHubClient(slug.owner, slug.name); |
| final github.CheckRun githubCheckRun = |
| await githubChecksUtil.createCheckRun( |
| gitHubClient, |
| slug, |
| checkRun.name, |
| pr.head.sha, |
| ); |
| userData['check_suite_id'] = checkSuiteEvent.checkSuite.id; |
| userData['check_run_id'] = githubCheckRun.id; |
| userData['repo_owner'] = slug.owner; |
| userData['repo_name'] = slug.name; |
| userData['user_agent'] = 'flutter-cocoon'; |
| await buildBucketClient.scheduleBuild(ScheduleBuildRequest( |
| builderId: BuilderId( |
| project: 'flutter', |
| bucket: 'try', |
| builder: checkRun.name, |
| ), |
| tags: <String, List<String>>{ |
| 'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'], |
| 'user_agent': const <String>['flutter-cocoon'], |
| 'github_link': <String>[ |
| 'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}' |
| ], |
| }, |
| properties: <String, String>{ |
| 'git_url': 'https://github.com/${slug.owner}/${slug.name}', |
| 'git_ref': 'refs/pull/${pr.number}/head', |
| }, |
| notify: NotificationConfig( |
| pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds', |
| userData: json.encode(userData), |
| ), |
| )); |
| return true; |
| } |
| |
| /// Gets a [buildbucket.Build] using its [id] and passing the additional |
| /// fields to be populated in the response. |
| Future<Build> getBuildById(int id, {String fields}) async { |
| final GetBuildRequest request = GetBuildRequest(id: id, fields: fields); |
| return buildBucketClient.getBuild(request); |
| } |
| } |