blob: 63d48410661e63a58eec770e5d6bc6296b6a19c4 [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 '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 '../model/luci/push_message.dart';
import '../request_handling/authentication.dart';
import '../request_handling/body.dart';
import '../request_handling/subscription_handler.dart';
import '../service/buildbucket.dart';
import '../service/config.dart';
import '../service/github_checks_service.dart';
import '../service/logging.dart';
import '../service/luci_build_service.dart';
import '../service/scheduler.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/github-updater?project=flutter-dashboard
///
/// This endpoint is responsible for updating GitHub with the status of
/// completed builds from LUCI.
@immutable
class PresubmitLuciSubscription extends SubscriptionHandler {
/// Creates an endpoint for listening to LUCI status updates.
const PresubmitLuciSubscription({
required super.cache,
required super.config,
required this.buildBucketClient,
required this.scheduler,
required this.luciBuildService,
required this.githubChecksService,
AuthenticationProvider? authProvider,
}) : super(subscriptionName: 'github-updater');
final BuildBucketClient buildBucketClient;
final LuciBuildService luciBuildService;
final GithubChecksService githubChecksService;
final Scheduler scheduler;
@override
Future<Body> post() async {
RepositorySlug slug;
final BuildPushMessage buildPushMessage = BuildPushMessage.fromPushMessage(message);
final Build build = buildPushMessage.build!;
final String builderName = build.tagsByName('builder').single;
log.fine('Available tags: ${build.tags.toString()}');
// Skip status update if we can not get the sha tag.
if (build.tagsByName('buildset').isEmpty) {
log.warning('Buildset tag not included, skipping Status Updates');
return Body.empty;
}
log.fine('Setting status: ${buildPushMessage.toJson()} for $builderName');
if (buildPushMessage.userData.containsKey('repo_owner') && buildPushMessage.userData.containsKey('repo_name')) {
// Message is coming from a github checks api enabled repo. We need to
// create the slug from the data in the message and send the check status
// update.
slug = RepositorySlug(
buildPushMessage.userData['repo_owner'] as String,
buildPushMessage.userData['repo_name'] as String,
);
bool rescheduled = false;
if (githubChecksService.taskFailed(buildPushMessage)) {
final int currentAttempt = githubChecksService.currentAttempt(buildPushMessage);
final int maxAttempt = await _getMaxAttempt(buildPushMessage, slug, builderName);
if (currentAttempt < maxAttempt) {
rescheduled = true;
log.fine('Rerun a failed task: $builderName');
await luciBuildService.rescheduleBuild(
builderName: builderName,
buildPushMessage: buildPushMessage,
rescheduleAttempt: currentAttempt + 1,
);
}
}
await githubChecksService.updateCheckStatus(
buildPushMessage,
luciBuildService,
slug,
rescheduled: rescheduled,
);
} else {
log.shout('This repo does not support checks API');
}
return Body.empty;
}
/// Gets target's allowed reschedule attempt.
///
/// Each target can define their own allowed max number of reschedule attemp, and it
/// is defined as a property `presubmit_max_attempts`.
///
/// If not property is defined, the target doesn't allow a reschedule after failures.
/// Typically the property will be used for targets that are likely flaky.
Future<int> _getMaxAttempt(
BuildPushMessage buildPushMessage,
RepositorySlug slug,
String builderName,
) async {
final Commit commit = Commit(
branch: buildPushMessage.userData['commit_branch'] as String,
repository: slug.fullName,
sha: buildPushMessage.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;
}
}