blob: 5964a011fe9f586794f1f3344caa7fdebd3b9cfd [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:convert';
import 'package:buildbucket/buildbucket_pb.dart' as bbv2;
import 'package:cocoon_service/src/model/luci/user_data.dart';
import 'package:cocoon_service/src/request_handling/subscription_handler_v2.dart';
import 'package:cocoon_service/src/service/github_checks_service_v2.dart';
import 'package:cocoon_service/src/service/luci_build_service_v2.dart';
import 'package:cocoon_service/src/service/scheduler_v2.dart';
import 'package:github/github.dart';
import 'package:meta/meta.dart';
import '../model/appengine/commit.dart';
import '../model/ci_yaml/ci_yaml.dart';
import '../model/ci_yaml/target.dart';
import '../request_handling/authentication.dart';
import '../request_handling/body.dart';
import '../service/config.dart';
import '../service/logging.dart';
/// An endpoint for listening to LUCI status updates for scheduled builds.
///
/// [ScheduleBuildRequest.notify] property is set to tell LUCI to use this
/// PubSub topic. LUCI then publishes updates about build status to that topic,
/// which we listen to on the github-updater subscription. When new messages
/// arrive, they are posted to this web service.
///
/// The PubSub subscription is set up here:
/// https://console.cloud.google.com/cloudpubsub/subscription/detail/build-bucket-presubmit-sub?project=flutter-dashboard
///
/// This endpoint is responsible for updating GitHub with the status of
/// completed builds from LUCI.
@immutable
class PresubmitLuciSubscriptionV2 extends SubscriptionHandlerV2 {
/// Creates an endpoint for listening to LUCI status updates.
const PresubmitLuciSubscriptionV2({
required super.cache,
required super.config,
required this.scheduler,
required this.luciBuildService,
required this.githubChecksService,
AuthenticationProvider? authProvider,
}) : super(subscriptionName: 'build-bucket-presubmit-sub');
final LuciBuildServiceV2 luciBuildService;
final GithubChecksServiceV2 githubChecksService;
final SchedulerV2 scheduler;
@override
Future<Body> post() async {
if (message.data == null) {
log.info('no data in message');
return Body.empty;
}
final bbv2.PubSubCallBack pubSubCallBack = bbv2.PubSubCallBack();
pubSubCallBack.mergeFromProto3Json(jsonDecode(message.data!) as Map<String, dynamic>);
final bbv2.BuildsV2PubSub buildsV2PubSub = pubSubCallBack.buildPubsub;
if (!buildsV2PubSub.hasBuild()) {
log.info('no build information in message');
return Body.empty;
}
final bbv2.Build build = buildsV2PubSub.build;
final String builderName = build.builder.builder;
final List<bbv2.StringPair> tags = build.tags;
log.fine('Available tags: ${tags.toString()}');
// Skip status update if we can not get the sha tag.
if (tags.where((element) => element.key == 'buildset').isEmpty) {
log.warning('Buildset tag not included, skipping Status Updates');
return Body.empty;
}
log.fine('Setting status (${build.status.toString()}) for $builderName');
if (!pubSubCallBack.hasUserData()) {
log.info('No user data was found in this request');
return Body.empty;
}
Map<String, dynamic> userDataMap = <String, dynamic>{};
try {
userDataMap = json.decode(String.fromCharCodes(pubSubCallBack.userData));
log.info('User data was not base64 encoded.');
} on FormatException {
userDataMap = UserData.decodeUserDataBytes(pubSubCallBack.userData);
log.info('Decoding base64 encoded user data.');
}
if (userDataMap.containsKey('repo_owner') && userDataMap.containsKey('repo_name')) {
final RepositorySlug slug =
RepositorySlug(userDataMap['repo_owner'] as String, userDataMap['repo_name'] as String);
bool rescheduled = false;
if (githubChecksService.taskFailed(build.status)) {
final int currentAttempt = githubChecksService.currentAttempt(tags);
final int maxAttempt = await _getMaxAttemptV2(
userDataMap,
slug,
builderName,
);
if (currentAttempt < maxAttempt) {
rescheduled = true;
log.fine('Rerunning failed task: $builderName');
await luciBuildService.rescheduleBuild(
builderName: builderName,
build: build,
rescheduleAttempt: currentAttempt + 1,
userDataMap: userDataMap,
);
}
}
await githubChecksService.updateCheckStatus(
build: build,
userDataMap: userDataMap,
luciBuildService: luciBuildService,
slug: slug,
rescheduled: rescheduled,
);
} else {
log.info('This repo does not support checks API');
}
return Body.empty;
}
Future<int> _getMaxAttemptV2(
Map<String, dynamic> userData,
RepositorySlug slug,
String builderName,
) async {
final Commit commit = Commit(
branch: userData['commit_branch'] as String,
repository: slug.fullName,
sha: userData['commit_sha'] as String,
);
late CiYaml ciYaml;
if (commit.branch == Config.defaultBranch(commit.slug)) {
ciYaml = await scheduler.getCiYaml(commit, validate: true);
} else {
ciYaml = await scheduler.getCiYaml(commit);
}
// Do not block on the target not found.
if (!ciYaml.presubmitTargets.any((element) => element.value.name == builderName)) {
// do not reschedule
log.warning('Did not find builder with name: $builderName in ciYaml for ${commit.sha}');
final List<String> availableBuilderList = ciYaml.presubmitTargets.map((Target e) => e.value.name).toList();
log.warning('ciYaml presubmit targets found: $availableBuilderList');
return 1;
}
final Target target = ciYaml.presubmitTargets.where((element) => element.value.name == builderName).single;
final Map<String, Object> properties = target.getProperties();
if (!properties.containsKey('presubmit_max_attempts')) {
return 1;
}
return properties['presubmit_max_attempts'] as int;
}
}