Support postsubmit checkruns for plugins/packages (#2254)

diff --git a/app_dart/bin/server.dart b/app_dart/bin/server.dart
index 36b641f..d0dbc37 100644
--- a/app_dart/bin/server.dart
+++ b/app_dart/bin/server.dart
@@ -103,6 +103,7 @@
         config: config,
         luciBuildService: luciBuildService,
         scheduler: scheduler,
+        githubChecksService: githubChecksService,
       ),
       '/api/push-build-status-to-github': PushBuildStatusToGithub(
         config: config,
diff --git a/app_dart/lib/src/request_handlers/postsubmit_luci_subscription.dart b/app_dart/lib/src/request_handlers/postsubmit_luci_subscription.dart
index 6be1cd4..7db236a 100644
--- a/app_dart/lib/src/request_handlers/postsubmit_luci_subscription.dart
+++ b/app_dart/lib/src/request_handlers/postsubmit_luci_subscription.dart
@@ -7,6 +7,7 @@
 import 'package:cocoon_service/ci_yaml.dart';
 import 'package:cocoon_service/src/service/luci_build_service.dart';
 import 'package:gcloud/db.dart';
+import 'package:github/github.dart';
 import 'package:meta/meta.dart';
 
 import '../model/appengine/commit.dart';
@@ -17,6 +18,7 @@
 import '../request_handling/subscription_handler.dart';
 import '../service/datastore.dart';
 import '../service/logging.dart';
+import '../service/github_checks_service.dart';
 import '../service/scheduler.dart';
 
 /// An endpoint for listening to build updates for postsubmit builds.
@@ -35,11 +37,13 @@
     @visibleForTesting this.datastoreProvider = DatastoreService.defaultProvider,
     required this.luciBuildService,
     required this.scheduler,
+    required this.githubChecksService,
   }) : super(subscriptionName: 'luci-postsubmit');
 
   final DatastoreServiceProvider datastoreProvider;
   final LuciBuildService luciBuildService;
   final Scheduler scheduler;
+  final GithubChecksService githubChecksService;
 
   @override
   Future<Body> post() async {
@@ -71,6 +75,22 @@
       userData = jsonDecode(String.fromCharCodes(base64.decode(buildPushMessage.userData!))) as Map<String, dynamic>;
     }
 
+    if (userData.containsKey('repo_owner') && userData.containsKey('repo_name')) {
+      // Message is coming from a github checks api (postsubmit) enabled repo. We need to
+      // create the slug from the data in the message and send the check status
+      // update.
+
+      RepositorySlug slug = RepositorySlug(
+        userData['repo_owner'] as String,
+        userData['repo_name'] as String,
+      );
+      await githubChecksService.updateCheckStatus(
+        buildPushMessage,
+        luciBuildService,
+        slug,
+      );
+    }
+
     final String? rawTaskKey = userData['task_key'] as String?;
     final String? rawCommitKey = userData['commit_key'] as String?;
     if (rawCommitKey == null) {
diff --git a/app_dart/lib/src/service/config.dart b/app_dart/lib/src/service/config.dart
index 5960e96..5b1a89f 100644
--- a/app_dart/lib/src/service/config.dart
+++ b/app_dart/lib/src/service/config.dart
@@ -49,6 +49,14 @@
         pluginsSlug,
       };
 
+  /// List of Github postsubmit supported repos.
+  ///
+  /// This adds support for check runs to the repo.
+  Set<gh.RepositorySlug> get postsubmitSupportedRepos => <gh.RepositorySlug>{
+        packagesSlug,
+        pluginsSlug,
+      };
+
   /// List of Cirrus supported repos.
   static Set<String> cirrusSupportedRepos = <String>{'plugins', 'packages', 'flutter'};
 
@@ -417,4 +425,8 @@
   bool githubPresubmitSupportedRepo(gh.RepositorySlug slug) {
     return supportedRepos.contains(slug);
   }
+
+  bool githubPostsubmitSupportedRepo(gh.RepositorySlug slug) {
+    return postsubmitSupportedRepos.contains(slug);
+  }
 }
diff --git a/app_dart/lib/src/service/github_checks_service.dart b/app_dart/lib/src/service/github_checks_service.dart
index 18c83f1..42f48fa 100644
--- a/app_dart/lib/src/service/github_checks_service.dart
+++ b/app_dart/lib/src/service/github_checks_service.dart
@@ -101,7 +101,7 @@
     // If status has completed with failure then provide more details.
     if (status == github.CheckRunStatus.completed && failedStatesSet.contains(conclusion)) {
       final Build build =
-          await luciBuildService.getTryBuildById(buildPushMessage.build!.id, fields: 'id,builder,summaryMarkdown');
+          await luciBuildService.getBuildById(buildPushMessage.build!.id, fields: 'id,builder,summaryMarkdown');
       output = github.CheckRunOutput(title: checkRun.name!, summary: getGithubSummary(build.summaryMarkdown));
     }
     log.fine('Updating check run with output: [$output]');
diff --git a/app_dart/lib/src/service/luci_build_service.dart b/app_dart/lib/src/service/luci_build_service.dart
index 3af821d..bcb0d96 100644
--- a/app_dart/lib/src/service/luci_build_service.dart
+++ b/app_dart/lib/src/service/luci_build_service.dart
@@ -390,7 +390,7 @@
 
   /// Gets [Build] using its [id] and passing the additional
   /// fields to be populated in the response.
-  Future<Build> getTryBuildById(String? id, {String? fields}) async {
+  Future<Build> getBuildById(String? id, {String? fields}) async {
     final GetBuildRequest request = GetBuildRequest(id: id, fields: fields);
     return buildBucketClient.getBuild(request);
   }
@@ -429,7 +429,7 @@
       if (!availableBuilderSet.contains(tuple.first.value.name)) {
         continue;
       }
-      final ScheduleBuildRequest scheduleBuildRequest = _createPostsubmitScheduleBuild(
+      final ScheduleBuildRequest scheduleBuildRequest = await _createPostsubmitScheduleBuild(
         commit: commit,
         target: tuple.first,
         task: tuple.second,
@@ -499,14 +499,14 @@
   /// Creates a [ScheduleBuildRequest] for [target] and [task] against [commit].
   ///
   /// By default, build [priority] is increased for release branches.
-  ScheduleBuildRequest _createPostsubmitScheduleBuild({
+  Future<ScheduleBuildRequest> _createPostsubmitScheduleBuild({
     required Commit commit,
     required Target target,
     required Task task,
     Map<String, Object>? properties,
     Map<String, List<String>>? tags,
     int priority = kDefaultPriority,
-  }) {
+  }) async {
     tags ??= <String, List<String>>{};
     tags.addAll(<String, List<String>>{
       'buildset': <String>[
@@ -521,10 +521,13 @@
     log.info('Task commit_key: $commitKey for task name: ${task.name}');
     log.info('Task task_key: $taskKey for task name: ${task.name}');
 
-    final Map<String, String> rawUserData = <String, String>{
+    final Map<String, dynamic> rawUserData = <String, dynamic>{
       'commit_key': commitKey,
       'task_key': taskKey,
     };
+
+    await createPostsubmitCheckRun(commit, target, rawUserData);
+
     tags['user_agent'] = <String>['flutter-cocoon'];
     // Tag `scheduler_job_id` is needed when calling buildbucket search build API.
     tags['scheduler_job_id'] = <String>['flutter/${target.value.name}'];
@@ -559,6 +562,29 @@
     );
   }
 
+  /// Creates postsubmit check runs for supported repositories.
+  Future<void> createPostsubmitCheckRun(
+    Commit commit,
+    Target target,
+    Map<String, dynamic> rawUserData,
+  ) async {
+    if (!config.githubPostsubmitSupportedRepo(commit.slug)) {
+      return;
+    }
+    final github.CheckRun checkRun = await githubChecksUtil.createCheckRun(
+      config,
+      target.slug,
+      commit.sha!,
+      target.value.name,
+    );
+    rawUserData['check_run_id'] = checkRun.id;
+    rawUserData['commit_sha'] = commit.sha;
+    rawUserData['commit_branch'] = commit.branch;
+    rawUserData['builder_name'] = target.value.name;
+    rawUserData['repo_owner'] = target.slug.owner;
+    rawUserData['repo_name'] = target.slug.name;
+  }
+
   /// Check to auto-rerun TOT test failures.
   ///
   /// A builder will be retried if:
@@ -584,7 +610,7 @@
     final BatchRequest request = BatchRequest(
       requests: <Request>[
         Request(
-          scheduleBuild: _createPostsubmitScheduleBuild(
+          scheduleBuild: await _createPostsubmitScheduleBuild(
             commit: commit,
             target: target,
             task: task,
diff --git a/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart b/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart
index b87d5e0..84c5c8d 100644
--- a/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart
+++ b/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart
@@ -8,6 +8,7 @@
 import 'package:cocoon_service/src/request_handling/exceptions.dart';
 import 'package:cocoon_service/src/service/datastore.dart';
 import 'package:gcloud/db.dart';
+import 'package:mockito/mockito.dart';
 import 'package:test/test.dart';
 
 import '../src/datastore/fake_config.dart';
@@ -17,6 +18,7 @@
 import '../src/service/fake_luci_build_service.dart';
 import '../src/service/fake_scheduler.dart';
 import '../src/utilities/entity_generators.dart';
+import '../src/utilities/mocks.dart';
 import '../src/utilities/push_message.dart';
 
 void main() {
@@ -24,13 +26,16 @@
   late FakeConfig config;
   late FakeHttpRequest request;
   late SubscriptionTester tester;
+  late MockGithubChecksService mockGithubChecksService;
 
   setUp(() async {
     config = FakeConfig(maxLuciTaskRetriesValue: 3);
+    mockGithubChecksService = MockGithubChecksService();
     handler = PostsubmitLuciSubscription(
       cache: CacheService(inMemory: true),
       config: config,
       authProvider: FakeAuthenticationProvider(),
+      githubChecksService: mockGithubChecksService,
       datastoreProvider: (_) => DatastoreService(config.db, 5),
       luciBuildService: FakeLuciBuildService(config: config),
       scheduler: FakeScheduler(
@@ -145,4 +150,25 @@
     expect(await tester.post(handler), Body.empty);
     expect(task.status, Task.statusInProgress);
   });
+
+  test('Requests with repo_owner and repo_name update checks', () async {
+    when(mockGithubChecksService.updateCheckStatus(any, any, any)).thenAnswer((_) async => true);
+    final Commit commit = generateCommit(1, sha: '87f88734747805589f2131753620d61b22922822');
+    final Task task = generateTask(
+      4507531199512576,
+      parent: commit,
+    );
+    config.db.values[task.key] = task;
+
+    tester.message = createBuildbucketPushMessage(
+      'COMPLETED',
+      result: 'SUCCESS',
+      builderName: 'Linux Packages',
+      // Use escaped string to mock json decoded ones.
+      userData:
+          '{\\"task_key\\":\\"${task.key.id}\\", \\"commit_key\\":\\"${task.key.parent?.id}\\", \\"repo_owner\\": \\"flutter\\", \\"repo_name\\": \\"packages\\"}',
+    );
+    await tester.post(handler);
+    verify(mockGithubChecksService.updateCheckStatus(any, any, any)).called(1);
+  });
 }
diff --git a/app_dart/test/service/luci_build_service_test.dart b/app_dart/test/service/luci_build_service_test.dart
index 3313adc..75d80d4 100644
--- a/app_dart/test/service/luci_build_service_test.dart
+++ b/app_dart/test/service/luci_build_service_test.dart
@@ -408,6 +408,7 @@
       service = LuciBuildService(
         config: FakeConfig(),
         buildBucketClient: mockBuildBucketClient,
+        githubChecksUtil: mockGithubChecksUtil,
         pubsub: pubsub,
       );
     });
@@ -462,6 +463,52 @@
           'debian-10.12');
     });
 
+    test('schedule postsubmit builds with correct userData for checkRuns', () async {
+      when(mockGithubChecksUtil.createCheckRun(any, any, any, any))
+          .thenAnswer((_) async => generateCheckRun(1, name: 'Linux 1'));
+      final Commit commit = generateCommit(0, repo: 'packages');
+      when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
+        return const ListBuildersResponse(builders: [
+          BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'Linux 1')),
+        ]);
+      });
+      final Tuple<Target, Task, int> toBeScheduled = Tuple<Target, Task, int>(
+        generateTarget(1,
+            properties: <String, String>{
+              'os': 'debian-10.12',
+            },
+            slug: RepositorySlug('flutter', 'packages')),
+        generateTask(1),
+        LuciBuildService.kDefaultPriority,
+      );
+      await service.schedulePostsubmitBuilds(
+        commit: commit,
+        toBeScheduled: <Tuple<Target, Task, int>>[
+          toBeScheduled,
+        ],
+      );
+      // Only one batch request should be published
+      expect(pubsub.messages.length, 1);
+      final BatchRequest request = pubsub.messages.first as BatchRequest;
+      expect(request.requests?.single.scheduleBuild, isNotNull);
+      final ScheduleBuildRequest scheduleBuild = request.requests!.single.scheduleBuild!;
+      expect(scheduleBuild.builderId.bucket, 'prod');
+      expect(scheduleBuild.builderId.builder, 'Linux 1');
+      expect(scheduleBuild.notify?.pubsubTopic, 'projects/flutter-dashboard/topics/luci-builds-prod');
+      final Map<String, dynamic> userData =
+          jsonDecode(String.fromCharCodes(base64Decode(scheduleBuild.notify!.userData!))) as Map<String, dynamic>;
+      expect(userData, <String, dynamic>{
+        'commit_key': 'flutter/flutter/master/1',
+        'task_key': '1',
+        'check_run_id': 1,
+        'commit_sha': '0',
+        'commit_branch': 'master',
+        'builder_name': 'Linux 1',
+        'repo_owner': 'flutter',
+        'repo_name': 'packages'
+      });
+    });
+
     test('Skip non-existing builder', () async {
       final Commit commit = generateCommit(0);
       when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
diff --git a/app_dart/test/src/datastore/fake_config.dart b/app_dart/test/src/datastore/fake_config.dart
index 2116840..0400751 100644
--- a/app_dart/test/src/datastore/fake_config.dart
+++ b/app_dart/test/src/datastore/fake_config.dart
@@ -52,6 +52,7 @@
     this.flutterGoldFollowUpAlertValue,
     this.flutterGoldDraftChangeValue,
     this.flutterGoldStalePRValue,
+    this.postsubmitSupportedReposValue,
     this.supportedBranchesValue,
     this.supportedReposValue,
     this.batchSizeValue,
@@ -96,6 +97,7 @@
   List<String>? supportedBranchesValue;
   String? overrideTreeStatusLabelValue;
   Set<gh.RepositorySlug>? supportedReposValue;
+  Set<gh.RepositorySlug>? postsubmitSupportedReposValue;
   Duration? githubRequestDelayValue;
 
   @override
@@ -231,6 +233,14 @@
   }
 
   @override
+  bool githubPostsubmitSupportedRepo(gh.RepositorySlug slug) {
+    return <gh.RepositorySlug>[
+      Config.packagesSlug,
+      Config.pluginsSlug,
+    ].contains(slug);
+  }
+
+  @override
   Future<String> generateGithubToken(gh.RepositorySlug slug) {
     throw UnimplementedError();
   }
@@ -268,6 +278,10 @@
   Set<gh.RepositorySlug> get supportedRepos => supportedReposValue ?? <gh.RepositorySlug>{Config.flutterSlug};
 
   @override
+  Set<gh.RepositorySlug> get postsubmitSupportedRepos =>
+      postsubmitSupportedReposValue ?? <gh.RepositorySlug>{Config.packagesSlug};
+
+  @override
   Future<Iterable<Branch>> getBranches(gh.RepositorySlug slug) async {
     if (supportedBranchesValue == null) {
       throw Exception('Test must set suportedBranchesValue to be able to use Config.getBranches');