blob: 1a18d94b085ec3822f8611889d326d793fb5ae7b [file] [log] [blame]
// Copyright 2019 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:async';
import 'package:appengine/appengine.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import '../datastore/cocoon_config.dart';
import '../model/appengine/task.dart';
import '../model/luci/buildbucket.dart';
import '../request_handling/api_request_handler.dart';
import 'buildbucket.dart';
part 'luci.g.dart';
const int _maxResults = 40;
const Map<Status, String> _luciStatusToTaskStatus = <Status, String>{
Status.unspecified: Task.statusInProgress,
Status.scheduled: Task.statusInProgress,
Status.started: Task.statusInProgress,
Status.canceled: Task.statusSkipped,
Status.success: Task.statusSucceeded,
Status.failure: Task.statusFailed,
Status.infraFailure: Task.statusFailed,
};
typedef LuciServiceProvider = LuciService Function(
ApiRequestHandler<dynamic> handler);
/// Service class for interacting with LUCI.
@immutable
class LuciService {
/// Creates a new [LuciService].
///
/// The [config] and [clientContext] arguments must not be null.
const LuciService({
@required this.config,
@required this.clientContext,
}) : assert(config != null),
assert(clientContext != null);
/// The Cocoon configuration. Guaranteed to be non-null.
final Config config;
/// The AppEngine context to use for requests. Guaranteed to be non-null.
final ClientContext clientContext;
/// Gets the list of recent LUCI tasks, broken out by the [BranchLuciBuilder]
/// that owns them.
///
/// The list of known LUCI builders is specified in [LuciBuilder.all].
Future<Map<BranchLuciBuilder, Map<String, List<LuciTask>>>>
getBranchRecentTasks({
String repo,
bool requireTaskName = false,
}) async {
assert(requireTaskName != null);
final List<LuciBuilder> builders = await LuciBuilder.getBuilders(config);
final Iterable<Build> builds =
await getBuilds(repo, requireTaskName, builders);
final Map<BranchLuciBuilder, Map<String, List<LuciTask>>> results =
<BranchLuciBuilder, Map<String, List<LuciTask>>>{};
for (Build build in builds) {
final String commit = build.input?.gitilesCommit?.hash ?? 'unknown';
final String ref = build.input?.gitilesCommit?.ref ?? 'unknown';
final LuciBuilder builder = builders.singleWhere((LuciBuilder builder) {
return builder.name == build.builderId.builder;
});
final BranchLuciBuilder branchLuciBuilder =
BranchLuciBuilder(luciBuilder: builder, branch: ref.split('/')[2]);
results[branchLuciBuilder] ??= <String, List<LuciTask>>{};
results[branchLuciBuilder][commit] ??= <LuciTask>[];
results[branchLuciBuilder][commit].add(LuciTask(
commitSha: commit,
ref: ref,
status: _luciStatusToTaskStatus[build.status],
buildNumber: build.number,
));
}
return results;
}
/// Gets the list of recent LUCI tasks, broken out by the [LuciBuilder] that
/// owns them.
///
/// The list of known LUCI builders is specified in [LuciBuilder.all].
Future<Map<LuciBuilder, List<LuciTask>>> getRecentTasks({
String repo,
bool requireTaskName = false,
}) async {
assert(requireTaskName != null);
final List<LuciBuilder> builders = await LuciBuilder.getBuilders(config);
final Iterable<Build> builds =
await getBuilds(repo, requireTaskName, builders);
final Map<LuciBuilder, List<LuciTask>> results =
<LuciBuilder, List<LuciTask>>{};
for (Build build in builds) {
final String commit = build.input?.gitilesCommit?.hash ?? 'unknown';
final String ref = build.input?.gitilesCommit?.ref ?? 'unknown';
final LuciBuilder builder = builders.singleWhere((LuciBuilder builder) {
return builder.name == build.builderId.builder;
});
results[builder] ??= <LuciTask>[];
results[builder].add(LuciTask(
commitSha: commit,
ref: ref,
status: _luciStatusToTaskStatus[build.status],
buildNumber: build.number,
));
}
return results;
}
/// Gets list of [build] for [repo] and available Luci [builders]
/// predefined in cocoon config.
///
/// Latest builds of each builder will be returned from new to old.
Future<Iterable<Build>> getBuilds(
String repo, bool requireTaskName, List<LuciBuilder> builders) async {
final BuildBucketClient buildBucketClient = BuildBucketClient();
bool includeBuilder(LuciBuilder builder) {
if (repo != null && builder.repo != repo) {
return false;
}
if (requireTaskName && builder.taskName == null) {
return false;
}
return true;
}
final List<Request> searchRequests =
builders.where(includeBuilder).map<Request>((LuciBuilder builder) {
return Request(
searchBuilds: SearchBuildsRequest(
pageSize: _maxResults,
predicate: BuildPredicate(
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: builder.name,
),
),
),
);
}).toList();
final BatchRequest batchRequest = BatchRequest(requests: searchRequests);
final BatchResponse batchResponse =
await buildBucketClient.batch(batchRequest);
final Iterable<Build> builds = batchResponse.responses
.map<SearchBuildsResponse>((Response response) => response.searchBuilds)
.expand<Build>((SearchBuildsResponse response) => response.builds);
return builds;
}
}
@immutable
class BranchLuciBuilder {
const BranchLuciBuilder({
this.luciBuilder,
this.branch,
});
final String branch;
final LuciBuilder luciBuilder;
}
@immutable
@JsonSerializable()
class LuciBuilder {
const LuciBuilder({
@required this.name,
@required this.repo,
this.taskName,
}) : assert(name != null);
/// Create a new [LuciBuilder] object from its JSON representation.
factory LuciBuilder.fromJson(Map<String, dynamic> json) =>
_$LuciBuilderFromJson(json);
/// The name of this builder.
@JsonKey(required: true, disallowNullValue: true)
final String name;
/// The name of the repository for which this builder runs.
@JsonKey(required: true, disallowNullValue: true)
final String repo;
/// The name of the devicelab task associated with this builder.
@JsonKey()
final String taskName;
/// Serializes this object to a JSON primitive.
Map<String, dynamic> toJson() => _$LuciBuilderToJson(this);
/// Loads and returns the list of known builders from the Cocoon [config].
static Future<List<LuciBuilder>> getBuilders(Config config) async {
final List<dynamic> builders = config.luciBuilders;
return builders
.map<LuciBuilder>((dynamic json) =>
LuciBuilder.fromJson(json as Map<String, dynamic>))
.toList();
}
}
@immutable
class LuciTask {
const LuciTask({
@required this.commitSha,
@required this.ref,
@required this.status,
@required this.buildNumber,
}) : assert(commitSha != null),
assert(ref != null),
assert(status != null),
assert(buildNumber != null);
/// The GitHub commit at which this task is being run.
final String commitSha;
// The GitHub ref at which this task is being run.
final String ref;
/// The status of this task. See the [Task] class for supported values.
final String status;
/// The build number of this task.
final int buildNumber;
}