De-duplicate GitHub check runs by name and keep latest result (#5040)

Fixes https://github.com/flutter/flutter/issues/185898

When a workflow runs multiple times on the same commit (e.g. when triggered repeatedly by labeling events), superseded failures can unintentionally block pull requests during Auto-Submit validations.

This PR updates the CiSuccessful.validateCheckRuns validation logic to:

- Group checkRuns by checkRun.name.
- De-duplicate check runs of the same name by evaluating their startedAt timestamp.
- Exclusively retain and validate the latest result per check run name when proceeding with PR validation.
diff --git a/auto_submit/lib/validations/ci_successful.dart b/auto_submit/lib/validations/ci_successful.dart
index c6974bf..09fefc2 100644
--- a/auto_submit/lib/validations/ci_successful.dart
+++ b/auto_submit/lib/validations/ci_successful.dart
@@ -232,8 +232,34 @@
   ) {
     log.info('Validating name: ${slug.name}/$prNumber, checkRuns: $checkRuns');
 
+    // Deduplicate check runs by name, keeping the most recent one.
+    // Duplications can happen for example if a workflow is triggered by a label event.
+    // If the workflow has run more than once on the same commit, every result
+    // is included in this initial pull of check runs for the given commit.
+    final uniqueCheckRuns = <String, github.CheckRun>{};
+    log.info('Deduplicating check runs...');
+    for (final checkRun in checkRuns) {
+      final name = checkRun.name;
+      if (name == null) {
+        continue;
+      }
+      final existing = uniqueCheckRuns[name];
+      if (existing == null) {
+        uniqueCheckRuns[name] = checkRun;
+      } else {
+        final existingStartedAt = existing.startedAt;
+        final currentStartedAt = checkRun.startedAt;
+        if (currentStartedAt.isAfter(existingStartedAt)) {
+          uniqueCheckRuns[name] = checkRun;
+          log.info(
+            'Replaced ${existing.name} (${existing.startedAt}) with newer ${checkRun.name} (${checkRun.startedAt})',
+          );
+        }
+      }
+    }
+
     final staleCheckRuns = <github.CheckRun>[];
-    for (var checkRun in checkRuns) {
+    for (var checkRun in uniqueCheckRuns.values) {
       final name = checkRun.name;
 
       if (checkRun.name == Config.kMergeQueueLockName) {
diff --git a/auto_submit/test/validations/ci_successful_test.dart b/auto_submit/test/validations/ci_successful_test.dart
index dd74400..0561779 100644
--- a/auto_submit/test/validations/ci_successful_test.dart
+++ b/auto_submit/test/validations/ci_successful_test.dart
@@ -208,6 +208,52 @@
     });
 
     test(
+      'ValidateCheckRuns de-dupes checkRuns and uses latest startedAt.',
+      () async {
+        githubService.checkRunsData = dedupedCheckRunsSuccessMock;
+        final checkRuns = await githubService.getCheckRuns(slug, 'ref');
+        const allSuccess = true;
+
+        expect(
+          ciSuccessful.validateCheckRuns(
+            slug,
+            prNumber,
+            PullRequestState.open,
+            checkRuns,
+            failures,
+            allSuccess,
+            Author(login: 'testAuthor'),
+          ),
+          isTrue,
+        );
+        expect(failures, isEmpty);
+      },
+    );
+
+    test(
+      'ValidateCheckRuns de-dupes checkRuns and uses latest startedAt (failure).',
+      () async {
+        githubService.checkRunsData = dedupedCheckRunsFailureMock;
+        final checkRuns = await githubService.getCheckRuns(slug, 'ref');
+        const allSuccess = true;
+
+        expect(
+          ciSuccessful.validateCheckRuns(
+            slug,
+            prNumber,
+            PullRequestState.open,
+            checkRuns,
+            failures,
+            allSuccess,
+            Author(login: 'testAuthor'),
+          ),
+          isFalse,
+        );
+        expect(failures, hasLength(1));
+      },
+    );
+
+    test(
       'ValidateCheckRuns allSucces false but no failures recorded.',
       () async {
         /// This test just checks that a checkRun that has not yet completed and
diff --git a/auto_submit/test/validations/ci_successful_test_data.dart b/auto_submit/test/validations/ci_successful_test_data.dart
index 9638790..96ba50e 100644
--- a/auto_submit/test/validations/ci_successful_test_data.dart
+++ b/auto_submit/test/validations/ci_successful_test_data.dart
@@ -177,3 +177,66 @@
     }
   }
   ''';
+const String dedupedCheckRunsSuccessMock = '''{
+  "total_count": 2,
+  "check_runs": [
+    {
+      "id": 1,
+      "head_sha": "be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
+      "external_id": "",
+      "details_url": "https://example.com/failure",
+      "status": "completed",
+      "conclusion": "failure",
+      "started_at": "2018-05-04T01:14:52Z",
+      "name": "my_checkrun",
+      "check_suite": {
+        "id": 5
+      }
+    },
+    {
+      "id": 2,
+      "head_sha": "be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
+      "external_id": "",
+      "details_url": "https://example.com/success",
+      "status": "completed",
+      "conclusion": "success",
+      "started_at": "2018-05-04T02:14:52Z",
+      "name": "my_checkrun",
+      "check_suite": {
+        "id": 5
+      }
+    }
+  ]
+}''';
+
+const String dedupedCheckRunsFailureMock = '''{
+  "total_count": 2,
+  "check_runs": [
+    {
+      "id": 1,
+      "head_sha": "be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
+      "external_id": "",
+      "details_url": "https://example.com/success",
+      "status": "completed",
+      "conclusion": "success",
+      "started_at": "2018-05-04T01:14:52Z",
+      "name": "my_checkrun",
+      "check_suite": {
+        "id": 5
+      }
+    },
+    {
+      "id": 2,
+      "head_sha": "be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
+      "external_id": "",
+      "details_url": "https://example.com/failure",
+      "status": "completed",
+      "conclusion": "failure",
+      "started_at": "2018-05-04T02:14:52Z",
+      "name": "my_checkrun",
+      "check_suite": {
+        "id": 5
+      }
+    }
+  ]
+}''';