Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app_dart/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ dynamicTestSuppression: true
# The Gemini model to use for log analysis.
geminiModel: gemini-3-flash-preview

# Whether to allow unified check run flow to specific users or to everyone.
unifiedCheckRunFlow:
useForAll: true
useForUsers:
- ievdokdm
- eyebrowsoffire
- andywolff
- camsim99
- walley892
- loic-sharma
- vashworth
- mboetger
- justinmc

# Whether to process LUCI notifications of builds progress ordered within check run.
orderedPresubmit:
useForAll: true
Expand Down
1 change: 1 addition & 0 deletions app_dart/lib/cocoon_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export 'src/service/config.dart';
export 'src/service/firestore.dart';
export 'src/service/flags/dynamic_config.dart';
export 'src/service/flags/ordered_presubmit_flags.dart';
export 'src/service/flags/unified_check_run_flow_flags.dart';
export 'src/service/gerrit_service.dart';
export 'src/service/github_checks_service.dart';
export 'src/service/issue_service.dart';
Expand Down
14 changes: 14 additions & 0 deletions app_dart/lib/src/generated_config.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions app_dart/lib/src/model/common/presubmit_completed_check.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class PresubmitCompletedJob {
final int checkRunId;
final int? checkSuiteId;
final String? headBranch;
final bool isUnifiedCheckRun;
final CiStage? stage;
final int? prNum;
final int attempt;
Expand All @@ -52,6 +53,7 @@ class PresubmitCompletedJob {
required this.checkRunId,
required this.checkSuiteId,
required this.headBranch,
required this.isUnifiedCheckRun,
this.stage,
this.prNum,
this.attempt = 1,
Expand All @@ -78,6 +80,7 @@ class PresubmitCompletedJob {
checkRunId: userData.guardCheckRunId ?? userData.checkRunId!,
checkSuiteId: userData.checkSuiteId,
headBranch: userData.commit.branch,
isUnifiedCheckRun: userData.guardCheckRunId != null,
stage: userData.stage,
prNum: userData.pullRequestNumber,
attempt: _getAttempt(build),
Expand All @@ -100,7 +103,7 @@ class PresubmitCompletedJob {
cocoon_checks.CheckRun get checkRun {
return cocoon_checks.CheckRun(
id: checkRunId,
name: Config.kDashboardCheckName,
name: isUnifiedCheckRun ? Config.kDashboardCheckName : name,
headSha: sha,
conclusion: status.toConclusion(),
checkSuite: CheckSuite(
Expand All @@ -118,11 +121,7 @@ class PresubmitCompletedJob {
slug: slug,
prNum: prNum ?? 0,
checkRunId: checkRunId,
stage:
stage ??
(slug == Config.flutterSlug
? CiStage.fusionTests
: CiStage.genericTests),
stage: stage ?? CiStage.fusionTests,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Defaulting the stage to CiStage.fusionTests for non-fusion repositories (where slug != Config.flutterSlug) is incorrect and can cause mismatches when querying or updating staging documents in Firestore. It should default to CiStage.genericTests for non-fusion repositories.

Suggested change
stage: stage ?? CiStage.fusionTests,
stage:
stage ??
(slug == Config.flutterSlug
? CiStage.fusionTests
: CiStage.genericTests),

);
}

Expand Down
58 changes: 41 additions & 17 deletions app_dart/lib/src/request_handlers/presubmit_subscription.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ base class PresubmitSubscription extends SubscriptionHandler {
required super.subscriptionName,
super.authProvider,
}) : _ciYamlFetcher = ciYamlFetcher,
_githubChecksService = githubChecksService,
_luciBuildService = luciBuildService,
_scheduler = scheduler,
_firestore = firestore;

final LuciBuildService _luciBuildService;
final GithubChecksService _githubChecksService;
final CiYamlFetcher _ciYamlFetcher;
final Scheduler _scheduler;
final FirestoreService _firestore;
Expand Down Expand Up @@ -141,16 +143,20 @@ base class PresubmitSubscription extends SubscriptionHandler {
tagSet ??= BuildTags.fromStringPairs(build.tags);
final builderName = build.builder.builder;
var rescheduled = false;
final isUnifiedCheckRun = userData.guardCheckRunId != null;
log.info('Unified Check Run ${isUnifiedCheckRun ? 'Enabled' : 'Disabled'}');
if (build.status.isTaskFailed()) {
// If failed we need summaryMarkdown. For github check run flow this
// called in [GithubChecksService.updateCheckStatus(...)]
build = await _luciBuildService.getBuildById(
build.id,
buildMask: bbv2.BuildMask(
// Need to use allFields as there is a bug with fieldMask and summaryMarkdown.
allFields: true,
),
);
if (isUnifiedCheckRun) {
// If failed we need summaryMarkdown. For github check run flow this
// called in [GithubChecksService.updateCheckStatus(...)]
build = await _luciBuildService.getBuildById(
build.id,
buildMask: bbv2.BuildMask(
// Need to use allFields as there is a bug with fieldMask and summaryMarkdown.
allFields: true,
),
);
}
final maxAttempt = await _getMaxAttempt(
userData.commit,
builderName,
Expand All @@ -159,14 +165,17 @@ base class PresubmitSubscription extends SubscriptionHandler {
if (tagSet.currentAttempt < maxAttempt) {
rescheduled = true;
log.info('Rerunning failed task: $builderName');
await UnifiedCheckRun.reInitializeInProgressJob(
firestoreService: _firestore,
completedJob: PresubmitCompletedJob.fromBuild(
build,
userData,
summaryPrepend: '### ⚠️ Test failed but automatically rescheduled',
),
);
if (isUnifiedCheckRun) {
await UnifiedCheckRun.reInitializeInProgressJob(
firestoreService: _firestore,
completedJob: PresubmitCompletedJob.fromBuild(
build,
userData,
summaryPrepend:
'### ⚠️ Test failed but automatically rescheduled',
),
);
}
await _luciBuildService.reschedulePresubmitBuild(
builderName: builderName,
build: build,
Expand All @@ -190,6 +199,21 @@ base class PresubmitSubscription extends SubscriptionHandler {
'### ⚠️ Test failed but marked as suppressed on dashboard';
}
}
if (!isUnifiedCheckRun) {
if (userData.checkRunId == null) {
log.error('checkRunId is null for non-unified check run');
return;
}
await _githubChecksService.updateCheckStatus(
checkRunId: userData.checkRunId!,
build: build,
luciBuildService: _luciBuildService,
slug: userData.commit.slug,
rescheduled: rescheduled,
conclusionOverride: override,
summaryPrepend: suppressedMessage,
);
}
if (!rescheduled) {
final check = PresubmitCompletedJob.fromBuild(
build,
Expand Down
8 changes: 6 additions & 2 deletions app_dart/lib/src/service/firestore/unified_check_run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ final class UnifiedCheckRun {
CheckRun? mergeQueueGuard,
@visibleForTesting DateTime Function() utcNow = DateTime.timestamp,
}) async {
if (dashboardChecks != null && pullRequest != null) {
if (dashboardChecks != null &&
pullRequest != null &&
config.flags.isUnifiedCheckRunFlowEnabledForUser(
pullRequest.user!.login!,
)) {
// Create the presubmit_guard and associated presubmit_job documents.
log.info(
'Storing UnifiedCheckRun data for ${slug.fullName}#${pullRequest.number}.',
'Storing UnifiedCheckRun data for ${slug.fullName}#${pullRequest.number} as it enabled for user ${pullRequest.user!.login}.',
);
Comment on lines +39 to 47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the null assertion operator ! on pullRequest.user and pullRequest.user!.login can lead to runtime crashes if the user or login is null (e.g., for certain automated actions or deleted accounts). Use null-safe access with a fallback instead.

    if (dashboardChecks != null &&
        pullRequest != null &&
        config.flags.isUnifiedCheckRunFlowEnabledForUser(
          pullRequest.user?.login ?? '',
        )) {
      // Create the presubmit_guard and associated presubmit_job documents.
      log.info(
        'Storing UnifiedCheckRun data for ${slug.fullName}#${pullRequest.number} as it enabled for user ${pullRequest.user?.login}.',
      );

// We store the creation time of the guard since there might be several
// guards for the same PR created and each new one created after previous
Expand Down
17 changes: 17 additions & 0 deletions app_dart/lib/src/service/flags/dynamic_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'ci_yaml_flags.dart';
import 'content_aware_hashing_flags.dart';
import 'dynamic_config_updater.dart';
import 'ordered_presubmit_flags.dart';
import 'unified_check_run_flow_flags.dart';

part 'dynamic_config.g.dart';

Expand All @@ -38,6 +39,7 @@ final class DynamicConfig {
contentAwareHashing: ContentAwareHashing.defaultInstance,
closeMqGuardAfterPresubmit: false,
enableGeminiLogAnalysis: false,
unifiedCheckRunFlow: UnifiedCheckRunFlow.defaultInstance,
orderedPresubmit: OrderedPresubmit.defaultInstance,
dynamicTestSuppression: false,
geminiModel: 'gemini-3-flash-preview',
Expand Down Expand Up @@ -67,6 +69,10 @@ final class DynamicConfig {
@JsonKey()
final bool enableGeminiLogAnalysis;

/// Flags related tp unified check-run flow configuration.
@JsonKey()
final UnifiedCheckRunFlow unifiedCheckRunFlow;

/// Flags related to ordered presubmit configuration.
@JsonKey()
final OrderedPresubmit orderedPresubmit;
Expand All @@ -85,6 +91,7 @@ final class DynamicConfig {
required this.contentAwareHashing,
required this.closeMqGuardAfterPresubmit,
required this.enableGeminiLogAnalysis,
required this.unifiedCheckRunFlow,
required this.orderedPresubmit,
required this.dynamicTestSuppression,
required this.geminiModel,
Expand All @@ -99,6 +106,7 @@ final class DynamicConfig {
ContentAwareHashing? contentAwareHashing,
bool? closeMqGuardAfterPresubmit,
bool? enableGeminiLogAnalysis,
UnifiedCheckRunFlow? unifiedCheckRunFlow,
OrderedPresubmit? orderedPresubmit,
bool? dynamicTestSuppression,
String? geminiModel,
Expand All @@ -114,6 +122,8 @@ final class DynamicConfig {
defaultInstance.closeMqGuardAfterPresubmit,
enableGeminiLogAnalysis:
enableGeminiLogAnalysis ?? defaultInstance.enableGeminiLogAnalysis,
unifiedCheckRunFlow:
unifiedCheckRunFlow ?? defaultInstance.unifiedCheckRunFlow,
orderedPresubmit: orderedPresubmit ?? defaultInstance.orderedPresubmit,
dynamicTestSuppression:
dynamicTestSuppression ?? defaultInstance.dynamicTestSuppression,
Expand Down Expand Up @@ -149,6 +159,13 @@ final class DynamicConfig {
/// The inverse operation of [DynamicConfig.fromJson].
Map<String, Object?> toJson() => _$DynamicConfigToJson(this);

bool isUnifiedCheckRunFlowEnabledForUser(String githubUsername) {
if (unifiedCheckRunFlow.useForAll) {
return true;
}
return unifiedCheckRunFlow.useForUsers.contains(githubUsername);
}

bool isOrderedPresubmitEnabledForUser(String githubUsername) {
if (orderedPresubmit.useForAll) {
return true;
Expand Down
6 changes: 6 additions & 0 deletions app_dart/lib/src/service/flags/dynamic_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions app_dart/lib/src/service/flags/unified_check_run_flow_flags.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2025 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'unified_check_run_flow_flags.g.dart';

/// Flags related to content-aware hashing.
@JsonSerializable()
@immutable
final class UnifiedCheckRunFlow {
/// Default configuration for [UnifiedCheckRunFlow] flags.
static const defaultInstance = UnifiedCheckRunFlow._(
useForAll: false,
useForUsers: [],
);

/// Whether to use unified check-run flow with only one check-run created
/// for all LUCI tests or github check-run flow.
@JsonKey()
final bool useForAll;

/// List of users to use unified check-run flow.
@JsonKey()
final List<String> useForUsers;

const UnifiedCheckRunFlow._({
required this.useForAll, //
required this.useForUsers, //
});

/// Creates [UnifiedCheckRunFlow] flags from the provided fields.
///
/// Any omitted fields default to the values in [defaultInstance].
factory UnifiedCheckRunFlow({bool? useForAll, List<String>? useForUsers}) {
return UnifiedCheckRunFlow._(
useForAll: useForAll ?? defaultInstance.useForAll,
useForUsers: useForUsers ?? defaultInstance.useForUsers,
);
}

/// Creates [UnifiedCheckRunFlow] flags from a [json] object.
///
/// Any omitted fields default to the values in [defaultInstance].
factory UnifiedCheckRunFlow.fromJson(Map<String, Object?>? json) {
return _$UnifiedCheckRunFlowFromJson(json ?? {});
}

/// The inverse operation of [UnifiedCheckRunFlow.fromJson].
Map<String, Object?> toJson() => _$UnifiedCheckRunFlowToJson(this);
}
24 changes: 24 additions & 0 deletions app_dart/lib/src/service/flags/unified_check_run_flow_flags.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading