blob: 6ee97949ab2b0994726adc7d0886db496df9b36e [file] [log] [blame] [edit]
// 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.
// Note that we need this file because Github does not expose a field within the
// checks that states whether or not a particular check is required or not.
import 'package:cocoon_server/logging.dart';
import 'package:github/github.dart' as github;
import 'package:retry/retry.dart';
import '../exception/retryable_exception.dart';
import '../model/auto_submit_query_result.dart' as auto;
import '../service/config.dart';
import '../service/github_service.dart';
import 'validation.dart';
const String ciyamlValidation = 'ci.yaml validation';
/// Required check runs are check runs noted in the autosubmit.yaml configuration.
/// In order for a pull request to be merged any check runs specified in the
/// required check runs must pass before the bot will merge the pull request
/// regardless of review status.
class RequiredCheckRuns extends Validation {
const RequiredCheckRuns({required super.config, RetryOptions? retryOptions})
: retryOptions = retryOptions ?? Config.requiredChecksRetryOptions;
final RetryOptions retryOptions;
Future<bool> waitForRequiredChecks({
required github.RepositorySlug slug,
required String sha,
required Set<String> checkNames,
}) async {
final githubService = await config.createGithubService(slug);
final targetCheckRuns = <github.CheckRun>[];
for (var checkRun in checkNames) {
targetCheckRuns.addAll(
await githubService.getCheckRunsFiltered(
slug: slug,
ref: sha,
checkName: checkRun,
),
);
}
var checksCompleted = true;
try {
for (var checkRun in targetCheckRuns) {
await retryOptions.retry(() async {
await _verifyCheckRunCompleted(slug, githubService, checkRun);
}, retryIf: (Exception e) => e is RetryableException);
}
} catch (e, s) {
log.warn('Required check has not completed in time', e, s);
checksCompleted = false;
}
return checksCompleted;
}
@override
String get name => 'RequiredCheckRuns';
@override
Future<ValidationResult> validate(
auto.QueryResult result,
github.PullRequest messagePullRequest,
) async {
final pullRequest = result.repository!.pullRequest!;
final commit = pullRequest.commits!.nodes!.single.commit!;
final sha = commit.oid;
final slug = messagePullRequest.base!.repo!.slug();
final repositoryConfiguration = await config.getRepositoryConfiguration(
slug,
);
final requiredCheckRuns = repositoryConfiguration.requiredCheckRunsOnRevert;
final success = await waitForRequiredChecks(
slug: slug,
sha: sha!,
checkNames: requiredCheckRuns,
);
return ValidationResult(
success,
success ? Action.REMOVE_LABEL : Action.IGNORE_TEMPORARILY,
success
? 'All required check runs have completed.'
: 'Some of the required checks did not complete in time.',
);
}
}
/// Function signature that will be executed with retries.
typedef RetryHandler = void Function();
/// Simple function to wait on completed checkRuns with retries.
Future<void> _verifyCheckRunCompleted(
github.RepositorySlug slug,
GithubService githubService,
github.CheckRun targetCheckRun,
) async {
final checkRuns = await githubService.getCheckRunsFiltered(
slug: slug,
ref: targetCheckRun.headSha!,
checkName: targetCheckRun.name,
);
if (checkRuns.first.name != targetCheckRun.name ||
checkRuns.first.conclusion != github.CheckRunConclusion.success) {
throw RetryableException('${targetCheckRun.name} has not yet completed.');
}
}