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
+ }
+ }
+ ]
+}''';