Schedule luci builds using CheckSuite and CheckRun events. (#828)

* Schedule luci builds using CheckSuite and CheckRun events.

This is required to add functionality to re-trigger tasks on failure.

Bug:
  https://github.com/flutter/flutter/issues/56422

* Remove unused variable.

* Address code review comments.

* Adds docs explaining the retries functionality.

* Reformat comment as todo.
diff --git a/app_dart/lib/src/request_handlers/github_webhook.dart b/app_dart/lib/src/request_handlers/github_webhook.dart
index 89480d7..76f8367 100644
--- a/app_dart/lib/src/request_handlers/github_webhook.dart
+++ b/app_dart/lib/src/request_handlers/github_webhook.dart
@@ -110,7 +110,7 @@
           await _checkForGoldenTriage(pullRequestEvent);
         } else {
           await luciBuildService.cancelBuilds(
-            pr.head.repo.name,
+            pullRequestEvent.repository.slug(),
             pr.number,
             pr.head.sha,
             'Pull request closed',
@@ -127,19 +127,19 @@
       case 'reopened':
         // These cases should trigger LUCI jobs.
         await _checkForLabelsAndTests(pullRequestEvent);
-        await _scheduleIfMergeable(pr);
+        await _scheduleIfMergeable(pullRequestEvent);
         break;
       case 'labeled':
         // This should only trigger a LUCI job for flutter/flutter right now,
         // since it is in the needsCQLabelList.
         if (kNeedsCQLabelList.contains(pr.base.repo.fullName.toLowerCase())) {
-          await _scheduleIfMergeable(pr);
+          await _scheduleIfMergeable(pullRequestEvent);
         }
         break;
       case 'synchronize':
         // This indicates the PR has new commits. We need to cancel old jobs
         // and schedule new ones.
-        await _scheduleIfMergeable(pr);
+        await _scheduleIfMergeable(pullRequestEvent);
         break;
       case 'unlabeled':
         // Cancel the jobs if someone removed the label on a repo that needs
@@ -149,7 +149,7 @@
         }
         if (!await _checkForCqLabel(pr.labels)) {
           await luciBuildService.cancelBuilds(
-            pr.head.repo.name,
+            pullRequestEvent.repository.slug(),
             pr.number,
             pr.head.sha,
             'Tryjobs canceled (label removed)',
@@ -173,11 +173,11 @@
   /// without a details link. Once the test starts running then the state is set
   /// to "pending" with a details link pointing to the build in LUCI infrastructure.
   Future<void> _scheduleIfMergeable(
-    PullRequest pr,
+    PullRequestEvent pullRequestEvent,
   ) async {
     // The mergeable flag may be null. False indicates there's a merge conflict,
     // null indicates unknown. Err on the side of allowing the job to run.
-
+    final PullRequest pr = pullRequestEvent.pullRequest;
     // For flutter/flutter tests need to be optimized before enforcing CQ.
     if (kNeedsCQLabelList.contains(pr.base.repo.fullName.toLowerCase())) {
       if (!await _checkForCqLabel(pr.labels)) {
@@ -187,15 +187,15 @@
 
     // Always cancel running builds so we don't ever schedule duplicates.
     await luciBuildService.cancelBuilds(
-      pr.head.repo.name,
+      pullRequestEvent.repository.slug(),
       pr.number,
       pr.head.sha,
       'Newer commit available',
     );
     await luciBuildService.scheduleBuilds(
+      slug: pullRequestEvent.repository.slug(),
       prNumber: pr.number,
       commitSha: pr.head.sha,
-      repositoryName: pr.head.repo.name,
     );
     await githubStatusService.setBuildsPendingStatus(
         pr.number, pr.head.sha, pr.head.repo.slug());
diff --git a/app_dart/lib/src/request_handlers/luci_status.dart b/app_dart/lib/src/request_handlers/luci_status.dart
index ad1502d..a1444ee 100644
--- a/app_dart/lib/src/request_handlers/luci_status.dart
+++ b/app_dart/lib/src/request_handlers/luci_status.dart
@@ -72,11 +72,9 @@
     final PushMessageEnvelope envelope = PushMessageEnvelope.fromJson(
       json.decode(requestString) as Map<String, dynamic>,
     );
-    final BuildPushMessage buildMessage = BuildPushMessage.fromJson(
+    final BuildPushMessage buildPushMessage = BuildPushMessage.fromJson(
         json.decode(envelope.message.data) as Map<String, dynamic>);
-    final Build build = buildMessage.build;
-    final Map<String, dynamic> userData =
-        jsonDecode(buildMessage.userData) as Map<String, dynamic>;
+    final Build build = buildPushMessage.build;
     final String builderName = build.tagsByName('builder').single;
     final RepositorySlug slug = await config.repoNameForBuilder(builderName);
 
@@ -91,15 +89,13 @@
         .tagsByName('buildset')
         .firstWhere((String tag) => tag.startsWith(shaPrefix))
         .substring(shaPrefix.length);
-    log.debug('Setting status: ${buildMessage.toJson()} for $builderName');
-    switch (buildMessage.build.status) {
+    log.debug('Setting status: ${buildPushMessage.toJson()} for $builderName');
+    switch (buildPushMessage.build.status) {
       case Status.completed:
-        await _rescheduleOrMarkCompleted(
+        await _markCompleted(
           sha: sha,
           builderName: builderName,
           build: build,
-          retries: userData['retries'] as int,
-          luciBuildService: luciBuildService,
           githubStatusService: githubStatusService,
           slug: slug,
         );
@@ -120,50 +116,18 @@
     return Body.empty;
   }
 
-  /// Reschedules jobs that failed for infra reasons up to
-  /// [CocoonConfig.luciTryInfraFailureRetries] times, and updates statuses on
-  /// GitHub for all other cases.
-  Future<void> _rescheduleOrMarkCompleted({
+  /// Updates the github status using the push_message [build] sent by LUCI
+  /// as a pub/sub message.
+  Future<void> _markCompleted({
     @required String sha,
     @required String builderName,
     @required Build build,
-    @required int retries,
-    @required LuciBuildService luciBuildService,
     @required GithubStatusService githubStatusService,
     @required RepositorySlug slug,
   }) async {
     assert(sha != null);
     assert(builderName != null);
     assert(build != null);
-    if (build.result == Result.failure) {
-      switch (build.failureReason) {
-        case FailureReason.buildbucketFailure:
-        case FailureReason.infraFailure:
-          log.info('Retrying: $builderName for $sha');
-          final bool rescheduled = await luciBuildService.rescheduleBuild(
-            commitSha: sha,
-            builderName: builderName,
-            build: build,
-            retries: retries,
-          );
-          if (rescheduled) {
-            final bool success = await githubStatusService.setPendingStatus(
-              ref: sha,
-              builderName: builderName,
-              buildUrl: '',
-              slug: slug,
-            );
-            if (!success) {
-              log.warning('Failed to set status for $builderName');
-            }
-            return;
-          }
-          break;
-        case FailureReason.invalidBuildDefinition:
-        case FailureReason.buildFailure:
-          break;
-      }
-    }
     await githubStatusService.setCompletedStatus(
       ref: sha,
       builderName: builderName,
diff --git a/app_dart/lib/src/service/github_status_service.dart b/app_dart/lib/src/service/github_status_service.dart
index a194baa..c468a55 100644
--- a/app_dart/lib/src/service/github_status_service.dart
+++ b/app_dart/lib/src/service/github_status_service.dart
@@ -27,9 +27,8 @@
   ) async {
     final GitHub gitHubClient =
         await config.createGitHubClient(slug.owner, slug.name);
-    final String repositoryName = slug.name;
     final Map<String, bb.Build> builds = await luciBuildService
-        .buildsForRepositoryAndPr(repositoryName, prNumber, commitSha);
+        .buildsForRepositoryAndPr(slug, prNumber, commitSha);
     final List<String> builderNames = config.luciTryBuilders
         .map((Map<String, dynamic> entry) => entry['name'] as String)
         .toList();
diff --git a/app_dart/lib/src/service/luci_build_service.dart b/app_dart/lib/src/service/luci_build_service.dart
index cbbb82e..a7e466c 100644
--- a/app_dart/lib/src/service/luci_build_service.dart
+++ b/app_dart/lib/src/service/luci_build_service.dart
@@ -4,7 +4,11 @@
 
 import 'dart:convert';
 
+import 'package:appengine/appengine.dart';
+import 'package:cocoon_service/src/foundation/github_checks_util.dart';
+import 'package:cocoon_service/src/model/github/checks.dart';
 import 'package:cocoon_service/src/request_handling/exceptions.dart';
+import 'package:github/github.dart' as github;
 import 'package:meta/meta.dart';
 
 import '../../cocoon_service.dart';
@@ -17,22 +21,34 @@
 /// and cancel builds for github repos. It uses [config.luciTryBuilders] to
 /// get the list of available builders.
 class LuciBuildService {
-  LuciBuildService(this.config, this.buildBucketClient, this.serviceAccount);
+  LuciBuildService(this.config, this.buildBucketClient, this.serviceAccount,
+      {GithubChecksUtil githubChecksUtil})
+      : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil();
 
   BuildBucketClient buildBucketClient;
   Config config;
   ServiceAccountInfo serviceAccount;
+  Logging log;
+  GithubChecksUtil githubChecksUtil;
+
   static const Set<Status> failStatusSet = <Status>{
     Status.canceled,
     Status.failure,
     Status.infraFailure
   };
 
-  /// Returns a map of the BuildBucket builds for a given [repositoryName]
+  /// Sets the appengine [log] used by this class to log debug and error
+  /// messages. This method has to be called before any other method in this
+  /// class.
+  void setLogger(Logging log) {
+    this.log = log;
+  }
+
+  /// Returns a map of the BuildBucket builds for a given Github [slug]
   /// [prNumber] and [commitSha] using the [builderName] as key and [Build]
   /// as value.
   Future<Map<String, Build>> buildsForRepositoryAndPr(
-    String repositoryName,
+    github.RepositorySlug slug,
     int prNumber,
     String commitSha,
   ) async {
@@ -49,7 +65,7 @@
             tags: <String, List<String>>{
               'buildset': <String>['pr/git/$prNumber'],
               'github_link': <String>[
-                'https://github.com/flutter/$repositoryName/pull/$prNumber'
+                'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber'
               ],
               'user_agent': const <String>['flutter-cocoon'],
             },
@@ -81,44 +97,48 @@
   }
 
   /// Schedules BuildBucket builds for a given [prNumber], [commitSha]
-  /// and repositoryName. It returns [true] if it was able to schedule
-  /// build or [false] otherwise.
+  /// and Github [slug].
   Future<bool> scheduleBuilds({
     @required int prNumber,
     @required String commitSha,
-    @required String repositoryName,
+    @required github.RepositorySlug slug,
+    CheckSuiteEvent checkSuiteEvent,
   }) async {
     assert(prNumber != null);
     assert(commitSha != null);
-    assert(repositoryName != null);
-    if (!config.githubPresubmitSupportedRepo(repositoryName)) {
+    assert(slug != null);
+    final github.GitHub githubClient =
+        await config.createGitHubClient(slug.owner, slug.name);
+    if (!config.githubPresubmitSupportedRepo(slug.name)) {
       throw BadRequestException(
-          'Repository $repositoryName is not supported by this service.');
+          'Repository ${slug.name} is not supported by this service.');
     }
 
     final Map<String, Build> builds = await buildsForRepositoryAndPr(
-      repositoryName,
+      slug,
       prNumber,
       commitSha,
     );
-
     if (builds != null &&
         builds.values.any((Build build) {
           return build.status == Status.scheduled ||
               build.status == Status.started;
         })) {
+      log.error(
+          'Either builds are empty or they are already scheduled or started. '
+          'PR: $prNumber, Commit: $commitSha, Owner: ${slug.owner} '
+          'Repo: ${slug.name}');
       return false;
     }
 
     final List<Map<String, dynamic>> builders = config.luciTryBuilders;
     final List<String> builderNames = builders
-        .where(
-            (Map<String, dynamic> builder) => builder['repo'] == repositoryName)
+        .where((Map<String, dynamic> builder) => builder['repo'] == slug.name)
         .map<String>(
             (Map<String, dynamic> builder) => builder['name'] as String)
         .toList();
     if (builderNames.isEmpty) {
-      throw InternalServerError('$repositoryName does not have any builders');
+      throw InternalServerError('${slug.name} does not have any builders');
     }
 
     final List<Request> requests = <Request>[];
@@ -128,6 +148,20 @@
         bucket: 'try',
         builder: builder,
       );
+      final Map<String, dynamic> userData = <String, dynamic>{'retries': 0};
+      if (checkSuiteEvent != null) {
+        final github.CheckRun checkRun =
+            await githubClient.checks.checkRuns.createCheckRun(
+          checkSuiteEvent.repository.slug(),
+          name: builder,
+          headSha: commitSha,
+        );
+        userData['check_suite_id'] = checkSuiteEvent.checkSuite.id;
+        userData['check_run_id'] = checkRun.id;
+        userData['repo_owner'] = slug.owner;
+        userData['repo_name'] = slug.name;
+        userData['user_agent'] = 'flutter-cocoon';
+      }
       requests.add(
         Request(
           scheduleBuild: ScheduleBuildRequest(
@@ -136,11 +170,11 @@
               'buildset': <String>['pr/git/$prNumber', 'sha/git/$commitSha'],
               'user_agent': const <String>['flutter-cocoon'],
               'github_link': <String>[
-                'https://github.com/flutter/$repositoryName/pull/$prNumber'
+                'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber'
               ],
             },
             properties: <String, String>{
-              'git_url': 'https://github.com/flutter/$repositoryName',
+              'git_url': 'https://github.com/${slug.owner}/${slug.name}',
               'git_ref': 'refs/pull/$prNumber/head',
             },
             notify: NotificationConfig(
@@ -159,14 +193,14 @@
 
   /// Cancels all the current builds for a given [repositoryName], [prNumber]
   /// and [commitSha] adding a message for the cancelation reason.
-  Future<void> cancelBuilds(String repositoryName, int prNumber,
+  Future<void> cancelBuilds(github.RepositorySlug slug, int prNumber,
       String commitSha, String reason) async {
-    if (!config.githubPresubmitSupportedRepo(repositoryName)) {
+    if (!config.githubPresubmitSupportedRepo(slug.name)) {
       throw BadRequestException(
-          'This service does not support repository $repositoryName.');
+          'This service does not support repository ${slug.name}');
     }
     final Map<String, Build> builds = await buildsForRepositoryAndPr(
-      repositoryName,
+      slug,
       prNumber,
       commitSha,
     );
@@ -192,14 +226,20 @@
   /// Gets a list of failed builds for a given [repositoryName], [prNumber] and
   /// [commitSha].
   Future<List<Build>> failedBuilds(
-    String repositoryName,
+    github.RepositorySlug slug,
     int prNumber,
     String commitSha,
   ) async {
     final Map<String, Build> builds =
-        await buildsForRepositoryAndPr(repositoryName, prNumber, commitSha);
+        await buildsForRepositoryAndPr(slug, prNumber, commitSha);
+    final List<String> builderNames = config.luciTryBuilders
+        .map((Map<String, dynamic> entry) => entry['name'] as String)
+        .toList();
+    // Return only builds that exist in the configuration file.
     return builds.values
-        .where((Build build) => failStatusSet.contains(build.status))
+        .where((Build build) =>
+            failStatusSet.contains(build.status) &&
+            builderNames.contains(build.builderId.builder))
         .toList();
   }
 
@@ -214,7 +254,7 @@
   Future<bool> rescheduleBuild({
     @required String commitSha,
     @required String builderName,
-    @required push_message.Build build,
+    @required push_message.BuildPushMessage buildPushMessage,
     @required int retries,
   }) async {
     if (retries >= config.luciTryInfraFailureRetries) {
@@ -224,27 +264,135 @@
     // Ensure we are using V2 bucket name istead of V1.
     // V1 bucket name  is "luci.flutter.prod" while the api
     // is expecting just the last part after "."(prod).
-    final String bucketName = build.bucket.split('.').last;
+    final String bucketName = buildPushMessage.build.bucket.split('.').last;
+    final Map<String, dynamic> userData =
+        jsonDecode(buildPushMessage.userData) as Map<String, dynamic>;
+    userData['retries'] += 1;
     await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
       builderId: BuilderId(
-        project: build.project,
+        project: buildPushMessage.build.project,
         bucket: bucketName,
         builder: builderName,
       ),
       tags: <String, List<String>>{
-        'buildset': build.tagsByName('buildset'),
-        'user_agent': build.tagsByName('user_agent'),
-        'github_link': build.tagsByName('github_link'),
+        'buildset': buildPushMessage.build.tagsByName('buildset'),
+        'user_agent': buildPushMessage.build.tagsByName('user_agent'),
+        'github_link': buildPushMessage.build.tagsByName('github_link'),
       },
-      properties: (build.buildParameters['properties'] as Map<String, dynamic>)
+      properties: (buildPushMessage.build.buildParameters['properties']
+              as Map<String, dynamic>)
           .cast<String, String>(),
       notify: NotificationConfig(
         pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
-        userData: json.encode(<String, dynamic>{
-          'retries': retries + 1,
-        }),
+        userData: json.encode(userData),
       ),
     ));
     return true;
   }
+
+  /// Sends a [BuildBucket.scheduleBuild] request using [CheckRunEvent]. It
+  /// returns [true] if it is able to send the scheduleBuildRequest or [false]
+  /// if not.
+  Future<bool> rescheduleUsingCheckRunEvent(CheckRunEvent checkRunEvent) async {
+    final github.RepositorySlug slug = checkRunEvent.repository.slug();
+    final Map<String, dynamic> userData = <String, dynamic>{};
+    final github.PullRequest pr = checkRunEvent.checkRun.pullRequests[0];
+    final github.GitHub gitHubClient =
+        await config.createGitHubClient(slug.owner, slug.name);
+    final github.CheckRun githubCheckRun =
+        await githubChecksUtil.createCheckRun(
+      gitHubClient,
+      slug,
+      checkRunEvent.checkRun.name,
+      pr.head.sha,
+    );
+    userData['check_suite_id'] = checkRunEvent.checkRun.checkSuite.id;
+    userData['check_run_id'] = githubCheckRun.id;
+    userData['repo_owner'] = slug.owner;
+    userData['repo_name'] = slug.name;
+    userData['user_agent'] = 'flutter-cocoon';
+    userData['retries'] = 1;
+    await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
+      builderId: BuilderId(
+        project: 'flutter',
+        bucket: 'try',
+        builder: checkRunEvent.checkRun.name,
+      ),
+      tags: <String, List<String>>{
+        'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'],
+        'user_agent': const <String>['flutter-cocoon'],
+        'github_link': <String>[
+          'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}'
+        ],
+      },
+      properties: <String, String>{
+        'git_url': 'https://github.com/${slug.owner}/${slug.name}',
+        'git_ref': 'refs/pull/${pr.number}/head',
+      },
+      notify: NotificationConfig(
+        pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
+        userData: json.encode(userData),
+      ),
+    ));
+    return true;
+  }
+
+  /// Sends a [BuildBucket.scheduleBuild] request using [CheckSuiteEvent],
+  /// [gitgub.CheckRun] and [RepositorySlug]. It returns [true] if it is able to
+  /// send the scheduleBuildRequest or [false] if not.
+  Future<bool> rescheduleUsingCheckSuiteEvent(
+      CheckSuiteEvent checkSuiteEvent, github.CheckRun checkRun) async {
+    final github.RepositorySlug slug = checkSuiteEvent.repository.slug();
+    final Map<String, dynamic> userData = <String, dynamic>{};
+    final github.PullRequest pr = checkSuiteEvent.checkSuite.pullRequests[0];
+    final github.GitHub gitHubClient =
+        await config.createGitHubClient(slug.owner, slug.name);
+    final github.CheckRun githubCheckRun =
+        await githubChecksUtil.createCheckRun(
+      gitHubClient,
+      slug,
+      checkRun.name,
+      pr.head.sha,
+    );
+    userData['check_suite_id'] = checkSuiteEvent.checkSuite.id;
+    userData['check_run_id'] = githubCheckRun.id;
+    userData['repo_owner'] = slug.owner;
+    userData['repo_name'] = slug.name;
+    userData['user_agent'] = 'flutter-cocoon';
+    // Retries were used to auto re-run builds when they failed with infra
+    // failure. Now with github checks api support automated retries won't be
+    // needed anymore and will be removed:
+    // TODO(godofredoc): remove retries https://github.com/flutter/flutter/issues/60942.
+    userData['retries'] = 1;
+    await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
+      builderId: BuilderId(
+        project: 'flutter',
+        bucket: 'try',
+        builder: checkRun.name,
+      ),
+      tags: <String, List<String>>{
+        'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'],
+        'user_agent': const <String>['flutter-cocoon'],
+        'github_link': <String>[
+          'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}'
+        ],
+      },
+      properties: <String, String>{
+        'git_url': 'https://github.com/${slug.owner}/${slug.name}',
+        'git_ref': 'refs/pull/${pr.number}/head',
+      },
+      notify: NotificationConfig(
+        pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
+        userData: json.encode(userData),
+      ),
+    ));
+    return true;
+  }
+
+  /// Gets a [buildbucket.Build] using its [id] and passing the additional
+  /// fields to be populated in the response.
+  Future<Build> getBuildById(int id, {String fields}) async {
+    final GetBuildRequest request = GetBuildRequest(id: id, fields: fields);
+    return buildBucketClient.getBuild(request);
+  }
 }
diff --git a/app_dart/test/request_handlers/github_webhook_test.dart b/app_dart/test/request_handlers/github_webhook_test.dart
index dc7612f..2fe2648 100644
--- a/app_dart/test/request_handlers/github_webhook_test.dart
+++ b/app_dart/test/request_handlers/github_webhook_test.dart
@@ -1050,9 +1050,9 @@
       "sha": "the_head_sha",
       "repo": {
         "name": "cocoon",
-        "full_name": "digiter/cocoon",
+        "full_name": "flutter/cocoon",
         "owner": {
-          "login": "abc"
+          "login": "flutter"
         }
       }
     }
@@ -1075,6 +1075,11 @@
         final String hmac = getHmac(body, key);
         request.headers.set('X-Hub-Signature', 'sha1=$hmac');
 
+        config.luciTryBuildersValue = (json.decode(
+                    '[{"name": "Cocoon", "repo": "cocoon"},{"name": "Linux", "repo": "flutter"}, {"name": "Mac", "repo": "flutter"}]')
+                as List<dynamic>)
+            .cast<Map<String, dynamic>>();
+
         await tester.post(webhook);
 
         if (never) {
diff --git a/app_dart/test/request_handlers/luci_status_test.dart b/app_dart/test/request_handlers/luci_status_test.dart
index 86f6a8f..d82400c 100644
--- a/app_dart/test/request_handlers/luci_status_test.dart
+++ b/app_dart/test/request_handlers/luci_status_test.dart
@@ -8,7 +8,6 @@
 
 import 'package:cocoon_service/cocoon_service.dart';
 import 'package:cocoon_service/src/model/appengine/service_account_info.dart';
-import 'package:cocoon_service/src/model/luci/buildbucket.dart';
 import 'package:cocoon_service/src/request_handling/exceptions.dart';
 import 'package:http/testing.dart' as http_test;
 import 'package:http/http.dart' as http;
@@ -255,70 +254,6 @@
     );
   });
 
-  test('Reschedules an infra failure', () async {
-    request.bodyBytes = utf8.encode(pushMessageJson('COMPLETED',
-        builderName: 'Linux',
-        result: 'FAILURE',
-        failureReason: 'INFRA_FAILURE')) as Uint8List;
-    request.headers.add(HttpHeaders.authorizationHeader, authHeader);
-    mockGitHubClient = MockGitHub();
-    config.githubClient = mockGitHubClient;
-    final List<RepositoryStatus> repositoryStatuses = <RepositoryStatus>[
-      RepositoryStatus()
-        ..context = 'Linux'
-        ..state = 'failure',
-    ];
-    when(mockGitHubClient.repositories).thenReturn(mockRepositoriesService);
-    when(mockRepositoriesService.listStatuses(any, ref)).thenAnswer((_) {
-      return Stream<RepositoryStatus>.fromIterable(repositoryStatuses);
-    });
-
-    await tester.post(handler);
-    expect(
-      jsonEncode(
-        verify(buildBucketClient.scheduleBuild(captureAny))
-            .captured
-            .single
-            .toJson(),
-      ),
-      jsonEncode(
-        ScheduleBuildRequest(
-          builderId: const BuilderId(
-            project: 'flutter',
-            bucket: 'prod',
-            builder: 'Linux',
-          ),
-          tags: const <String, List<String>>{
-            'buildset': <String>['pr/git/37647', 'sha/git/$ref'],
-            'user_agent': <String>['flutter-cocoon'],
-            'github_link': <String>[
-              'https://github.com/flutter/flutter/pull/37647'
-            ],
-          },
-          properties: const <String, String>{
-            'git_ref': 'refs/pull/37647/head',
-            'git_url': 'https://github.com/flutter/flutter',
-          },
-          notify: NotificationConfig(
-            pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
-            userData: json.encode(<String, dynamic>{
-              'retries': 1,
-            }),
-          ),
-        ).toJson(),
-      ),
-    );
-    expect(
-      verify(mockRepositoriesService.createStatus(
-        RepositorySlug('flutter', 'flutter'),
-        ref,
-        captureAny,
-      )).captured.single.toJson(),
-      jsonDecode(
-          '{"state":"pending","target_url":"","description":"Flutter LUCI Build: Linux","context":"Linux"}'),
-    );
-  });
-
   test('Does not schedule after too many retries with infra failure', () async {
     request.bodyBytes = utf8.encode(pushMessageJson('COMPLETED',
         builderName: 'Linux',
diff --git a/app_dart/test/service/luci_build_service_test.dart b/app_dart/test/service/luci_build_service_test.dart
index 9473abb..3861944 100644
--- a/app_dart/test/service/luci_build_service_test.dart
+++ b/app_dart/test/service/luci_build_service_test.dart
@@ -11,10 +11,12 @@
     as push_message;
 import 'package:cocoon_service/src/request_handling/exceptions.dart';
 import 'package:cocoon_service/src/service/luci_build_service.dart';
+import 'package:github/github.dart';
 import 'package:mockito/mockito.dart';
 import 'package:test/test.dart';
 
 import '../src/datastore/fake_cocoon_config.dart';
+import '../src/request_handling/fake_logging.dart';
 import '../src/utilities/mocks.dart';
 import '../src/utilities/push_message.dart';
 
@@ -23,6 +25,7 @@
   FakeConfig config;
   MockBuildBucketClient mockBuildBucketClient;
   LuciBuildService service;
+  RepositorySlug slug;
   group('buildsForRepositoryAndPr', () {
     const Build macBuild = Build(
       id: 999,
@@ -50,6 +53,7 @@
       mockBuildBucketClient = MockBuildBucketClient();
       service =
           LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+      slug = RepositorySlug('flutter', 'cocoon');
     });
     test('Empty responses are handled correctly', () async {
       when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -64,7 +68,7 @@
         );
       });
       final Map<String, Build> builds =
-          await service.buildsForRepositoryAndPr('cocoon', 1, 'abcd');
+          await service.buildsForRepositoryAndPr(slug, 1, 'abcd');
       expect(builds.keys, isEmpty);
     });
 
@@ -86,7 +90,7 @@
         );
       });
       final Map<String, Build> builds =
-          await service.buildsForRepositoryAndPr('cocoon', 1, 'abcd');
+          await service.buildsForRepositoryAndPr(slug, 1, 'abcd');
       expect(builds,
           equals(<String, Build>{'Mac': macBuild, 'Linux': linuxBuild}));
     });
@@ -98,6 +102,8 @@
       mockBuildBucketClient = MockBuildBucketClient();
       service =
           LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+      service.setLogger(FakeLogging());
+      slug = RepositorySlug('flutter', 'cocoon');
     });
     test('try to schedule builds already started', () async {
       when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -124,7 +130,7 @@
       final bool result = await service.scheduleBuilds(
         prNumber: 1,
         commitSha: 'abc',
-        repositoryName: 'cocoon',
+        slug: slug,
       );
       expect(result, isFalse);
     });
@@ -153,7 +159,7 @@
       final bool result = await service.scheduleBuilds(
         prNumber: 1,
         commitSha: 'abc',
-        repositoryName: 'cocoon',
+        slug: slug,
       );
       expect(result, isFalse);
     });
@@ -170,16 +176,17 @@
       final bool result = await service.scheduleBuilds(
         prNumber: 1,
         commitSha: 'abc',
-        repositoryName: 'cocoon',
+        slug: slug,
       );
       expect(result, isTrue);
     });
     test('Try to schedule build on a unsupported repo', () async {
+      slug = RepositorySlug('flutter', 'notsupported');
       expect(
           () async => await service.scheduleBuilds(
                 prNumber: 1,
                 commitSha: 'abc',
-                repositoryName: 'notsupported',
+                slug: slug,
               ),
           throwsA(const TypeMatcher<BadRequestException>()));
     });
@@ -192,6 +199,7 @@
       mockBuildBucketClient = MockBuildBucketClient();
       service =
           LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+      slug = RepositorySlug('flutter', 'cocoon');
     });
     test('Cancel builds when build list is empty', () async {
       when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -199,7 +207,7 @@
           responses: <Response>[],
         );
       });
-      await service.cancelBuilds('cocoon', 1, 'abc', 'new builds');
+      await service.cancelBuilds(slug, 1, 'abc', 'new builds');
       verify(mockBuildBucketClient.batch(any)).called(1);
     });
     test('Cancel builds that are scheduled', () async {
@@ -221,7 +229,7 @@
           ],
         );
       });
-      await service.cancelBuilds('cocoon', 1, 'abc', 'new builds');
+      await service.cancelBuilds(slug, 1, 'abc', 'new builds');
       expect(
           verify(mockBuildBucketClient.batch(captureAny))
               .captured[1]
@@ -231,9 +239,10 @@
           json.decode('{"id": "998", "summaryMarkdown": "new builds"}'));
     });
     test('Cancel builds from unsuported repo', () async {
+      slug = RepositorySlug('flutter', 'notsupported');
       expect(
           () async => await service.cancelBuilds(
-                'notsupported',
+                slug,
                 1,
                 'abc',
                 'new builds',
@@ -249,6 +258,7 @@
       mockBuildBucketClient = MockBuildBucketClient();
       service =
           LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+      slug = RepositorySlug('flutter', 'cocoon');
     });
     test('Failed builds from an empty list', () async {
       when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -256,7 +266,8 @@
           responses: <Response>[],
         );
       });
-      final List<Build> result = await service.failedBuilds('cocoon', 1, 'abc');
+      config.luciTryBuildersValue = <Map<String, dynamic>>[];
+      final List<Build> result = await service.failedBuilds(slug, 1, 'abc');
       expect(result, isEmpty);
     });
     test('Failed builds from a list of builds with failures', () async {
@@ -278,12 +289,16 @@
           ],
         );
       });
-      final List<Build> result = await service.failedBuilds('cocoon', 1, 'abc');
+      config.luciTryBuildersValue = (json.decode(
+                  '[{"name": "Linux", "repo": "flutter", "taskName": "linux_bot"}]')
+              as List<dynamic>)
+          .cast<Map<String, dynamic>>();
+      final List<Build> result = await service.failedBuilds(slug, 1, 'abc');
       expect(result, hasLength(1));
     });
   });
   group('rescheduleBuild', () {
-    push_message.Build build;
+    push_message.BuildPushMessage buildPushMessage;
 
     setUp(() {
       serviceAccountInfo = const ServiceAccountInfo(email: 'abc@abcd.com');
@@ -296,18 +311,24 @@
         'COMPLETED',
         result: 'FAILURE',
         builderName: 'Linux Host Engine',
-      ))['build'] as Map<String, dynamic>;
-      build = push_message.Build.fromJson(json);
+      )) as Map<String, dynamic>;
+      buildPushMessage = push_message.BuildPushMessage.fromJson(json);
     });
     test('Reschedule an existing build', () async {
       final bool rescheduled = await service.rescheduleBuild(
-          commitSha: 'abc', builderName: 'mybuild', build: build, retries: 1);
+          commitSha: 'abc',
+          builderName: 'mybuild',
+          buildPushMessage: buildPushMessage,
+          retries: 1);
       expect(rescheduled, isTrue);
       verify(mockBuildBucketClient.scheduleBuild(any)).called(1);
     });
     test('Reschedule after too many retries', () async {
       final bool rescheduled = await service.rescheduleBuild(
-          commitSha: 'abc', builderName: 'mybuild', build: build, retries: 3);
+          commitSha: 'abc',
+          builderName: 'mybuild',
+          buildPushMessage: buildPushMessage,
+          retries: 3);
       expect(rescheduled, isFalse);
       verifyNever(mockBuildBucketClient.scheduleBuild(any));
     });