blob: 91ce315f82926303bf552d227f1bc0b84b03ee17 [file] [log] [blame]
// 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);
}
}