Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9741318
conductor(setup): Add conductor setup files
ievdokdm Feb 10, 2026
3c968d9
feat(service): Integrate presubmit guard and check details endpoints
ievdokdm Feb 10, 2026
436f22c
conductor(plan): Mark Phase 1 infrastructure tasks as complete
ievdokdm Feb 10, 2026
085b744
conductor(checkpoint): Checkpoint end of Phase 1: Infrastructure & Da…
ievdokdm Feb 10, 2026
3eac01d
conductor(plan): Mark Phase 1 as complete
ievdokdm Feb 10, 2026
81d8ef4
feat(ui): Implement PreSubmitView with sidebar, header, and log viewer
ievdokdm Feb 10, 2026
a498231
feat(ui): Finalize PreSubmitView layout and integration
ievdokdm Feb 10, 2026
3f2a646
conductor(plan): Mark all tasks as complete
ievdokdm Feb 10, 2026
99aff7d
chore(conductor): Mark track 'PreSubmitView' as complete
ievdokdm Feb 10, 2026
51bbe91
docs(conductor): Synchronize docs for track 'PreSubmitView'
ievdokdm Feb 10, 2026
c8aaffb
feat(ui): Update PreSubmitView and ShaSelector for improved UI and st…
ievdokdm Feb 11, 2026
a030503
feat(ui): Enhance PreSubmitView and related components for improved f…
ievdokdm Feb 11, 2026
edcac8b
Remove pr_dashboard_20260209 track files and associated assets
ievdokdm Feb 11, 2026
2e5f0d2
fix(ui): Update copyright notice in presubmit_view and streamline tes…
ievdokdm Feb 11, 2026
72c69fb
fix(ui): Remove unused mock SHA from PreSubmitView and update tests f…
ievdokdm Feb 11, 2026
f219dbc
fix(ui): Remove unnecessary blank line in PreSubmitView test for clea…
ievdokdm Feb 11, 2026
9dd0227
feat(ui): Enhance mock data handling in DevelopmentCocoonService and …
ievdokdm Feb 12, 2026
f57518f
feat: integration testing in dashboard/ with actual server (#4945)
jtmcdole Feb 12, 2026
843388e
176990 Implement get presubmit guard summaries api (#4946)
ievdokdm Feb 12, 2026
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
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ analyzer:
avoid_dynamic_calls: ignore
comment_references: ignore
deprecated_member_use_from_same_package: ignore
duplicate_ignore: ignore
lines_longer_than_80_chars: ignore
only_throw_errors: ignore
exclude:
Expand Down
5 changes: 4 additions & 1 deletion app_dart/bin/gae_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import 'package:cocoon_server/google_auth_provider.dart';
import 'package:cocoon_server/secret_manager.dart';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/server.dart';
import 'package:cocoon_service/src/request_handling/dashboard_authentication.dart';
import 'package:cocoon_service/src/foundation/appengine_utils.dart';
import 'package:cocoon_service/src/foundation/providers.dart';
import 'package:cocoon_service/src/service/big_query.dart';
import 'package:cocoon_service/src/service/build_status_service.dart';
import 'package:cocoon_service/src/service/commit_service.dart';
Expand All @@ -24,6 +25,8 @@ Future<void> main() async {
await withAppEngineServices(() async {
useLoggingPackageAdaptor();

Providers.contextProvider = () => AppEngineClientContext(context);

// This is bad, and I should feel bad, but I won't because the logging system
// is inherently bad. We're allocating the logger (or getting back one) and
// then turning it off - there is no way to "filter". Luckily; the library
Expand Down
3 changes: 3 additions & 0 deletions app_dart/lib/cocoon_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export 'src/foundation/context.dart';
export 'src/foundation/utils.dart';
export 'src/request_handlers/check_flaky_builders.dart';
export 'src/request_handlers/create_branch.dart';
Expand Down Expand Up @@ -32,6 +33,8 @@ export 'src/request_handlers/update_existing_flaky_issues.dart';
export 'src/request_handlers/vacuum_github_commits.dart';
export 'src/request_handling/authentication.dart';
export 'src/request_handling/cache_request_handler.dart';
export 'src/request_handling/checkrun_authentication.dart';
export 'src/request_handling/dashboard_authentication.dart';
export 'src/request_handling/pubsub.dart';
export 'src/request_handling/pubsub_authentication.dart';
export 'src/request_handling/request_handler.dart';
Expand Down
6 changes: 6 additions & 0 deletions app_dart/lib/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'cocoon_service.dart';
import 'src/request_handlers/get_engine_artifacts_ready.dart';
import 'src/request_handlers/get_presubmit_checks.dart';
import 'src/request_handlers/get_presubmit_guard.dart';
import 'src/request_handlers/get_presubmit_guard_summaries.dart';
import 'src/request_handlers/get_tree_status_changes.dart';
import 'src/request_handlers/github_webhook_replay.dart';
import 'src/request_handlers/lookup_hash.dart';
Expand Down Expand Up @@ -190,6 +191,11 @@ Server createServer({
authenticationProvider: authProvider,
firestore: firestore,
),
'/api/get-presubmit-guard-summaries': GetPresubmitGuardSummaries(
config: config,
authenticationProvider: authProvider,
firestore: firestore,
),
'/api/get-presubmit-checks': GetPresubmitChecks(
config: config,
authenticationProvider: authProvider,
Expand Down
15 changes: 15 additions & 0 deletions app_dart/lib/src/foundation/appengine_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2026 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:appengine/appengine.dart' as gae;

import 'context.dart';

class AppEngineClientContext implements ClientContext {
final gae.ClientContext _context;
AppEngineClientContext(this._context);

@override
bool get isDevelopmentEnvironment => _context.isDevelopmentEnvironment;
}
12 changes: 12 additions & 0 deletions app_dart/lib/src/foundation/context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2026 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.

/// A context for the current request.
///
/// This abstracts the underlying App Engine context to avoid direct dependencies
/// on package:appengine in core logic.
abstract class ClientContext {
/// Whether the application is running in the development environment.
bool get isDevelopmentEnvironment;
}
15 changes: 7 additions & 8 deletions app_dart/lib/src/foundation/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:appengine/appengine.dart' as gae;
import 'package:http/http.dart' as http;

import 'context.dart';
import 'typedefs.dart';

/// Class that holds static default providers.
Expand All @@ -18,17 +18,16 @@ class Providers {
/// * [HttpClientProvider], which defines this interface.
static http.Client freshHttpClient() => http.Client();

/// Default [gae.Logging] provider.
/// Initializes the [ClientContext] provider.
///
/// See also:
///
/// * [LoggingProvider], which defines this interface.
static gae.Logging serviceScopeLogger() => gae.loggingService;
/// This must be called before [serviceScopeContext] is used.
static ClientContextProvider contextProvider = () =>
throw UnimplementedError('ClientContext provider not initialized');

/// Default [gae.ClientContext] provider.
/// Default [ClientContext] provider.
///
/// See also:
///
/// * [ClientContextProvider], which defines this interface.
static gae.ClientContext serviceScopeContext() => gae.context;
static ClientContext serviceScopeContext() => contextProvider();
}
3 changes: 2 additions & 1 deletion app_dart/lib/src/foundation/typedefs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:appengine/appengine.dart';
import 'package:http/http.dart' as http;

import 'context.dart';

/// Signature for a function that returns an App Engine [ClientContext].
///
/// This is used in [AuthenticationProvider] to provide the client context
Expand Down
24 changes: 12 additions & 12 deletions app_dart/lib/src/request_handlers/get_presubmit_guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,18 @@ final class GetPresubmitGuard extends ApiRequestHandler {
// Consolidate metadata from the first record.
final first = guards.first;

final GuardStatus guardStatus;
if (guards.any((g) => g.failedBuilds > 0)) {
guardStatus = GuardStatus.failed;
} else if (guards.every(
(g) => g.failedBuilds == 0 && g.remainingBuilds == 0,
)) {
guardStatus = GuardStatus.succeeded;
} else if (guards.every((g) => g.remainingBuilds == g.builds.length)) {
guardStatus = GuardStatus.waitingForBackfill;
} else {
guardStatus = GuardStatus.inProgress;
}
final totalFailed = guards.fold<int>(0, (sum, g) => sum + g.failedBuilds);
final totalRemaining = guards.fold<int>(
0,
(sum, g) => sum + g.remainingBuilds,
);
final totalBuilds = guards.fold<int>(0, (sum, g) => sum + g.builds.length);

final guardStatus = GuardStatus.calculate(
failedBuilds: totalFailed,
remainingBuilds: totalRemaining,
totalBuilds: totalBuilds,
);

final response = rpc_model.PresubmitGuardResponse(
prNum: first.pullRequestId,
Expand Down
112 changes: 112 additions & 0 deletions app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2026 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 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:cocoon_common/guard_status.dart';
import 'package:cocoon_common/rpc_model.dart' as rpc_model;
import 'package:github/github.dart';
import 'package:meta/meta.dart';

import '../../cocoon_service.dart';
import '../model/firestore/presubmit_guard.dart';
import '../request_handling/api_request_handler.dart';
import '../service/firestore/unified_check_run.dart';

/// Request handler for retrieving all presubmit guards for a specific pull request.
///
/// GET: /api/get-presubmit-guard-summaries
///
/// Parameters:
/// repo: (string in query) required. The repository name (e.g., 'flutter').
/// pr: (int in query) required. The pull request number.
/// owner: (string in query) optional. The repository owner (e.g., 'flutter').
@immutable
final class GetPresubmitGuardSummaries extends ApiRequestHandler {
/// Defines the [GetPresubmitGuardSummaries] handler.
const GetPresubmitGuardSummaries({
required super.config,
required super.authenticationProvider,
required FirestoreService firestore,
}) : _firestore = firestore;

final FirestoreService _firestore;

/// The name of the query parameter for the repository name (e.g. 'flutter').
static const String kRepoParam = 'repo';

/// The name of the query parameter for the pull request number.
static const String kPRParam = 'pr';

/// The name of the query parameter for the repository owner (e.g. 'flutter').
static const String kOwnerParam = 'owner';

@override
Future<Response> get(Request request) async {
checkRequiredQueryParameters(request, [kRepoParam, kPRParam]);

final repo = request.uri.queryParameters[kRepoParam]!;
final prNumber = int.parse(request.uri.queryParameters[kPRParam]!);
final owner = request.uri.queryParameters[kOwnerParam] ?? 'flutter';

final slug = RepositorySlug(owner, repo);
final guards = await UnifiedCheckRun.getPresubmitGuardsForPullRequest(
firestoreService: _firestore,
slug: slug,
pullRequestId: prNumber,
);

if (guards.isEmpty) {
return Response.json({
'error': 'No guards found for PR $prNumber in $slug',
}, statusCode: HttpStatus.notFound);
}

// Group guards by commitSha
final groupedGuards = <String, List<PresubmitGuard>>{};
for (final guard in guards) {
groupedGuards.putIfAbsent(guard.commitSha, () => []).add(guard);
}

final responseGuards = <rpc_model.PresubmitGuardSummary>[];
for (final entry in groupedGuards.entries) {
final sha = entry.key;
final shaGuards = entry.value;

final totalFailed = shaGuards.fold<int>(
0,
(int sum, PresubmitGuard g) => sum + g.failedBuilds,
);
final totalRemaining = shaGuards.fold<int>(
0,
(int sum, PresubmitGuard g) => sum + g.remainingBuilds,
);
final totalBuilds = shaGuards.fold<int>(
0,
(int sum, PresubmitGuard g) => sum + g.builds.length,
);
final earliestCreationTime = shaGuards.fold<int>(
// assuming creation time is always in the past :)
DateTime.now().millisecondsSinceEpoch,
(int curr, PresubmitGuard g) => min(g.creationTime, curr),
);

responseGuards.add(
rpc_model.PresubmitGuardSummary(
commitSha: sha,
creationTime: earliestCreationTime,
guardStatus: GuardStatus.calculate(
failedBuilds: totalFailed,
remainingBuilds: totalRemaining,
totalBuilds: totalBuilds,
),
),
);
}

return Response.json(responseGuards);
}
}
2 changes: 1 addition & 1 deletion app_dart/lib/src/request_handling/authentication.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import 'dart:async';
import 'dart:io';

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

import '../foundation/context.dart';
import 'exceptions.dart';

@immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:cocoon_server/logging.dart';
import 'package:github/github.dart';
import 'package:meta/meta.dart';
Expand All @@ -15,7 +14,6 @@ import '../foundation/providers.dart';
import '../foundation/typedefs.dart';
import '../model/google/token_info.dart';
import '../service/firebase_jwt_validator.dart';
import 'dashboard_authentication.dart';
import 'exceptions.dart';

/// Class capable of authenticating [HttpRequest]s from the Checkrun page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:cocoon_server/logging.dart';
import 'package:meta/meta.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:cocoon_server/logging.dart';
import 'package:googleapis/oauth2/v2.dart';
import 'package:meta/meta.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:cocoon_server/logging.dart';
import 'package:meta/meta.dart';

Expand Down
13 changes: 13 additions & 0 deletions app_dart/lib/src/service/firestore/unified_check_run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,19 @@ final class UnifiedCheckRun {
);
}

/// Queries for [PresubmitGuard] records by [slug] and [pullRequestId].
static Future<List<PresubmitGuard>> getPresubmitGuardsForPullRequest({
required FirestoreService firestoreService,
required RepositorySlug slug,
required int pullRequestId,
}) async {
return await _queryPresubmitGuards(
firestoreService: firestoreService,
slug: slug,
pullRequestId: pullRequestId,
);
}

static Future<List<PresubmitGuard>> _queryPresubmitGuards({
required FirestoreService firestoreService,
Transaction? transaction,
Expand Down
2 changes: 2 additions & 0 deletions app_dart/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dev_dependencies:
build_runner: ^2.4.15
cocoon_common_test:
path: ../packages/cocoon_common_test
cocoon_integration_test:
path: ../packages/cocoon_integration_test
cocoon_server_test:
path: ../packages/cocoon_server_test
dart_flutter_team_lints: 3.5.2
Expand Down
4 changes: 1 addition & 3 deletions app_dart/test/foundation/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:io';

import 'package:cocoon_common/cocoon_common.dart';
import 'package:cocoon_common_test/cocoon_common_test.dart';
import 'package:cocoon_integration_test/testing.dart';
import 'package:cocoon_server/logging.dart';
import 'package:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/src/foundation/utils.dart';
Expand All @@ -18,9 +19,6 @@ import 'package:http/testing.dart';
import 'package:retry/retry.dart';
import 'package:test/test.dart';

import '../src/bigquery/fake_tabledata_resource.dart';
import '../src/utilities/entity_generators.dart';

const String branchRegExp = '''
master
flutter-1.1-candidate.1
Expand Down
4 changes: 1 addition & 3 deletions app_dart/test/model/ci_yaml/ci_yaml_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cocoon_integration_test/testing.dart';
import 'package:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/protos.dart' as pb;
import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart';
Expand All @@ -10,9 +11,6 @@ import 'package:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/flags/ci_yaml_flags.dart';
import 'package:test/test.dart';

import '../../src/model/ci_yaml_matcher.dart';
import '../../src/service/fake_scheduler.dart';

void main() {
useTestLoggerPerTest();

Expand Down
Loading