Revert will wait for required checks to complete. (#2199)

diff --git a/auto_submit/lib/exception/retryable_checkrun_exception.dart b/auto_submit/lib/exception/retryable_checkrun_exception.dart
new file mode 100644
index 0000000..43a9948
--- /dev/null
+++ b/auto_submit/lib/exception/retryable_checkrun_exception.dart
@@ -0,0 +1,12 @@
+// 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.
+
+class RetryableCheckRunException implements Exception {
+  RetryableCheckRunException(this.cause);
+
+  final String cause;
+
+  @override
+  String toString() => cause;
+}
diff --git a/auto_submit/lib/service/config.dart b/auto_submit/lib/service/config.dart
index 40ae784..4806d3e 100644
--- a/auto_submit/lib/service/config.dart
+++ b/auto_submit/lib/service/config.dart
@@ -12,6 +12,7 @@
 import 'package:http/http.dart' as http;
 import 'package:neat_cache/cache_provider.dart';
 import 'package:neat_cache/neat_cache.dart';
+import 'package:retry/retry.dart';
 
 import '../foundation/providers.dart';
 import '../service/secrets.dart';
@@ -36,9 +37,17 @@
   static const String kAutosubmitLabel = 'autosubmit';
   static const String kRevertLabel = 'revert';
 
-  static const int backOfMultiplier = 200;
-  static const int backoffAttempts = 3;
-  static const int maxDelaySeconds = 1;
+  static const RetryOptions mergeRetryOptions = RetryOptions(
+    delayFactor: Duration(milliseconds: 200),
+    maxDelay: Duration(seconds: 1),
+    maxAttempts: 5,
+  );
+
+  static const RetryOptions requiredChecksRetryOptions = RetryOptions(
+    delayFactor: Duration(milliseconds: 500),
+    maxDelay: Duration(seconds: 5),
+    maxAttempts: 5,
+  );
 
   static const String flutter = 'flutter';
   static const String flutterGcpProjectId = 'flutter-dashboard';
diff --git a/auto_submit/lib/service/github_service.dart b/auto_submit/lib/service/github_service.dart
index 6cec27b..2affddc 100644
--- a/auto_submit/lib/service/github_service.dart
+++ b/auto_submit/lib/service/github_service.dart
@@ -24,6 +24,24 @@
     return await github.checks.checkRuns.listCheckRunsForRef(slug, ref: ref).toList();
   }
 
+  Future<List<CheckRun>> getCheckRunsFiltered({
+    required RepositorySlug slug,
+    required String ref,
+    String? checkName,
+    CheckRunStatus? status,
+    CheckRunFilter? filter,
+  }) async {
+    return await github.checks.checkRuns
+        .listCheckRunsForRef(
+          slug,
+          ref: ref,
+          checkName: checkName,
+          status: status,
+          filter: filter,
+        )
+        .toList();
+  }
+
   /// Fetches the specified commit.
   Future<RepositoryCommit> getCommit(RepositorySlug slug, String sha) async {
     return await github.repositories.getCommit(slug, sha);
diff --git a/auto_submit/lib/service/validation_service.dart b/auto_submit/lib/service/validation_service.dart
index d60aba7..6c412c0 100644
--- a/auto_submit/lib/service/validation_service.dart
+++ b/auto_submit/lib/service/validation_service.dart
@@ -38,12 +38,7 @@
 /// tested. The expectation is that the list of validation will grow overtime.
 class ValidationService {
   ValidationService(this.config, {RetryOptions? retryOptions})
-      : retryOptions = retryOptions ??
-            const RetryOptions(
-              delayFactor: Duration(milliseconds: Config.backOfMultiplier),
-              maxDelay: Duration(seconds: Config.maxDelaySeconds),
-              maxAttempts: Config.backoffAttempts,
-            ) {
+      : retryOptions = retryOptions ?? Config.mergeRetryOptions {
     /// Validates a PR marked with the reverts label.
     revertValidation = Revert(config: config);
     approverService = ApproverService(config);
@@ -303,6 +298,10 @@
 
         log.info(message);
       }
+    } else if (!revertValidationResult.result && revertValidationResult.action == Action.IGNORE_TEMPORARILY) {
+      // if required check runs have not completed process again.
+      log.info('Some of the required checks have not completed. Requeueing.');
+      return;
     } else {
       // since we do not temporarily ignore anything with a revert request we
       // know we will report the error and remove the label.
@@ -338,10 +337,9 @@
       // The createGitHubGraphQLClient can throw Exception on github permissions
       // errors.
       final graphql.GraphQLClient client = await config.createGitHubGraphQLClient(slug);
-
       graphql.QueryResult? result;
 
-      await _runProcessMergeWithRetries(
+      await retryOptions.retry(
         () async {
           result = await _processMergeInternal(
             client: client,
@@ -350,7 +348,7 @@
             messagePullRequest: messagePullRequest,
           );
         },
-        retryOptions,
+        retryIf: (Exception e) => e is RetryableMergeException,
       );
 
       if (result != null && result!.hasException) {
@@ -478,14 +476,6 @@
 /// Function signature that will be executed with retries.
 typedef RetryHandler = Function();
 
-/// Runs the internal processMerge with retries.
-Future<void> _runProcessMergeWithRetries(RetryHandler retryHandler, RetryOptions retryOptions) {
-  return retryOptions.retry(
-    retryHandler,
-    retryIf: (Exception e) => e is RetryableMergeException,
-  );
-}
-
 /// Internal wrapper for the logic of merging a pull request into github.
 Future<graphql.QueryResult> _processMergeInternal({
   required graphql.GraphQLClient client,
diff --git a/auto_submit/lib/validations/required_check_runs.dart b/auto_submit/lib/validations/required_check_runs.dart
new file mode 100644
index 0000000..445b050
--- /dev/null
+++ b/auto_submit/lib/validations/required_check_runs.dart
@@ -0,0 +1,19 @@
+// 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.
+
+const String ciyamlValidation = 'ci.yaml validation';
+
+/// flutter, engine, cocoon, plugins, packages, buildroot and tests
+const Map<String, List<String>> requiredCheckRunsMapping = {
+  'flutter': [ciyamlValidation],
+  'engine': [ciyamlValidation],
+  'cocoon': [ciyamlValidation],
+  'plugins': [ciyamlValidation],
+  'packages': [ciyamlValidation],
+  'buildroot': [ciyamlValidation],
+  'tests': [ciyamlValidation],
+};
diff --git a/auto_submit/lib/validations/revert.dart b/auto_submit/lib/validations/revert.dart
index c7e3932..0c2fee2 100644
--- a/auto_submit/lib/validations/revert.dart
+++ b/auto_submit/lib/validations/revert.dart
@@ -2,25 +2,33 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:auto_submit/exception/retryable_checkrun_exception.dart';
 import 'package:auto_submit/model/auto_submit_query_result.dart' as auto;
+import 'package:auto_submit/service/config.dart';
 import 'package:auto_submit/service/github_service.dart';
+import 'package:auto_submit/validations/required_check_runs.dart';
 import 'package:auto_submit/validations/validation.dart';
 import 'package:github/github.dart' as github;
+import 'package:retry/retry.dart';
 
 import '../service/log.dart';
 
 class Revert extends Validation {
   Revert({
     required super.config,
-  });
+    RetryOptions? retryOptions,
+  }) : retryOptions = retryOptions ?? Config.requiredChecksRetryOptions;
 
   static const Set<String> allowedReviewers = <String>{ORG_MEMBER, ORG_OWNER};
+  final RetryOptions retryOptions;
 
   @override
   Future<ValidationResult> validate(auto.QueryResult result, github.PullRequest messagePullRequest) async {
     final auto.PullRequest pullRequest = result.repository!.pullRequest!;
     final String authorAssociation = pullRequest.authorAssociation!;
     final String? author = pullRequest.author!.login;
+    final auto.Commit commit = pullRequest.commits!.nodes!.single.commit!;
+    String? sha = commit.oid;
 
     if (!isValidAuthor(author, authorAssociation)) {
       String message = 'The author $author does not have permissions to make this request.';
@@ -47,6 +55,22 @@
 
     github.RepositorySlug repositorySlug = _getSlugFromLink(revertLink);
     GithubService githubService = await config.createGithubService(repositorySlug);
+
+    bool requiredChecksCompleted = await waitForRequiredChecks(
+      githubService: githubService,
+      slug: repositorySlug,
+      sha: sha!,
+      checkNames: requiredCheckRunsMapping[repositorySlug.name]!,
+    );
+
+    if (!requiredChecksCompleted) {
+      return ValidationResult(
+        false,
+        Action.IGNORE_TEMPORARILY,
+        'Some of the required checks did not complete in time.',
+      );
+    }
+
     int pullRequestId = _getPullRequestNumberFromLink(revertLink);
     github.PullRequest requestToRevert = await githubService.getPullRequest(repositorySlug, pullRequestId);
 
@@ -100,4 +124,66 @@
     List<String> linkSplit = link.split('#');
     return int.parse(linkSplit.elementAt(1));
   }
+
+  /// Wait for the required checks to complete, and if repository has no checks
+  /// true is returned.
+  Future<bool> waitForRequiredChecks({
+    required GithubService githubService,
+    required github.RepositorySlug slug,
+    required String sha,
+    required List<String> checkNames,
+  }) async {
+    List<github.CheckRun> targetCheckRuns = [];
+    for (var element in checkNames) {
+      targetCheckRuns.addAll(
+        await githubService.getCheckRunsFiltered(
+          slug: slug,
+          ref: sha,
+          checkName: element,
+        ),
+      );
+    }
+
+    bool checksCompleted = true;
+
+    try {
+      for (github.CheckRun checkRun in targetCheckRuns) {
+        await retryOptions.retry(
+          () async {
+            await _verifyCheckRunCompleted(
+              slug,
+              githubService,
+              checkRun,
+            );
+          },
+          retryIf: (Exception e) => e is RetryableCheckRunException,
+        );
+      }
+    } catch (e) {
+      log.warning('Required check has not completed in time. ${e.toString()}');
+      checksCompleted = false;
+    }
+
+    return checksCompleted;
+  }
+}
+
+/// Function signature that will be executed with retries.
+typedef RetryHandler = Function();
+
+/// Simple function to wait on completed checkRuns with retries.
+Future<void> _verifyCheckRunCompleted(
+  github.RepositorySlug slug,
+  GithubService githubService,
+  github.CheckRun targetCheckRun,
+) async {
+  List<github.CheckRun> 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 RetryableCheckRunException('${targetCheckRun.name} has not yet completed.');
+  }
 }
diff --git a/auto_submit/test/service/validation_service_test.dart b/auto_submit/test/service/validation_service_test.dart
index 3e9acfb..50c7b30 100644
--- a/auto_submit/test/service/validation_service_test.dart
+++ b/auto_submit/test/service/validation_service_test.dart
@@ -42,7 +42,10 @@
     githubGraphQLClient = FakeGraphQLClient();
     githubService = FakeGithubService(client: MockGitHub());
     config = FakeConfig(githubService: githubService, githubGraphQLClient: githubGraphQLClient);
-    validationService = ValidationService(config);
+    validationService = ValidationService(
+      config,
+      retryOptions: const RetryOptions(delayFactor: Duration.zero, maxDelay: Duration.zero, maxAttempts: 1),
+    );
     slug = RepositorySlug('flutter', 'cocoon');
 
     jobsResource = MockJobsResource();
@@ -291,12 +294,58 @@
       assert(pubsub.messagesQueue.isEmpty);
     });
 
+    test('Revert returns on in process required checkRuns.', () async {
+      githubGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult();
+      final PullRequestHelper flutterRequest = PullRequestHelper(
+        prNumber: 0,
+        lastCommitHash: oid,
+        reviews: <PullRequestReviewHelper>[],
+      );
+
+      githubService.checkRunsData = inProgressCheckRunsMock;
+      githubService.createCommentData = createCommentMock;
+      githubService.throwOnCreateIssue = true;
+      githubService.useRealComment = true;
+      final FakePubSub pubsub = FakePubSub();
+      final PullRequest pullRequest = generatePullRequest(
+        prNumber: 0,
+        repoName: slug.name,
+        authorAssociation: 'OWNER',
+        labelName: 'revert',
+        body: 'Reverts flutter/flutter#1234',
+      );
+
+      final FakeRevert fakeRevert = FakeRevert(config: config);
+      fakeRevert.validationResult =
+          ValidationResult(false, Action.IGNORE_TEMPORARILY, 'Some of the required checks did not complete in time.');
+      validationService.revertValidation = fakeRevert;
+      final FakeApproverService fakeApproverService = FakeApproverService(config);
+      validationService.approverService = fakeApproverService;
+
+      unawaited(pubsub.publish('auto-submit-queue-sub', pullRequest));
+      final auto.QueryResult queryResult = createQueryResult(flutterRequest);
+
+      await validationService.processRevertRequest(
+        config: config,
+        result: queryResult,
+        messagePullRequest: pullRequest,
+        ackId: 'test',
+        pubsub: pubsub,
+      );
+
+      // if the merge is successful we do not remove the label and we do not add a comment to the issue.
+      expect(githubService.issueComment, isNull);
+      expect(githubService.labelRemoved, false);
+      // We acknowledge the issue.
+      assert(pubsub.messagesQueue.isNotEmpty);
+    });
+
     test('Exhaust retries on merge on retryable error.', () async {
       validationService = ValidationService(
         config,
         retryOptions: const RetryOptions(
-          delayFactor: Duration(milliseconds: 1),
-          maxDelay: Duration(milliseconds: 1),
+          delayFactor: Duration.zero,
+          maxDelay: Duration.zero,
           maxAttempts: 1,
         ),
       );
@@ -481,8 +530,7 @@
     test('Merge fails the first time but then succeeds after retry.', () async {
       validationService = ValidationService(
         config,
-        retryOptions:
-            const RetryOptions(delayFactor: Duration(milliseconds: 50), maxDelay: Duration(seconds: 1), maxAttempts: 3),
+        retryOptions: const RetryOptions(delayFactor: Duration.zero, maxDelay: Duration.zero, maxAttempts: 3),
       );
 
       // Create a result that will trigger a retry.
diff --git a/auto_submit/test/src/service/fake_github_service.dart b/auto_submit/test/src/service/fake_github_service.dart
index 0a15c0a..3148b27 100644
--- a/auto_submit/test/src/service/fake_github_service.dart
+++ b/auto_submit/test/src/service/fake_github_service.dart
@@ -107,6 +107,27 @@
   }
 
   @override
+  Future<List<CheckRun>> getCheckRunsFiltered({
+    required RepositorySlug slug,
+    required String ref,
+    String? checkName,
+    CheckRunStatus? status,
+    CheckRunFilter? filter,
+  }) async {
+    List<CheckRun> checkRuns = await getCheckRuns(slug, ref);
+    if (checkName != null) {
+      List<CheckRun> checkRunsFilteredByName = [];
+      for (CheckRun checkRun in checkRuns) {
+        if (checkRun.name == checkName) {
+          checkRunsFilteredByName.add(checkRun);
+        }
+      }
+      return checkRunsFilteredByName;
+    }
+    return checkRuns;
+  }
+
+  @override
   Future<RepositoryCommit> getCommit(RepositorySlug slug, String sha) async {
     final RepositoryCommit commit = RepositoryCommit.fromJson(jsonDecode(commitMock!) as Map<String, dynamic>);
     return commit;
diff --git a/auto_submit/test/validations/revert_test.dart b/auto_submit/test/validations/revert_test.dart
index 3d8ff93..c81766e 100644
--- a/auto_submit/test/validations/revert_test.dart
+++ b/auto_submit/test/validations/revert_test.dart
@@ -7,11 +7,12 @@
 import 'package:auto_submit/model/auto_submit_query_result.dart';
 import 'package:auto_submit/validations/validation.dart';
 import 'package:github/github.dart' as github;
+import 'package:retry/retry.dart';
+import 'package:test/test.dart';
 
 import 'revert_test_data.dart';
 
 import 'package:auto_submit/validations/revert.dart';
-import 'package:test/scaffolding.dart';
 
 import '../utilities/mocks.dart';
 import '../src/service/fake_config.dart';
@@ -20,16 +21,20 @@
 
 void main() {
   late FakeConfig config;
-  FakeGithubService githubService = FakeGithubService();
+  late FakeGithubService githubService;
   late FakeGraphQLClient githubGraphQLClient;
   MockGitHub gitHub = MockGitHub();
   late Revert revert;
 
   /// Setup objects needed across test groups.
   setUp(() {
+    githubService = FakeGithubService();
     githubGraphQLClient = FakeGraphQLClient();
     config = FakeConfig(githubService: githubService, githubGraphQLClient: githubGraphQLClient, githubClient: gitHub);
-    revert = Revert(config: config);
+    revert = Revert(
+      config: config,
+      retryOptions: const RetryOptions(delayFactor: Duration.zero, maxDelay: Duration.zero, maxAttempts: 1),
+    );
   });
 
   group('Author validation tests.', () {
@@ -142,6 +147,40 @@
       );
     });
 
+    test('Validation returns on checkRun that has not completed.', () async {
+      Map<String, dynamic> pullRequestJsonMap = jsonDecode(revertPullRequestJson) as Map<String, dynamic>;
+      github.PullRequest revertPullRequest = github.PullRequest.fromJson(pullRequestJsonMap);
+      final Map<String, dynamic> queryResultJsonDecode =
+          jsonDecode(queryResultRepositoryOwnerJson) as Map<String, dynamic>;
+      final QueryResult queryResult = QueryResult.fromJson(queryResultJsonDecode);
+
+      Map<String, dynamic> originalPullRequestJsonMap = jsonDecode(originalPullRequestJson) as Map<String, dynamic>;
+      github.PullRequest originalPullRequest = github.PullRequest.fromJson(originalPullRequestJsonMap);
+      githubService.pullRequestData = originalPullRequest;
+
+      // code gets the original file list then the current file list.
+      githubService.usePullRequestFilesList = true;
+      githubService.pullRequestFilesMockList.add(originalPullRequestFilesJson);
+      githubService.pullRequestFilesMockList.add(revertPullRequestFilesJson);
+
+      // Need to set the mock checkRuns for required CheckRun validation
+      githubService.checkRunsData = ciyamlCheckRunNotComplete;
+
+      revert = Revert(
+        config: config,
+        retryOptions: const RetryOptions(
+          delayFactor: Duration.zero,
+          maxDelay: Duration.zero,
+          maxAttempts: 1,
+        ),
+      );
+      ValidationResult validationResult = await revert.validate(queryResult, revertPullRequest);
+
+      expect(validationResult.result, isFalse);
+      expect(validationResult.action, Action.IGNORE_TEMPORARILY);
+      expect(validationResult.message, 'Some of the required checks did not complete in time.');
+    });
+
     test('Validation fails on pull request file lists not matching.', () async {
       Map<String, dynamic> pullRequestJsonMap = jsonDecode(revertPullRequestJson) as Map<String, dynamic>;
       github.PullRequest revertPullRequest = github.PullRequest.fromJson(pullRequestJsonMap);
@@ -158,6 +197,9 @@
       githubService.pullRequestFilesMockList.add(originalPullRequestFilesSubsetJson);
       githubService.pullRequestFilesMockList.add(revertPullRequestFilesJson);
 
+      // Need to set the mock checkRuns for required CheckRun validation
+      githubService.checkRunsData = ciyamlCheckRun;
+
       ValidationResult validationResult = await revert.validate(queryResult, revertPullRequest);
       assert(!validationResult.result);
       assert(validationResult.action == Action.REMOVE_LABEL);
@@ -183,6 +225,9 @@
       githubService.pullRequestFilesMockList.add(originalPullRequestFilesJson);
       githubService.pullRequestFilesMockList.add(revertPullRequestFilesJson);
 
+      // Need to set the mock checkRuns for required CheckRun validation
+      githubService.checkRunsData = ciyamlCheckRun;
+
       ValidationResult validationResult = await revert.validate(queryResult, revertPullRequest);
       assert(validationResult.result);
       assert(validationResult.message == 'Revert request has been verified and will be queued for merge.');
diff --git a/auto_submit/test/validations/revert_test_data.dart b/auto_submit/test/validations/revert_test_data.dart
index 4a681bd..a5e2bb3 100644
--- a/auto_submit/test/validations/revert_test_data.dart
+++ b/auto_submit/test/validations/revert_test_data.dart
@@ -510,3 +510,42 @@
     }
   ]
 """;
+
+const String ciyamlCheckRun = '''
+{
+  "total_count": 1,
+  "check_runs": [
+    {
+      "id": 8752872923,
+      "name": "ci.yaml validation",
+      "head_sha": "60612a38d705d00a234e0aabba08247fc0dda1ac",
+      "status": "completed",
+      "conclusion": "success",
+      "started_at": "2022-10-06T20:50:57Z",
+      "completed_at": "2022-10-06T20:51:40Z",
+      "check_suite": {
+        "id": 8654966141
+      }
+    }
+  ]
+}
+''';
+
+const String ciyamlCheckRunNotComplete = '''
+{
+  "total_count": 1,
+  "check_runs": [
+    {
+      "id": 8752872923,
+      "name": "ci.yaml validation",
+      "head_sha": "60612a38d705d00a234e0aabba08247fc0dda1ac",
+      "status": "in_progress",
+      "started_at": "2022-10-06T20:50:57Z",
+      "completed_at": "2022-10-06T20:51:40Z",
+      "check_suite": {
+        "id": 8654966141
+      }
+    }
+  ]
+}
+''';