blob: 61b21d94e1404e1f16d9605301dcfe5380817d58 [file] [log] [blame]
// Copyright 2022 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:auto_submit/model/auto_submit_query_result.dart';
import 'package:auto_submit/service/github_service.dart';
import 'package:auto_submit/validations/validation.dart';
import 'package:github/github.dart' as github;
import '../service/config.dart';
import '../service/log.dart';
/// Validates all the CI build/tests ran and were successful.
class CiSuccessful extends Validation {
/// The status checks that are not related to changes in this PR.
static const Set<String> notInAuthorsControl = <String>{
'luci-flutter', // flutter repo
'luci-engine', // engine repo
'submit-queue', // plugins repo
};
CiSuccessful({
required super.config,
});
@override
/// Implements the CI build/tests validations.
Future<ValidationResult> validate(QueryResult result, github.PullRequest messagePullRequest) async {
bool allSuccess = true;
github.RepositorySlug slug = messagePullRequest.base!.repo!.slug();
final PullRequest pullRequest = result.repository!.pullRequest!;
Set<FailureDetail> failures = <FailureDetail>{};
List<ContextNode> statuses = <ContextNode>[];
Commit commit = pullRequest.commits!.nodes!.single.commit!;
// Recently most of the repositories have migrated away of using the status
// APIs and for those repos commit.status is null.
if (commit.status != null && commit.status!.contexts!.isNotEmpty) {
statuses.addAll(commit.status!.contexts!);
}
// Check tree status of repos. If the tree status is not ready,
// we want to hold and wait for the status, same as waiting
// for checks to finish.
if (!treeStatusCheck(slug, statuses)) {
log.warning('Statuses were not ready for ${slug.fullName}, sha: $commit.');
return ValidationResult(false, Action.IGNORE_TEMPORARILY, 'Hold to wait for the tree status ready.');
}
// List of labels associated with the pull request.
final List<String> labelNames = (messagePullRequest.labels as List<github.IssueLabel>)
.map<String>((github.IssueLabel labelMap) => labelMap.name)
.toList();
/// Validate if all statuses have been successful.
allSuccess = validateStatuses(slug, labelNames, statuses, failures, allSuccess);
final GithubService gitHubService = await config.createGithubService(slug);
final String? sha = commit.oid;
List<github.CheckRun> checkRuns = <github.CheckRun>[];
if (messagePullRequest.head != null && sha != null) {
checkRuns.addAll(await gitHubService.getCheckRuns(slug, sha));
}
/// Validate if all checkRuns have succeeded.
allSuccess = validateCheckRuns(slug, checkRuns, failures, allSuccess);
if (!allSuccess && failures.isEmpty) {
return ValidationResult(allSuccess, Action.IGNORE_TEMPORARILY, '');
}
final StringBuffer buffer = StringBuffer();
if (failures.isNotEmpty) {
for (FailureDetail detail in failures) {
buffer.writeln('- The status or check suite ${detail.markdownLink} has failed. Please fix the '
'issues identified (or deflake) before re-applying this label.');
}
}
Action action = labelNames.contains(config.overrideTreeStatusLabel) ? Action.IGNORE_FAILURE : Action.REMOVE_LABEL;
return ValidationResult(allSuccess, action, buffer.toString());
}
/// Check the tree status.
///
/// If a repo has a tree status, we should wait for it to show up instead of posting
/// a failure to GitHub pull request.
/// If a repo doesn't have a tree status, simply return `true`.
bool treeStatusCheck(github.RepositorySlug slug, List<ContextNode> statuses) {
bool treeStatusValid = false;
if (!Config.reposWithTreeStatus.contains(slug)) {
return true;
}
if (statuses.isEmpty) {
return false;
}
final String treeStatusName = 'luci-${slug.name}';
log.info('Validating tree status: ${slug.name}, statuses: $statuses');
/// Scan list of statuses to see if the tree status exists (this list is expected to be <5 items)
for (ContextNode status in statuses) {
if (status.context == treeStatusName) {
// Does only one tree status need to be set for the condition?
treeStatusValid = true;
}
}
return treeStatusValid;
}
/// Validate the ci build test run statuses to see which have succeeded and
/// which have failed.
///
/// Failures will be added the set of overall failures.
/// Returns allSuccess unmodified if there were no failures, false otherwise.
bool validateStatuses(
github.RepositorySlug slug,
List<String> labelNames,
List<ContextNode> statuses,
Set<FailureDetail> failures,
bool allSuccess,
) {
final String overrideTreeStatusLabel = config.overrideTreeStatusLabel;
log.info('Validating name: ${slug.name}, statuses: $statuses');
for (ContextNode status in statuses) {
// How can name be null but presumed to not be null below when added to failure?
final String? name = status.context;
if (status.state != STATUS_SUCCESS) {
if (notInAuthorsControl.contains(name) && labelNames.contains(overrideTreeStatusLabel)) {
continue;
}
allSuccess = false;
if (status.state == STATUS_FAILURE && !notInAuthorsControl.contains(name)) {
failures.add(FailureDetail(name!, status.targetUrl!));
}
}
}
return allSuccess;
}
/// Validate the checkRuns to see if all have completed successfully or not.
///
/// Failures will be added the set of overall failures.
/// Returns allSuccess unmodified if there were no failures, false otherwise.
bool validateCheckRuns(
github.RepositorySlug slug,
List<github.CheckRun> checkRuns,
Set<FailureDetail> failures,
bool allSuccess,
) {
log.info('Validating name: ${slug.name}, checkRuns: $checkRuns');
for (github.CheckRun checkRun in checkRuns) {
final String? name = checkRun.name;
if (checkRun.conclusion == github.CheckRunConclusion.skipped ||
checkRun.conclusion == github.CheckRunConclusion.success ||
(checkRun.status == github.CheckRunStatus.completed &&
checkRun.conclusion == github.CheckRunConclusion.neutral)) {
// checkrun has passed.
continue;
} else if (checkRun.status == github.CheckRunStatus.completed) {
// checkrun has failed.
failures.add(FailureDetail(name!, checkRun.detailsUrl as String));
}
allSuccess = false;
}
return allSuccess;
}
}