Skip to content

Commit 06c9b8d

Browse files
committed
implementaiton of re-run unified check run
1 parent 86c5690 commit 06c9b8d

File tree

6 files changed

+632
-41
lines changed

6 files changed

+632
-41
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2026 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// @docImport 'failed_presubmit_checks.dart';
6+
library;
7+
8+
import 'package:github/github.dart';
9+
10+
/// Contains the list of failed checks that are proposed to be re-run.
11+
///
12+
/// See: [UnifiedCheckRun.reInitializeFailedChecks]
13+
class FailedChecksForRerun {
14+
final CheckRun checkRunGuard;
15+
final List<String> checkNames;
16+
17+
const FailedChecksForRerun({
18+
required this.checkRunGuard,
19+
required this.checkNames,
20+
});
21+
22+
@override
23+
bool operator ==(Object other) =>
24+
identical(this, other) ||
25+
(other is FailedChecksForRerun &&
26+
other.checkRunGuard == checkRunGuard &&
27+
other.checkNames == checkNames);
28+
29+
@override
30+
int get hashCode => Object.hashAll([checkRunGuard, checkNames]);
31+
32+
@override
33+
String toString() => 'FailedChecksForRerun("$checkRunGuard", "$checkNames")';
34+
}

app_dart/lib/src/model/firestore/presubmit_check.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import '../../service/firestore.dart';
1515
import '../bbv2_extension.dart';
1616
import 'base.dart';
1717

18-
const String collectionId = 'presubmit_checks';
19-
2018
@immutable
2119
final class PresubmitCheckId extends AppDocumentId<PresubmitCheck> {
2220
PresubmitCheckId({
@@ -87,6 +85,7 @@ final class PresubmitCheckId extends AppDocumentId<PresubmitCheck> {
8785
}
8886

8987
final class PresubmitCheck extends AppDocument<PresubmitCheck> {
88+
static const collectionId = 'presubmit_checks';
9089
static const fieldCheckRunId = 'checkRunId';
9190
static const fieldBuildName = 'buildName';
9291
static const fieldBuildNumber = 'buildNumber';
@@ -181,10 +180,11 @@ final class PresubmitCheck extends AppDocument<PresubmitCheck> {
181180
required String buildName,
182181
required int checkRunId,
183182
required int creationTime,
183+
int? attemptNumber,
184184
}) {
185185
return PresubmitCheck(
186186
buildName: buildName,
187-
attemptNumber: 1,
187+
attemptNumber: attemptNumber ?? 1,
188188
checkRunId: checkRunId,
189189
creationTime: creationTime,
190190
status: TaskStatus.waitingForBackfill,

app_dart/lib/src/service/firestore/unified_check_run.dart

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:collection/collection.dart';
1111
import 'package:github/github.dart';
1212
import 'package:googleapis/firestore/v1.dart' hide Status;
1313

14+
import '../../model/common/failed_presubmit_checks.dart';
1415
import '../../model/common/presubmit_check_state.dart';
1516
import '../../model/common/presubmit_guard_conclusion.dart';
1617
import '../../model/firestore/base.dart';
@@ -76,6 +77,87 @@ final class UnifiedCheckRun {
7677
}
7778
}
7879

80+
static Future<FailedChecksForRerun> reInitializeFailedChecks({
81+
required FirestoreService firestoreService,
82+
required RepositorySlug slug,
83+
required CiStage stage,
84+
required PullRequest pullRequest,
85+
required int checkRunId,
86+
}) async {
87+
final logCrumb =
88+
'reInitializeFailedChecks(${slug.fullName}, $stage, ${pullRequest.number}, $checkRunId)';
89+
90+
log.info('$logCrumb Re-Running failed checks.');
91+
final transaction = await firestoreService.beginTransaction();
92+
final guardDocumentName = PresubmitGuard.documentNameFor(
93+
slug: slug,
94+
pullRequestId: pullRequest.number!,
95+
checkRunId: checkRunId,
96+
stage: stage,
97+
);
98+
final guardDocument = await firestoreService.getDocument(
99+
guardDocumentName,
100+
transaction: transaction,
101+
);
102+
final guard = PresubmitGuard.fromDocument(guardDocument);
103+
104+
final failedBuilds = guard.builds?.entries
105+
.where((entry) => entry.value.isFailure)
106+
.map((entry) => entry.key)
107+
.toList();
108+
109+
if (failedBuilds == null || failedBuilds.isEmpty) {
110+
log.info('$logCrumb No failed builds found.');
111+
return FailedChecksForRerun(
112+
checkRunGuard: guard.checkRun,
113+
checkNames: [],
114+
);
115+
}
116+
117+
final checks = [
118+
for (final buildName in failedBuilds)
119+
PresubmitCheck.init(
120+
buildName: buildName,
121+
checkRunId: checkRunId,
122+
creationTime: pullRequest.createdAt!.microsecondsSinceEpoch,
123+
attemptNumber:
124+
((await getLatestPresubmitCheck(
125+
firestoreService: firestoreService,
126+
checkRunId: checkRunId,
127+
buildName: buildName,
128+
transaction: transaction,
129+
))?.attemptNumber ??
130+
0) +
131+
1, // Increment the latest attempt number.
132+
),
133+
];
134+
135+
guard.failedBuilds = 0;
136+
guard.remainingBuilds = failedBuilds.length;
137+
final builds = guard.builds!;
138+
for (final buildName in failedBuilds) {
139+
builds[buildName] = TaskStatus.waitingForBackfill;
140+
}
141+
guard.builds = builds;
142+
143+
try {
144+
final response = await firestoreService.commit(
145+
transaction,
146+
documentsToWrites([...checks, guard]),
147+
);
148+
log.info(
149+
'$logCrumb: results = ${response.writeResults?.map((e) => e.toJson())}',
150+
);
151+
return FailedChecksForRerun(
152+
checkRunGuard: guard.checkRun,
153+
checkNames: failedBuilds,
154+
);
155+
} catch (e) {
156+
log.info('$logCrumb: failed to update presubmit check', e);
157+
rethrow;
158+
}
159+
}
160+
79161
/// Returns _all_ checks running against the specified github [checkRunId].
80162
static Future<List<PresubmitCheck>> queryAllPresubmitChecksForGuard({
81163
required FirestoreService firestoreService,
@@ -112,25 +194,47 @@ final class UnifiedCheckRun {
112194
)).firstOrNull;
113195
}
114196

197+
/// Returns the latest check for the specified github [checkRunId] and
198+
/// [buildName].
199+
static Future<PresubmitCheck?> getLatestPresubmitCheck({
200+
required FirestoreService firestoreService,
201+
required int checkRunId,
202+
required String buildName,
203+
Transaction? transaction,
204+
}) async {
205+
return (await _queryPresubmitChecks(
206+
firestoreService: firestoreService,
207+
checkRunId: checkRunId,
208+
buildName: buildName,
209+
status: null,
210+
transaction: transaction,
211+
limit: 1,
212+
)).firstOrNull;
213+
}
214+
115215
static Future<List<PresubmitCheck>> _queryPresubmitChecks({
116216
required FirestoreService firestoreService,
117217
required int checkRunId,
118218
String? buildName,
119219
TaskStatus? status,
120220
Transaction? transaction,
121221
int? attemptNumber,
222+
// By default order by attempt number descending to get the latest check first.
223+
Map<String, String>? orderMap = const {
224+
PresubmitCheck.fieldAttemptNumber: kQueryOrderDescending,
225+
},
226+
int? limit,
122227
}) async {
123228
final filterMap = {
124229
'${PresubmitCheck.fieldCheckRunId} =': checkRunId,
125230
'${PresubmitCheck.fieldBuildName} =': ?buildName,
126231
'${PresubmitCheck.fieldStatus} =': ?status?.value,
127232
'${PresubmitCheck.fieldAttemptNumber} =': ?attemptNumber,
128233
};
129-
// For tasks, therer is no reason to _not_ order this way.
130-
final orderMap = {PresubmitCheck.fieldCreationTime: kQueryOrderDescending};
131234
final documents = await firestoreService.query(
132-
collectionId,
235+
PresubmitCheck.collectionId,
133236
filterMap,
237+
limit: limit,
134238
orderMap: orderMap,
135239
transaction: transaction,
136240
);
@@ -285,7 +389,7 @@ final class UnifiedCheckRun {
285389
if (e.status == 404) {
286390
// An attempt to read a document not in firestore should not be retried.
287391
log.info(
288-
'$logCrumb: $collectionId document not found for $transaction',
392+
'$logCrumb: ${PresubmitCheck.collectionId} document not found for $transaction',
289393
);
290394
await firestoreService.rollback(transaction);
291395
return PresubmitGuardConclusion(
@@ -296,7 +400,7 @@ final class UnifiedCheckRun {
296400
summary: 'Internal server error',
297401
details:
298402
'''
299-
$collectionId document not found for stage "${guardId.stage}" for $changeCrumb. Got 404 from Firestore.
403+
${PresubmitCheck.collectionId} document not found for stage "${guardId.stage}" for $changeCrumb. Got 404 from Firestore.
300404
Error: ${e.toString()}
301405
$stack
302406
''',
@@ -341,7 +445,7 @@ For CI stage ${guardId.stage}:
341445
'to "${state.status.name}".',
342446
);
343447
} catch (e) {
344-
log.info('$logCrumb: failed to update presubmit check', e);
448+
log.error('$logCrumb: failed to update presubmit check', e);
345449
rethrow;
346450
}
347451
}

app_dart/lib/src/service/scheduler.dart

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ class Scheduler {
382382
tasks: [],
383383
pullRequest: pullRequest,
384384
config: _config,
385+
checkRun: lock,
385386
);
386387

387388
await _runCiTestingStage(
@@ -1550,38 +1551,8 @@ $stacktrace
15501551
);
15511552
}
15521553

1553-
final isFusion = slug == Config.flutterSlug;
1554-
final List<Target> presubmitTargets;
1555-
final EngineArtifacts engineArtifacts;
1556-
if (isFusion) {
1557-
// Fusion repos have presubmits split across two .ci.yaml files.
1558-
// /ci.yaml
1559-
// /engine/src/flutter/.ci.yaml
1560-
presubmitTargets = [
1561-
...await getPresubmitTargets(pullRequest),
1562-
...await getPresubmitTargets(
1563-
pullRequest,
1564-
type: CiType.fusionEngine,
1565-
),
1566-
];
1567-
final opt = await _filesChangedOptimizer.checkPullRequest(
1568-
pullRequest,
1569-
);
1570-
if (opt.shouldUsePrebuiltEngine) {
1571-
engineArtifacts = EngineArtifacts.usingExistingEngine(
1572-
commitSha: pullRequest.base!.sha!,
1573-
);
1574-
} else {
1575-
engineArtifacts = EngineArtifacts.builtFromSource(
1576-
commitSha: pullRequest.head!.sha!,
1577-
);
1578-
}
1579-
} else {
1580-
presubmitTargets = await getPresubmitTargets(pullRequest);
1581-
engineArtifacts = const EngineArtifacts.noFrameworkTests(
1582-
reason: 'Not flutter/flutter',
1583-
);
1584-
}
1554+
final (presubmitTargets, engineArtifacts) =
1555+
await _getAllTargetsForPullRequest(slug, pullRequest);
15851556

15861557
final target = presubmitTargets.firstWhereOrNull(
15871558
(target) => checkRunEvent.checkRun!.name == target.name,
@@ -1657,6 +1628,64 @@ $stacktrace
16571628
log.info(
16581629
'Requested to re-run failed tests for ${checkRunEvent.checkRun!.id} check-run id',
16591630
);
1631+
// The CheckRunEvent.checkRun.pullRequests array is empty for this
1632+
// event, so we need to find the matching pull request.
1633+
final slug = checkRunEvent.repository!.slug();
1634+
final headSha = checkRunEvent.checkRun!.headSha!;
1635+
final checkSuiteId = checkRunEvent.checkRun!.checkSuite!.id!;
1636+
final pullRequest = await _githubChecksService
1637+
.findMatchingPullRequest(slug, headSha, checkSuiteId);
1638+
1639+
var stage = CiStage.fusionEngineBuild;
1640+
1641+
var failedChecks = await UnifiedCheckRun.reInitializeFailedChecks(
1642+
firestoreService: _firestore,
1643+
slug: slug,
1644+
stage: stage,
1645+
pullRequest: pullRequest!,
1646+
checkRunId: checkRunEvent.checkRun!.id!,
1647+
);
1648+
if (failedChecks.checkNames.isEmpty) {
1649+
stage = CiStage.fusionTests;
1650+
failedChecks = await UnifiedCheckRun.reInitializeFailedChecks(
1651+
firestoreService: _firestore,
1652+
slug: slug,
1653+
stage: stage,
1654+
pullRequest: pullRequest,
1655+
checkRunId: checkRunEvent.checkRun!.id!,
1656+
);
1657+
}
1658+
if (failedChecks.checkNames.isEmpty) {
1659+
log.error(
1660+
'No failed targets found for ${checkRunEvent.checkRun!.id} check-run id',
1661+
);
1662+
return ProcessCheckRunResult.missingEntity(
1663+
'No failed targets found for ${checkRunEvent.checkRun!.id} check-run id',
1664+
);
1665+
}
1666+
1667+
final (targets, artifacts) = await _getAllTargetsForPullRequest(
1668+
slug,
1669+
pullRequest,
1670+
);
1671+
1672+
final failedTargets = targets
1673+
.where((target) => failedChecks.checkNames.contains(target.name))
1674+
.toList();
1675+
if (failedTargets.length != failedChecks.checkNames.length) {
1676+
log.error('Failed to find all failed targets in presubmit targets');
1677+
return const ProcessCheckRunResult.missingEntity(
1678+
'Failed to find all failed targets in presubmit targets',
1679+
);
1680+
}
1681+
1682+
await _luciBuildService.scheduleTryBuilds(
1683+
targets: failedTargets,
1684+
pullRequest: pullRequest,
1685+
engineArtifacts: artifacts,
1686+
checkRunGuard: failedChecks.checkRunGuard,
1687+
stage: stage,
1688+
);
16601689
} else {
16611690
log.warn(
16621691
'Requested unexpected action: ${checkRunEvent.requestedAction?.identifier} for ${checkRunEvent.checkRun!.id} check-run id',
@@ -1668,6 +1697,41 @@ $stacktrace
16681697
return const ProcessCheckRunResult.success();
16691698
}
16701699

1700+
1701+
Future<(List<Target>, EngineArtifacts)> _getAllTargetsForPullRequest(
1702+
RepositorySlug slug,
1703+
PullRequest pullRequest,
1704+
) async {
1705+
final isFusion = slug == Config.flutterSlug;
1706+
final List<Target> presubmitTargets;
1707+
final EngineArtifacts engineArtifacts;
1708+
if (isFusion) {
1709+
// Fusion repos have presubmits split across two .ci.yaml files.
1710+
// /ci.yaml
1711+
// /engine/src/flutter/.ci.yaml
1712+
presubmitTargets = [
1713+
...await getPresubmitTargets(pullRequest),
1714+
...await getPresubmitTargets(pullRequest, type: CiType.fusionEngine),
1715+
];
1716+
final opt = await _filesChangedOptimizer.checkPullRequest(pullRequest);
1717+
if (opt.shouldUsePrebuiltEngine) {
1718+
engineArtifacts = EngineArtifacts.usingExistingEngine(
1719+
commitSha: pullRequest.base!.sha!,
1720+
);
1721+
} else {
1722+
engineArtifacts = EngineArtifacts.builtFromSource(
1723+
commitSha: pullRequest.head!.sha!,
1724+
);
1725+
}
1726+
} else {
1727+
presubmitTargets = await getPresubmitTargets(pullRequest);
1728+
engineArtifacts = const EngineArtifacts.noFrameworkTests(
1729+
reason: 'Not flutter/flutter',
1730+
);
1731+
}
1732+
return (presubmitTargets, engineArtifacts);
1733+
}
1734+
16711735
/// Push [Commit] to BigQuery as part of the infra metrics dashboards.
16721736
Future<void> _uploadToBigQuery(fs.Commit commit) async {
16731737
const projectId = 'flutter-dashboard';

0 commit comments

Comments
 (0)