From 8a4ddb441f0b0d1274af7608bef9d0c2a7540a59 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 14:53:11 -0800 Subject: [PATCH 01/16] conductor spec --- cocoon.code-workspace | 5 ++- conductor/tracks.md | 1 + .../get_presubmit_guard_20260204/index.md | 5 +++ .../metadata.json | 8 ++++ .../get_presubmit_guard_20260204/plan.md | 36 +++++++++++++++++ .../get_presubmit_guard_20260204/spec.md | 40 +++++++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 conductor/tracks/get_presubmit_guard_20260204/index.md create mode 100644 conductor/tracks/get_presubmit_guard_20260204/metadata.json create mode 100644 conductor/tracks/get_presubmit_guard_20260204/plan.md create mode 100644 conductor/tracks/get_presubmit_guard_20260204/spec.md diff --git a/cocoon.code-workspace b/cocoon.code-workspace index 85a636ce9..b4b45992c 100644 --- a/cocoon.code-workspace +++ b/cocoon.code-workspace @@ -2,7 +2,7 @@ "folders": [ { "path": "analyze" - }, + }, { "path": "app_dart" }, @@ -29,6 +29,9 @@ }, { "path": "tooling" + }, + { + "path": "conductor" } ], "settings": {} diff --git a/conductor/tracks.md b/conductor/tracks.md index b7bb98e53..6561e1709 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,3 +1,4 @@ # Tracks Registry --- +\n---\n\n- [ ] **Track: Implement get presubmit guard api**\n*Link: [./tracks/get_presubmit_guard_20260204/](./tracks/get_presubmit_guard_20260204/)* diff --git a/conductor/tracks/get_presubmit_guard_20260204/index.md b/conductor/tracks/get_presubmit_guard_20260204/index.md new file mode 100644 index 000000000..77b51d306 --- /dev/null +++ b/conductor/tracks/get_presubmit_guard_20260204/index.md @@ -0,0 +1,5 @@ +# Track get_presubmit_guard_20260204 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/tracks/get_presubmit_guard_20260204/metadata.json b/conductor/tracks/get_presubmit_guard_20260204/metadata.json new file mode 100644 index 000000000..96cc3d944 --- /dev/null +++ b/conductor/tracks/get_presubmit_guard_20260204/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "get_presubmit_guard_20260204", + "type": "feature", + "status": "new", + "created_at": "2026-02-04T12:00:00Z", + "updated_at": "2026-02-04T12:00:00Z", + "description": "Implement get presubmit guard api" +} diff --git a/conductor/tracks/get_presubmit_guard_20260204/plan.md b/conductor/tracks/get_presubmit_guard_20260204/plan.md new file mode 100644 index 000000000..fc8680841 --- /dev/null +++ b/conductor/tracks/get_presubmit_guard_20260204/plan.md @@ -0,0 +1,36 @@ +# Implementation Plan: Implement get presubmit guard api + +## Phase 1: Infrastructure & Data Model +Establish the data structures and service interfaces required to interact with Firestore for `PresubmitGuard` records. + +- [ ] Task: Define `PresubmitGuard` Model and DTOs + - [ ] Create/Update the Firestore model for `PresubmitGuard` if not already present in `app_dart/lib/src/model/firestore/`. + - [ ] Create a `GetPresubmitGuardResponse` DTO to match the specified API response format (pr_num, check_run_id, author, stages). +- [ ] Task: Create Firestore Service Method + - [ ] Implement a method in the Firestore service (e.g., `app_dart/lib/src/service/firestore.dart`) to query `PresubmitGuard` records by `slug` and `commit_sha`. +- [ ] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) + +## Phase 2: API Endpoint Implementation +Implement the request handler and wire it into the `app_dart` server. + +- [ ] Task: Implement `GetPresubmitGuard` Request Handler + - [ ] Create a new request handler class in `app_dart/lib/src/request_handlers/`. + - [ ] Implement logic to: + - Extract `slug` and `sha` from query parameters. + - Call the Firestore service to retrieve records. + - Consolidate records into the `GetPresubmitGuardResponse` format. + - Handle cases with no matching records (e.g., return 404 or empty response). +- [ ] Task: Register Endpoint in Server + - [ ] Add the new `/api/get-presubmit-guard` route to the `app_dart` server configuration (e.g., `app_dart/lib/src/server.dart` or equivalent). +- [ ] Task: Conductor - User Manual Verification 'Phase 2: API Endpoint Implementation' (Protocol in workflow.md) + +## Phase 3: Verification & Documentation +Ensure the feature is robust, tested, and documented. + +- [ ] Task: Write Unit Tests for Request Handler + - [ ] Implement tests in `app_dart/test/request_handlers/` using mocked Firestore service. + - [ ] Verify consolidation logic for multiple stages. + - [ ] Verify mapping of `pull_request_id` to `pr_num`. +- [ ] Task: Update API Documentation + - [ ] Document the new endpoint in the project's API docs or README. +- [ ] Task: Conductor - User Manual Verification 'Phase 3: Verification & Documentation' (Protocol in workflow.md) diff --git a/conductor/tracks/get_presubmit_guard_20260204/spec.md b/conductor/tracks/get_presubmit_guard_20260204/spec.md new file mode 100644 index 000000000..ee4acf2cd --- /dev/null +++ b/conductor/tracks/get_presubmit_guard_20260204/spec.md @@ -0,0 +1,40 @@ +# Specification: Implement get presubmit guard api + +## Overview +This track involves implementing a new backend API endpoint in the `app_dart` service to serve real-time presubmit check statuses (based on the `PresubmitGuard` entity) to the Cocoon dashboard. The dashboard needs to display the progress and results of various validation stages for active pull requests to provide developers with actionable visibility into their PR health. + +## User Stories +As a Flutter developer, I want to see the real-time status of my PR's presubmit checks on the Cocoon dashboard so that I can quickly identify and address failures without navigating through multiple GitHub or LUCI pages. + +## Functional Requirements +1. **New API Endpoint:** Create an authenticated GET endpoint in `app_dart` (e.g., `/api/get-presubmit-guard`). +2. **Input Parameters:** The endpoint must accept a `slug` (e.g., `flutter/flutter`) and a `commit_sha` as query parameters. +3. **Data Retrieval:** + - Query Cloud Firestore for all `PresubmitGuard` records matching the provided slug and commit SHA. + - Note: There is a separate `PresubmitGuard` record for every stage (e.g., one for `fusion`, one for `engine`). +4. **Response Format:** Return a single consolidated JSON object containing: + - `pr_num`: The GitHub Pull Request Number (mapped from `pull_request_id` in Firestore). + - `check_run_id`: The GitHub Check Run ID (shared across all stage records). + - `author`: The GitHub handle of the PR author (shared across all stage records). + - `stages`: A list of objects (one for each record found), each containing: + - `name`: The name of the stage (e.g., `fusion` or `engine`). + - `created_at`: The timestamp when the stage was created/started. + - `builds`: A map where keys are `check_name` and values are of type `TaskStatus`. + - **Sample `TaskStatus` values:** "Cancelled", "New", "In Progress", "Infra Failure", "Failed", "Succeeded", "Skipped". + +## Non-Functional Requirements +- **Performance:** The endpoint should respond within 200ms for typical queries. +- **Reliability:** Handle cases where Firestore records may be partially populated or missing due to webhook latency. Consolidate metadata from the first available record. +- **Security:** Ensure the endpoint is protected by existing authentication mechanisms used for dashboard APIs. + +## Acceptance Criteria +- [ ] A new GET endpoint exists in `app_dart` that accepts `slug` and `sha`. +- [ ] The endpoint correctly retrieves all matching `PresubmitGuard` records and consolidates them into a single response. +- [ ] The response includes the shared `pr_num`, `check_run_id`, `author`, and the list of individual stages with their build statuses. +- [ ] Unit tests in `app_dart` verify the consolidation logic with mocked Firestore data. +- [ ] The API documentation is updated to include this new endpoint. + +## Out of Scope +- Implementing the frontend UI in the `dashboard/` package. +- Modifying the webhook ingestion logic (assuming `PresubmitGuard` is already being populated correctly). +- Support for historical/archived presubmit data (focus is on real-time). From 0603d935efedfbc45fcaeb52547a4eb617afd317 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:05:21 -0800 Subject: [PATCH 02/16] feat(api): Implement get presubmit guard api Task: Implement get presubmit guard api Summary: Implemented a new API endpoint /api/get-presubmit-guard to serve consolidated presubmit guard statuses. Files: - app_dart/lib/server.dart: Registered the new endpoint. - app_dart/lib/src/request_handlers/get_presubmit_guard.dart: Implemented the request handler. - app_dart/lib/src/service/firestore.dart: Added queryPresubmitGuards method. - app_dart/test/request_handlers/get_presubmit_guard_test.dart: Added unit tests for the handler. - app_dart/test/service/presubmit_guard_query_test.dart: Added unit tests for the firestore query. - app_dart/test/src/utilities/entity_generators.dart: Added PresubmitGuard generator for tests. - packages/cocoon_common/lib/rpc_model.dart: Exported new DTO. - packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart: Defined response DTO. - packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart: Generated JSON serialization code. Why: Provides the dashboard with real-time visibility into presubmit checks. --- app_dart/lib/server.dart | 34 ++++++ .../request_handlers/get_presubmit_guard.dart | 63 +++++++++++ app_dart/lib/src/service/firestore.dart | 17 +++ .../get_presubmit_guard_test.dart | 105 ++++++++++++++++++ .../service/presubmit_guard_query_test.dart | 57 ++++++++++ .../test/src/utilities/entity_generators.dart | 27 +++++ packages/cocoon_common/lib/rpc_model.dart | 2 + .../lib/src/rpc_model/presubmit_guard.dart | 57 ++++++++++ .../lib/src/rpc_model/presubmit_guard.g.dart | 54 +++++++++ 9 files changed, 416 insertions(+) create mode 100644 app_dart/lib/src/request_handlers/get_presubmit_guard.dart create mode 100644 app_dart/test/request_handlers/get_presubmit_guard_test.dart create mode 100644 app_dart/test/service/presubmit_guard_query_test.dart create mode 100644 packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart create mode 100644 packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart index 2074486b8..fbf07fce8 100644 --- a/app_dart/lib/server.dart +++ b/app_dart/lib/server.dart @@ -7,6 +7,7 @@ import 'dart:math'; import 'cocoon_service.dart'; import 'src/request_handlers/get_engine_artifacts_ready.dart'; +import 'src/request_handlers/get_presubmit_guard.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'; @@ -155,6 +156,39 @@ Server createServer({ authenticationProvider: authProvider, firestore: firestore, ), + + /// Returns the presubmit guard status for a given slug and commit SHA. + /// + /// Consolidates multiple [PresubmitGuard] records (one per stage) into a single response. + /// + /// GET: /api/get-presubmit-guard + /// + /// Parameters: + /// slug: (string in query) required. The repository owner/name (e.g., 'flutter/flutter'). + /// sha: (string in query) required. The commit SHA to query for. + /// + /// Response: Status 200 OK + /// Returns [PresubmitGuardResponse]: + /// { + /// "pr_num": 123, + /// "check_run_id": 456, + /// "author": "dash", + /// "stages": [ + /// { + /// "name": "fusion", + /// "created_at": 123456789, + /// "builds": { + /// "test1": "Succeeded", + /// "test2": "In Progress" + /// } + /// } + /// ] + /// } + '/api/get-presubmit-guard': GetPresubmitGuard( + config: config, + authenticationProvider: authProvider, + firestore: firestore, + ), '/api/update-suppressed-test': UpdateSuppressedTest( authenticationProvider: authProvider, firestore: firestore, diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart new file mode 100644 index 000000000..2ae52cc73 --- /dev/null +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -0,0 +1,63 @@ +// 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 'dart:async'; + +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 '../request_handling/api_request_handler.dart'; + +@immutable +final class GetPresubmitGuard extends ApiRequestHandler { + const GetPresubmitGuard({ + required super.config, + required super.authenticationProvider, + required FirestoreService firestore, + }) : _firestore = firestore; + + final FirestoreService _firestore; + + static const String kSlugParam = 'slug'; + static const String kShaParam = 'sha'; + + @override + Future get(Request request) async { + checkRequiredQueryParameters(request, [kSlugParam, kShaParam]); + + final slugName = request.uri.queryParameters[kSlugParam]!; + final sha = request.uri.queryParameters[kShaParam]!; + + final slug = RepositorySlug.full(slugName); + final guards = await _firestore.queryPresubmitGuards( + slug: slug, + commitSha: sha, + ); + + if (guards.isEmpty) { + return Response.json(null); + } + + // Consolidate metadata from the first record. + final first = guards.first; + final response = rpc_model.PresubmitGuardResponse( + prNum: first.pullRequestId, + checkRunId: first.checkRunId, + author: first.author, + stages: [ + ...guards.map( + (g) => rpc_model.PresubmitGuardStage( + name: g.stage.name, + createdAt: g.creationTime, + builds: g.builds, + ), + ), + ], + ); + + return Response.json(response.toJson()); + } +} diff --git a/app_dart/lib/src/service/firestore.dart b/app_dart/lib/src/service/firestore.dart index 19e5faa08..5b0682c7c 100644 --- a/app_dart/lib/src/service/firestore.dart +++ b/app_dart/lib/src/service/firestore.dart @@ -16,6 +16,7 @@ import '../../cocoon_service.dart'; import '../model/firestore/commit.dart'; import '../model/firestore/github_build_status.dart'; import '../model/firestore/github_gold_status.dart'; +import '../model/firestore/presubmit_guard.dart'; import '../model/firestore/task.dart'; import 'firestore/commit_and_tasks.dart'; @@ -263,6 +264,22 @@ mixin FirestoreQueries { return tasks.isEmpty ? null : Task.fromDocument(tasks.first); } + /// Queries for [PresubmitGuard] records by [slug] and [commitSha]. + Future> queryPresubmitGuards({ + required RepositorySlug slug, + required String commitSha, + }) async { + final filterMap = { + '${PresubmitGuard.fieldSlug} =': slug.fullName, + '${PresubmitGuard.fieldCommitSha} =': commitSha, + }; + final documents = await query( + PresubmitGuard.collectionId, + filterMap, + ); + return [...documents.map(PresubmitGuard.fromDocument)]; + } + /// Queries the last updated build status for the [slug], [prNumber], and [head]. /// /// If not existing, returns a fresh new Build status. diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart new file mode 100644 index 000000000..806f13f33 --- /dev/null +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -0,0 +1,105 @@ +// 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 'dart:convert'; + +import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_server_test/test_logging.dart'; +import 'package:cocoon_service/src/model/firestore/base.dart'; +import 'package:cocoon_service/src/request_handlers/get_presubmit_guard.dart'; +import 'package:cocoon_service/src/request_handling/exceptions.dart'; +import 'package:github/github.dart'; +import 'package:test/test.dart'; + +import '../src/fake_config.dart'; +import '../src/request_handling/fake_dashboard_authentication.dart'; +import '../src/request_handling/fake_http.dart'; +import '../src/request_handling/request_handler_tester.dart'; +import '../src/service/fake_firestore_service.dart'; +import '../src/utilities/entity_generators.dart'; + +void main() { + useTestLoggerPerTest(); + + late RequestHandlerTester tester; + late GetPresubmitGuard handler; + late FakeFirestoreService firestore; + + Future decodeHandlerBody() async { + final body = await tester.get(handler); + return await utf8.decoder + .bind(body.body as Stream>) + .transform(json.decoder) + .single + as T?; + } + + setUp(() { + firestore = FakeFirestoreService(); + tester = RequestHandlerTester(); + handler = GetPresubmitGuard( + config: FakeConfig(), + authenticationProvider: FakeDashboardAuthentication(), + firestore: firestore, + ); + }); + + test('missing parameters', () async { + tester.request = FakeHttpRequest(); + expect(tester.get(handler), throwsA(isA())); + }); + + test('no guards found', () async { + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: 'abc', + }, + ); + + final result = await decodeHandlerBody(); + expect(result, isNull); + }); + + test('consolidates multiple stages', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const sha = 'abc'; + + final guard1 = generatePresubmitGuard( + slug: slug, + commitSha: sha, + stage: CiStage.fusionTests, + builds: {'test1': TaskStatus.succeeded}, + ); + final guard2 = generatePresubmitGuard( + slug: slug, + commitSha: sha, + stage: CiStage.fusionEngineBuild, + builds: {'engine1': TaskStatus.inProgress}, + ); + + firestore.putDocuments([guard1, guard2]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: sha, + }, + ); + + final result = (await decodeHandlerBody>())!; + expect(result['pr_num'], 123); + expect(result['author'], 'dash'); + expect(result['check_run_id'], 1); + + final stages = result['stages'] as List; + expect(stages.length, 2); + + final fusionStage = stages.firstWhere((s) => (s as Map)['name'] == 'fusion') as Map; + expect(fusionStage['builds'], {'test1': 'Succeeded'}); + + final engineStage = stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; + expect(engineStage['builds'], {'engine1': 'In Progress'}); + }); +} diff --git a/app_dart/test/service/presubmit_guard_query_test.dart b/app_dart/test/service/presubmit_guard_query_test.dart new file mode 100644 index 000000000..66644303b --- /dev/null +++ b/app_dart/test/service/presubmit_guard_query_test.dart @@ -0,0 +1,57 @@ +// 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:cocoon_service/src/model/firestore/base.dart'; +import 'package:cocoon_service/src/model/firestore/presubmit_guard.dart'; +import 'package:cocoon_service/src/service/firestore.dart'; +import 'package:github/github.dart'; +import 'package:test/test.dart'; + +import '../src/service/fake_firestore_service.dart'; +import '../src/utilities/entity_generators.dart'; + +void main() { + late FakeFirestoreService firestoreService; + + setUp(() { + firestoreService = FakeFirestoreService(); + }); + + test('queryPresubmitGuards returns matching guards', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const commitSha = 'abc'; + + final guard1 = generatePresubmitGuard( + slug: slug, + commitSha: commitSha, + stage: CiStage.fusionTests, + ); + final guard2 = generatePresubmitGuard( + slug: slug, + commitSha: commitSha, + stage: CiStage.fusionEngineBuild, + ); + final otherGuard = generatePresubmitGuard( + slug: slug, + pullRequestId: 456, + commitSha: 'def', + stage: CiStage.fusionTests, + ); + + firestoreService.putDocuments([ + guard1, + guard2, + otherGuard, + ]); + + final results = await firestoreService.queryPresubmitGuards( + slug: slug, + commitSha: commitSha, + ); + + expect(results.length, 2); + expect(results.any((g) => g.stage == CiStage.fusionTests), isTrue); + expect(results.any((g) => g.stage == CiStage.fusionEngineBuild), isTrue); + }); +} diff --git a/app_dart/test/src/utilities/entity_generators.dart b/app_dart/test/src/utilities/entity_generators.dart index 71a2d42ad..87d692bc1 100644 --- a/app_dart/test/src/utilities/entity_generators.dart +++ b/app_dart/test/src/utilities/entity_generators.dart @@ -7,7 +7,9 @@ import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_service/ci_yaml.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart'; import 'package:cocoon_service/src/model/firestore/github_build_status.dart'; +import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/firestore/github_gold_status.dart'; +import 'package:cocoon_service/src/model/firestore/presubmit_guard.dart'; import 'package:cocoon_service/src/model/firestore/suppressed_test.dart'; import 'package:cocoon_service/src/model/firestore/task.dart'; import 'package:cocoon_service/src/model/gerrit/commit.dart'; @@ -308,3 +310,28 @@ SuppressedTest generateSuppressedTest({ issueLink: issueLink, createTimestamp: createTimestamp ?? DateTime.fromMillisecondsSinceEpoch(1), )..name = '$kDatabase/documents/${SuppressedTest.kCollectionId}/$name'; + +PresubmitGuard generatePresubmitGuard({ + github.RepositorySlug? slug, + int pullRequestId = 123, + github.CheckRun? checkRun, + CiStage stage = CiStage.fusionTests, + String commitSha = 'abc', + int creationTime = 1, + String author = 'dash', + int buildCount = 1, + Map? builds, +}) { + return PresubmitGuard( + slug: slug ?? github.RepositorySlug('flutter', 'flutter'), + pullRequestId: pullRequestId, + checkRun: checkRun ?? generateCheckRun(1), + stage: stage, + commitSha: commitSha, + creationTime: creationTime, + author: author, + remainingBuilds: buildCount, + failedBuilds: 0, + builds: builds, + ); +} diff --git a/packages/cocoon_common/lib/rpc_model.dart b/packages/cocoon_common/lib/rpc_model.dart index 17d4128c0..7a6c43e27 100644 --- a/packages/cocoon_common/lib/rpc_model.dart +++ b/packages/cocoon_common/lib/rpc_model.dart @@ -34,6 +34,8 @@ export 'src/rpc_model/commit_status.dart' show CommitStatus; export 'src/rpc_model/content_hash_lookup.dart' show ContentHashLookup; export 'src/rpc_model/merge_group_hooks.dart' show MergeGroupHook, MergeGroupHooks; +export 'src/rpc_model/presubmit_guard.dart' + show PresubmitGuardResponse, PresubmitGuardStage; export 'src/rpc_model/suppressed_test.dart' show SuppressedTest, SuppressionUpdate; export 'src/rpc_model/task.dart' show Task; diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart new file mode 100644 index 000000000..98c3db2b4 --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart @@ -0,0 +1,57 @@ +// 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:cocoon_common/task_status.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'presubmit_guard.g.dart'; + +@immutable +@JsonSerializable() +final class PresubmitGuardResponse { + const PresubmitGuardResponse({ + required this.prNum, + required this.checkRunId, + required this.author, + required this.stages, + }); + + factory PresubmitGuardResponse.fromJson(Map json) => + _$PresubmitGuardResponseFromJson(json); + + @JsonKey(name: 'pr_num') + final int prNum; + + @JsonKey(name: 'check_run_id') + final int checkRunId; + + final String author; + + final List stages; + + Map toJson() => _$PresubmitGuardResponseToJson(this); +} + +@immutable +@JsonSerializable() +final class PresubmitGuardStage { + const PresubmitGuardStage({ + required this.name, + required this.createdAt, + required this.builds, + }); + + factory PresubmitGuardStage.fromJson(Map json) => + _$PresubmitGuardStageFromJson(json); + + final String name; + + @JsonKey(name: 'created_at') + final int createdAt; + + final Map builds; + + Map toJson() => _$PresubmitGuardStageToJson(this); +} diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart new file mode 100644 index 000000000..3d2f42aed --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'presubmit_guard.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PresubmitGuardResponse _$PresubmitGuardResponseFromJson( + Map json, +) => PresubmitGuardResponse( + prNum: (json['pr_num'] as num).toInt(), + checkRunId: (json['check_run_id'] as num).toInt(), + author: json['author'] as String, + stages: (json['stages'] as List) + .map((e) => PresubmitGuardStage.fromJson(e as Map)) + .toList(), +); + +Map _$PresubmitGuardResponseToJson( + PresubmitGuardResponse instance, +) => { + 'pr_num': instance.prNum, + 'check_run_id': instance.checkRunId, + 'author': instance.author, + 'stages': instance.stages, +}; + +PresubmitGuardStage _$PresubmitGuardStageFromJson(Map json) => + PresubmitGuardStage( + name: json['name'] as String, + createdAt: (json['created_at'] as num).toInt(), + builds: (json['builds'] as Map).map( + (k, e) => MapEntry(k, $enumDecode(_$TaskStatusEnumMap, e)), + ), + ); + +Map _$PresubmitGuardStageToJson( + PresubmitGuardStage instance, +) => { + 'name': instance.name, + 'created_at': instance.createdAt, + 'builds': instance.builds, +}; + +const _$TaskStatusEnumMap = { + TaskStatus.cancelled: 'cancelled', + TaskStatus.waitingForBackfill: 'waitingForBackfill', + TaskStatus.inProgress: 'inProgress', + TaskStatus.infraFailure: 'infraFailure', + TaskStatus.failed: 'failed', + TaskStatus.succeeded: 'succeeded', + TaskStatus.skipped: 'skipped', +}; From d90774172a8a5393e385da3470a190fe546adb34 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:05:36 -0800 Subject: [PATCH 03/16] chore(conductor): Mark track 'Implement get presubmit guard api' as complete --- conductor/tracks.md | 4 +- .../get_presubmit_guard_20260204/plan.md | 38 +++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/conductor/tracks.md b/conductor/tracks.md index 6561e1709..76b1b5670 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,4 +1,6 @@ # Tracks Registry --- -\n---\n\n- [ ] **Track: Implement get presubmit guard api**\n*Link: [./tracks/get_presubmit_guard_20260204/](./tracks/get_presubmit_guard_20260204/)* + +- [x] **Track: Implement get presubmit guard api** +*Link: [./tracks/get_presubmit_guard_20260204/](./tracks/get_presubmit_guard_20260204/)* diff --git a/conductor/tracks/get_presubmit_guard_20260204/plan.md b/conductor/tracks/get_presubmit_guard_20260204/plan.md index fc8680841..aef4c8d86 100644 --- a/conductor/tracks/get_presubmit_guard_20260204/plan.md +++ b/conductor/tracks/get_presubmit_guard_20260204/plan.md @@ -3,34 +3,34 @@ ## Phase 1: Infrastructure & Data Model Establish the data structures and service interfaces required to interact with Firestore for `PresubmitGuard` records. -- [ ] Task: Define `PresubmitGuard` Model and DTOs - - [ ] Create/Update the Firestore model for `PresubmitGuard` if not already present in `app_dart/lib/src/model/firestore/`. - - [ ] Create a `GetPresubmitGuardResponse` DTO to match the specified API response format (pr_num, check_run_id, author, stages). -- [ ] Task: Create Firestore Service Method - - [ ] Implement a method in the Firestore service (e.g., `app_dart/lib/src/service/firestore.dart`) to query `PresubmitGuard` records by `slug` and `commit_sha`. -- [ ] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) +- [x] Task: Define `PresubmitGuard` Model and DTOs [checkpoint: 8f78382] + - [x] Create/Update the Firestore model for `PresubmitGuard` if not already present in `app_dart/lib/src/model/firestore/`. + - [x] Create a `GetPresubmitGuardResponse` DTO to match the specified API response format (pr_num, check_run_id, author, stages). +- [x] Task: Create Firestore Service Method [checkpoint: 8f78382] + - [x] Implement a method in the Firestore service (e.g., `app_dart/lib/src/service/firestore.dart`) to query `PresubmitGuard` records by `slug` and `commit_sha`. +- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) ## Phase 2: API Endpoint Implementation Implement the request handler and wire it into the `app_dart` server. -- [ ] Task: Implement `GetPresubmitGuard` Request Handler - - [ ] Create a new request handler class in `app_dart/lib/src/request_handlers/`. - - [ ] Implement logic to: +- [x] Task: Implement `GetPresubmitGuard` Request Handler [checkpoint: 8f78382] + - [x] Create a new request handler class in `app_dart/lib/src/request_handlers/`. + - [x] Implement logic to: - Extract `slug` and `sha` from query parameters. - Call the Firestore service to retrieve records. - Consolidate records into the `GetPresubmitGuardResponse` format. - Handle cases with no matching records (e.g., return 404 or empty response). -- [ ] Task: Register Endpoint in Server - - [ ] Add the new `/api/get-presubmit-guard` route to the `app_dart` server configuration (e.g., `app_dart/lib/src/server.dart` or equivalent). -- [ ] Task: Conductor - User Manual Verification 'Phase 2: API Endpoint Implementation' (Protocol in workflow.md) +- [x] Task: Register Endpoint in Server [checkpoint: 8f78382] + - [x] Add the new `/api/get-presubmit-guard` route to the `app_dart` server configuration (e.g., `app_dart/lib/src/server.dart` or equivalent). +- [x] Task: Conductor - User Manual Verification 'Phase 2: API Endpoint Implementation' (Protocol in workflow.md) ## Phase 3: Verification & Documentation Ensure the feature is robust, tested, and documented. -- [ ] Task: Write Unit Tests for Request Handler - - [ ] Implement tests in `app_dart/test/request_handlers/` using mocked Firestore service. - - [ ] Verify consolidation logic for multiple stages. - - [ ] Verify mapping of `pull_request_id` to `pr_num`. -- [ ] Task: Update API Documentation - - [ ] Document the new endpoint in the project's API docs or README. -- [ ] Task: Conductor - User Manual Verification 'Phase 3: Verification & Documentation' (Protocol in workflow.md) +- [x] Task: Write Unit Tests for Request Handler [checkpoint: 8f78382] + - [x] Implement tests in `app_dart/test/request_handlers/` using mocked Firestore service. + - [x] Verify consolidation logic for multiple stages. + - [x] Verify mapping of `pull_request_id` to `pr_num`. +- [x] Task: Update API Documentation [checkpoint: 8f78382] + - [x] Document the new endpoint in the project's API docs or README. +- [x] Task: Conductor - User Manual Verification 'Phase 3: Verification & Documentation' (Protocol in workflow.md) From 172c15534669f8cf0dbed349707e1d60580f9884 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:07:07 -0800 Subject: [PATCH 04/16] docs(conductor): Synchronize docs for track 'Implement get presubmit guard api' --- conductor/product.md | 1 + 1 file changed, 1 insertion(+) diff --git a/conductor/product.md b/conductor/product.md index b02b3a91a..2e6d62392 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -18,6 +18,7 @@ Cocoon is the CI coordination and orchestration system for the Flutter project. * **CI Orchestration:** Automates the scheduling and tracking of LUCI builds for the Flutter framework and engine. * **Tree Status Dashboard:** A Flutter-based web application that provides a visual overview of build health across various commits and branches. * **Merge Queue Visibility:** APIs for querying and inspecting recent GitHub Merge Queue webhook events to diagnose integration issues. +* **Presubmit Visibility:** Backend APIs to provide real-time status of presubmit checks (Presubmit Guard) to the dashboard for active pull requests. * **Auto-submit Bot:** Handles automated pull request management, including label-based merges, reverts, and validation checks. * **GitHub Integration:** Robust handling of GitHub webhooks to sync commits, manage check runs, and report build statuses back to PRs. * **Unified Data Store:** Leverages Cloud Firestore and BigQuery for tracking build history, metrics, and CI health trends. From 76638c92e833c3aa82879981a74b664d0bfc502f Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:43:44 -0800 Subject: [PATCH 05/16] chore(conductor): Archive track 'Implement get presubmit guard api' --- .../get_presubmit_guard_20260204/index.md | 5 +++ .../metadata.json | 8 ++++ .../get_presubmit_guard_20260204/plan.md | 36 +++++++++++++++++ .../get_presubmit_guard_20260204/spec.md | 40 +++++++++++++++++++ conductor/tracks.md | 3 -- 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 conductor/archive/get_presubmit_guard_20260204/index.md create mode 100644 conductor/archive/get_presubmit_guard_20260204/metadata.json create mode 100644 conductor/archive/get_presubmit_guard_20260204/plan.md create mode 100644 conductor/archive/get_presubmit_guard_20260204/spec.md diff --git a/conductor/archive/get_presubmit_guard_20260204/index.md b/conductor/archive/get_presubmit_guard_20260204/index.md new file mode 100644 index 000000000..77b51d306 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_20260204/index.md @@ -0,0 +1,5 @@ +# Track get_presubmit_guard_20260204 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/get_presubmit_guard_20260204/metadata.json b/conductor/archive/get_presubmit_guard_20260204/metadata.json new file mode 100644 index 000000000..96cc3d944 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_20260204/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "get_presubmit_guard_20260204", + "type": "feature", + "status": "new", + "created_at": "2026-02-04T12:00:00Z", + "updated_at": "2026-02-04T12:00:00Z", + "description": "Implement get presubmit guard api" +} diff --git a/conductor/archive/get_presubmit_guard_20260204/plan.md b/conductor/archive/get_presubmit_guard_20260204/plan.md new file mode 100644 index 000000000..aef4c8d86 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_20260204/plan.md @@ -0,0 +1,36 @@ +# Implementation Plan: Implement get presubmit guard api + +## Phase 1: Infrastructure & Data Model +Establish the data structures and service interfaces required to interact with Firestore for `PresubmitGuard` records. + +- [x] Task: Define `PresubmitGuard` Model and DTOs [checkpoint: 8f78382] + - [x] Create/Update the Firestore model for `PresubmitGuard` if not already present in `app_dart/lib/src/model/firestore/`. + - [x] Create a `GetPresubmitGuardResponse` DTO to match the specified API response format (pr_num, check_run_id, author, stages). +- [x] Task: Create Firestore Service Method [checkpoint: 8f78382] + - [x] Implement a method in the Firestore service (e.g., `app_dart/lib/src/service/firestore.dart`) to query `PresubmitGuard` records by `slug` and `commit_sha`. +- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) + +## Phase 2: API Endpoint Implementation +Implement the request handler and wire it into the `app_dart` server. + +- [x] Task: Implement `GetPresubmitGuard` Request Handler [checkpoint: 8f78382] + - [x] Create a new request handler class in `app_dart/lib/src/request_handlers/`. + - [x] Implement logic to: + - Extract `slug` and `sha` from query parameters. + - Call the Firestore service to retrieve records. + - Consolidate records into the `GetPresubmitGuardResponse` format. + - Handle cases with no matching records (e.g., return 404 or empty response). +- [x] Task: Register Endpoint in Server [checkpoint: 8f78382] + - [x] Add the new `/api/get-presubmit-guard` route to the `app_dart` server configuration (e.g., `app_dart/lib/src/server.dart` or equivalent). +- [x] Task: Conductor - User Manual Verification 'Phase 2: API Endpoint Implementation' (Protocol in workflow.md) + +## Phase 3: Verification & Documentation +Ensure the feature is robust, tested, and documented. + +- [x] Task: Write Unit Tests for Request Handler [checkpoint: 8f78382] + - [x] Implement tests in `app_dart/test/request_handlers/` using mocked Firestore service. + - [x] Verify consolidation logic for multiple stages. + - [x] Verify mapping of `pull_request_id` to `pr_num`. +- [x] Task: Update API Documentation [checkpoint: 8f78382] + - [x] Document the new endpoint in the project's API docs or README. +- [x] Task: Conductor - User Manual Verification 'Phase 3: Verification & Documentation' (Protocol in workflow.md) diff --git a/conductor/archive/get_presubmit_guard_20260204/spec.md b/conductor/archive/get_presubmit_guard_20260204/spec.md new file mode 100644 index 000000000..ee4acf2cd --- /dev/null +++ b/conductor/archive/get_presubmit_guard_20260204/spec.md @@ -0,0 +1,40 @@ +# Specification: Implement get presubmit guard api + +## Overview +This track involves implementing a new backend API endpoint in the `app_dart` service to serve real-time presubmit check statuses (based on the `PresubmitGuard` entity) to the Cocoon dashboard. The dashboard needs to display the progress and results of various validation stages for active pull requests to provide developers with actionable visibility into their PR health. + +## User Stories +As a Flutter developer, I want to see the real-time status of my PR's presubmit checks on the Cocoon dashboard so that I can quickly identify and address failures without navigating through multiple GitHub or LUCI pages. + +## Functional Requirements +1. **New API Endpoint:** Create an authenticated GET endpoint in `app_dart` (e.g., `/api/get-presubmit-guard`). +2. **Input Parameters:** The endpoint must accept a `slug` (e.g., `flutter/flutter`) and a `commit_sha` as query parameters. +3. **Data Retrieval:** + - Query Cloud Firestore for all `PresubmitGuard` records matching the provided slug and commit SHA. + - Note: There is a separate `PresubmitGuard` record for every stage (e.g., one for `fusion`, one for `engine`). +4. **Response Format:** Return a single consolidated JSON object containing: + - `pr_num`: The GitHub Pull Request Number (mapped from `pull_request_id` in Firestore). + - `check_run_id`: The GitHub Check Run ID (shared across all stage records). + - `author`: The GitHub handle of the PR author (shared across all stage records). + - `stages`: A list of objects (one for each record found), each containing: + - `name`: The name of the stage (e.g., `fusion` or `engine`). + - `created_at`: The timestamp when the stage was created/started. + - `builds`: A map where keys are `check_name` and values are of type `TaskStatus`. + - **Sample `TaskStatus` values:** "Cancelled", "New", "In Progress", "Infra Failure", "Failed", "Succeeded", "Skipped". + +## Non-Functional Requirements +- **Performance:** The endpoint should respond within 200ms for typical queries. +- **Reliability:** Handle cases where Firestore records may be partially populated or missing due to webhook latency. Consolidate metadata from the first available record. +- **Security:** Ensure the endpoint is protected by existing authentication mechanisms used for dashboard APIs. + +## Acceptance Criteria +- [ ] A new GET endpoint exists in `app_dart` that accepts `slug` and `sha`. +- [ ] The endpoint correctly retrieves all matching `PresubmitGuard` records and consolidates them into a single response. +- [ ] The response includes the shared `pr_num`, `check_run_id`, `author`, and the list of individual stages with their build statuses. +- [ ] Unit tests in `app_dart` verify the consolidation logic with mocked Firestore data. +- [ ] The API documentation is updated to include this new endpoint. + +## Out of Scope +- Implementing the frontend UI in the `dashboard/` package. +- Modifying the webhook ingestion logic (assuming `PresubmitGuard` is already being populated correctly). +- Support for historical/archived presubmit data (focus is on real-time). diff --git a/conductor/tracks.md b/conductor/tracks.md index 76b1b5670..b7bb98e53 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,6 +1,3 @@ # Tracks Registry --- - -- [x] **Track: Implement get presubmit guard api** -*Link: [./tracks/get_presubmit_guard_20260204/](./tracks/get_presubmit_guard_20260204/)* From e53e759477b8178e40a7db1d2fdc12dfeb34e31c Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:44:50 -0800 Subject: [PATCH 06/16] added guard status --- .../request_handlers/get_presubmit_guard.dart | 14 ++++ .../get_presubmit_guard_test.dart | 81 +++++++++++++++++++ .../service/presubmit_guard_query_test.dart | 10 +++ .../get_presubmit_guard_20260204/index.md | 5 -- .../metadata.json | 8 -- .../get_presubmit_guard_20260204/plan.md | 36 --------- .../get_presubmit_guard_20260204/spec.md | 40 --------- packages/cocoon_common/lib/guard_status.dart | 42 ++++++++++ .../lib/src/rpc_model/presubmit_guard.dart | 5 ++ .../lib/src/rpc_model/presubmit_guard.g.dart | 9 +++ 10 files changed, 161 insertions(+), 89 deletions(-) delete mode 100644 conductor/tracks/get_presubmit_guard_20260204/index.md delete mode 100644 conductor/tracks/get_presubmit_guard_20260204/metadata.json delete mode 100644 conductor/tracks/get_presubmit_guard_20260204/plan.md delete mode 100644 conductor/tracks/get_presubmit_guard_20260204/spec.md create mode 100644 packages/cocoon_common/lib/guard_status.dart diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index 2ae52cc73..5ca4e1878 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -4,6 +4,7 @@ import 'dart:async'; +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'; @@ -43,10 +44,23 @@ 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 response = rpc_model.PresubmitGuardResponse( prNum: first.pullRequestId, checkRunId: first.checkRunId, author: first.author, + guardStatus: guardStatus, stages: [ ...guards.map( (g) => rpc_model.PresubmitGuardStage( diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index 806f13f33..ed5d7e148 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -72,12 +72,17 @@ void main() { stage: CiStage.fusionTests, builds: {'test1': TaskStatus.succeeded}, ); + guard1.failedBuilds = 0; + guard1.remainingBuilds = 0; + final guard2 = generatePresubmitGuard( slug: slug, commitSha: sha, stage: CiStage.fusionEngineBuild, builds: {'engine1': TaskStatus.inProgress}, ); + guard2.failedBuilds = 0; + guard2.remainingBuilds = 1; firestore.putDocuments([guard1, guard2]); @@ -92,6 +97,7 @@ void main() { expect(result['pr_num'], 123); expect(result['author'], 'dash'); expect(result['check_run_id'], 1); + expect(result['guard_status'], 'In Progress'); final stages = result['stages'] as List; expect(stages.length, 2); @@ -102,4 +108,79 @@ void main() { final engineStage = stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; expect(engineStage['builds'], {'engine1': 'In Progress'}); }); + + test('guardStatus is Failed if any stage has failed builds', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const sha = 'abc'; + + final guard = generatePresubmitGuard( + slug: slug, + commitSha: sha, + builds: {'test1': TaskStatus.failed}, + ); + guard.failedBuilds = 1; + guard.remainingBuilds = 0; + + firestore.putDocuments([guard]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: sha, + }, + ); + + final result = (await decodeHandlerBody>())!; + expect(result['guard_status'], 'Failed'); + }); + + test('guardStatus is Succeeded if all stages are complete without failures', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const sha = 'abc'; + + final guard = generatePresubmitGuard( + slug: slug, + commitSha: sha, + builds: {'test1': TaskStatus.succeeded}, + ); + guard.failedBuilds = 0; + guard.remainingBuilds = 0; + + firestore.putDocuments([guard]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: sha, + }, + ); + + final result = (await decodeHandlerBody>())!; + expect(result['guard_status'], 'Succeeded'); + }); + + test('guardStatus is New if all stages are waiting for backfill', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const sha = 'abc'; + + final guard = generatePresubmitGuard( + slug: slug, + commitSha: sha, + builds: {'test1': TaskStatus.waitingForBackfill}, + ); + guard.failedBuilds = 0; + guard.remainingBuilds = 1; + + firestore.putDocuments([guard]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: sha, + }, + ); + + final result = (await decodeHandlerBody>())!; + expect(result['guard_status'], 'New'); + }); } diff --git a/app_dart/test/service/presubmit_guard_query_test.dart b/app_dart/test/service/presubmit_guard_query_test.dart index 66644303b..48f77e74b 100644 --- a/app_dart/test/service/presubmit_guard_query_test.dart +++ b/app_dart/test/service/presubmit_guard_query_test.dart @@ -54,4 +54,14 @@ void main() { expect(results.any((g) => g.stage == CiStage.fusionTests), isTrue); expect(results.any((g) => g.stage == CiStage.fusionEngineBuild), isTrue); }); + + test('queryPresubmitGuards returns empty list when no guards found', () async { + final slug = RepositorySlug('flutter', 'flutter'); + final results = await firestoreService.queryPresubmitGuards( + slug: slug, + commitSha: 'non-existent', + ); + + expect(results, isEmpty); + }); } diff --git a/conductor/tracks/get_presubmit_guard_20260204/index.md b/conductor/tracks/get_presubmit_guard_20260204/index.md deleted file mode 100644 index 77b51d306..000000000 --- a/conductor/tracks/get_presubmit_guard_20260204/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Track get_presubmit_guard_20260204 Context - -- [Specification](./spec.md) -- [Implementation Plan](./plan.md) -- [Metadata](./metadata.json) diff --git a/conductor/tracks/get_presubmit_guard_20260204/metadata.json b/conductor/tracks/get_presubmit_guard_20260204/metadata.json deleted file mode 100644 index 96cc3d944..000000000 --- a/conductor/tracks/get_presubmit_guard_20260204/metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "track_id": "get_presubmit_guard_20260204", - "type": "feature", - "status": "new", - "created_at": "2026-02-04T12:00:00Z", - "updated_at": "2026-02-04T12:00:00Z", - "description": "Implement get presubmit guard api" -} diff --git a/conductor/tracks/get_presubmit_guard_20260204/plan.md b/conductor/tracks/get_presubmit_guard_20260204/plan.md deleted file mode 100644 index aef4c8d86..000000000 --- a/conductor/tracks/get_presubmit_guard_20260204/plan.md +++ /dev/null @@ -1,36 +0,0 @@ -# Implementation Plan: Implement get presubmit guard api - -## Phase 1: Infrastructure & Data Model -Establish the data structures and service interfaces required to interact with Firestore for `PresubmitGuard` records. - -- [x] Task: Define `PresubmitGuard` Model and DTOs [checkpoint: 8f78382] - - [x] Create/Update the Firestore model for `PresubmitGuard` if not already present in `app_dart/lib/src/model/firestore/`. - - [x] Create a `GetPresubmitGuardResponse` DTO to match the specified API response format (pr_num, check_run_id, author, stages). -- [x] Task: Create Firestore Service Method [checkpoint: 8f78382] - - [x] Implement a method in the Firestore service (e.g., `app_dart/lib/src/service/firestore.dart`) to query `PresubmitGuard` records by `slug` and `commit_sha`. -- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) - -## Phase 2: API Endpoint Implementation -Implement the request handler and wire it into the `app_dart` server. - -- [x] Task: Implement `GetPresubmitGuard` Request Handler [checkpoint: 8f78382] - - [x] Create a new request handler class in `app_dart/lib/src/request_handlers/`. - - [x] Implement logic to: - - Extract `slug` and `sha` from query parameters. - - Call the Firestore service to retrieve records. - - Consolidate records into the `GetPresubmitGuardResponse` format. - - Handle cases with no matching records (e.g., return 404 or empty response). -- [x] Task: Register Endpoint in Server [checkpoint: 8f78382] - - [x] Add the new `/api/get-presubmit-guard` route to the `app_dart` server configuration (e.g., `app_dart/lib/src/server.dart` or equivalent). -- [x] Task: Conductor - User Manual Verification 'Phase 2: API Endpoint Implementation' (Protocol in workflow.md) - -## Phase 3: Verification & Documentation -Ensure the feature is robust, tested, and documented. - -- [x] Task: Write Unit Tests for Request Handler [checkpoint: 8f78382] - - [x] Implement tests in `app_dart/test/request_handlers/` using mocked Firestore service. - - [x] Verify consolidation logic for multiple stages. - - [x] Verify mapping of `pull_request_id` to `pr_num`. -- [x] Task: Update API Documentation [checkpoint: 8f78382] - - [x] Document the new endpoint in the project's API docs or README. -- [x] Task: Conductor - User Manual Verification 'Phase 3: Verification & Documentation' (Protocol in workflow.md) diff --git a/conductor/tracks/get_presubmit_guard_20260204/spec.md b/conductor/tracks/get_presubmit_guard_20260204/spec.md deleted file mode 100644 index ee4acf2cd..000000000 --- a/conductor/tracks/get_presubmit_guard_20260204/spec.md +++ /dev/null @@ -1,40 +0,0 @@ -# Specification: Implement get presubmit guard api - -## Overview -This track involves implementing a new backend API endpoint in the `app_dart` service to serve real-time presubmit check statuses (based on the `PresubmitGuard` entity) to the Cocoon dashboard. The dashboard needs to display the progress and results of various validation stages for active pull requests to provide developers with actionable visibility into their PR health. - -## User Stories -As a Flutter developer, I want to see the real-time status of my PR's presubmit checks on the Cocoon dashboard so that I can quickly identify and address failures without navigating through multiple GitHub or LUCI pages. - -## Functional Requirements -1. **New API Endpoint:** Create an authenticated GET endpoint in `app_dart` (e.g., `/api/get-presubmit-guard`). -2. **Input Parameters:** The endpoint must accept a `slug` (e.g., `flutter/flutter`) and a `commit_sha` as query parameters. -3. **Data Retrieval:** - - Query Cloud Firestore for all `PresubmitGuard` records matching the provided slug and commit SHA. - - Note: There is a separate `PresubmitGuard` record for every stage (e.g., one for `fusion`, one for `engine`). -4. **Response Format:** Return a single consolidated JSON object containing: - - `pr_num`: The GitHub Pull Request Number (mapped from `pull_request_id` in Firestore). - - `check_run_id`: The GitHub Check Run ID (shared across all stage records). - - `author`: The GitHub handle of the PR author (shared across all stage records). - - `stages`: A list of objects (one for each record found), each containing: - - `name`: The name of the stage (e.g., `fusion` or `engine`). - - `created_at`: The timestamp when the stage was created/started. - - `builds`: A map where keys are `check_name` and values are of type `TaskStatus`. - - **Sample `TaskStatus` values:** "Cancelled", "New", "In Progress", "Infra Failure", "Failed", "Succeeded", "Skipped". - -## Non-Functional Requirements -- **Performance:** The endpoint should respond within 200ms for typical queries. -- **Reliability:** Handle cases where Firestore records may be partially populated or missing due to webhook latency. Consolidate metadata from the first available record. -- **Security:** Ensure the endpoint is protected by existing authentication mechanisms used for dashboard APIs. - -## Acceptance Criteria -- [ ] A new GET endpoint exists in `app_dart` that accepts `slug` and `sha`. -- [ ] The endpoint correctly retrieves all matching `PresubmitGuard` records and consolidates them into a single response. -- [ ] The response includes the shared `pr_num`, `check_run_id`, `author`, and the list of individual stages with their build statuses. -- [ ] Unit tests in `app_dart` verify the consolidation logic with mocked Firestore data. -- [ ] The API documentation is updated to include this new endpoint. - -## Out of Scope -- Implementing the frontend UI in the `dashboard/` package. -- Modifying the webhook ingestion logic (assuming `PresubmitGuard` is already being populated correctly). -- Support for historical/archived presubmit data (focus is on real-time). diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart new file mode 100644 index 000000000..00ba29778 --- /dev/null +++ b/packages/cocoon_common/lib/guard_status.dart @@ -0,0 +1,42 @@ +// 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:collection/collection.dart'; + +/// Represents different states of a presubmit guard. +enum GuardStatus { + /// The guard is waiting for backfill. + waitingForBackfill('New'), + + /// The guard is in progress. + inProgress('In Progress'), + + /// The guard has failed. + failed('Failed'), + + /// The guard ran successfully. + succeeded('Succeeded'); + + const GuardStatus(this._schemaValue); + final String _schemaValue; + + /// Returns the status represented by the provided [value]. + factory GuardStatus.from(String value) { + return tryFrom(value) ?? (throw ArgumentError.value(value, 'value')); + } + + /// Returns the guard status represented by the provided [value]. + static GuardStatus? tryFrom(String value) { + return values.firstWhereOrNull((v) => v.value == value); + } + + /// The canonical string value representing `this`. + String get value => _schemaValue; + + /// Returns the JSON representation of `this`. + Object? toJson() => _schemaValue; + + @override + String toString() => _schemaValue; +} diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart index 98c3db2b4..d67c6c4a2 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart @@ -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_common/guard_status.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -16,6 +17,7 @@ final class PresubmitGuardResponse { required this.checkRunId, required this.author, required this.stages, + required this.guardStatus, }); factory PresubmitGuardResponse.fromJson(Map json) => @@ -31,6 +33,9 @@ final class PresubmitGuardResponse { final List stages; + @JsonKey(name: 'guard_status') + final GuardStatus guardStatus; + Map toJson() => _$PresubmitGuardResponseToJson(this); } diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart index 3d2f42aed..85c7deca9 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart @@ -15,6 +15,7 @@ PresubmitGuardResponse _$PresubmitGuardResponseFromJson( stages: (json['stages'] as List) .map((e) => PresubmitGuardStage.fromJson(e as Map)) .toList(), + guardStatus: $enumDecode(_$GuardStatusEnumMap, json['guard_status']), ); Map _$PresubmitGuardResponseToJson( @@ -24,6 +25,14 @@ Map _$PresubmitGuardResponseToJson( 'check_run_id': instance.checkRunId, 'author': instance.author, 'stages': instance.stages, + 'guard_status': instance.guardStatus, +}; + +const _$GuardStatusEnumMap = { + GuardStatus.waitingForBackfill: 'waitingForBackfill', + GuardStatus.inProgress: 'inProgress', + GuardStatus.failed: 'failed', + GuardStatus.succeeded: 'succeeded', }; PresubmitGuardStage _$PresubmitGuardStageFromJson(Map json) => From bc778deb6fd31f299f1337c8233eb81b238081e9 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 15:52:56 -0800 Subject: [PATCH 07/16] year fix --- app_dart/lib/src/request_handlers/get_presubmit_guard.dart | 2 +- .../test/request_handlers/get_presubmit_guard_test.dart | 2 +- packages/cocoon_common/lib/guard_status.dart | 2 +- .../cocoon_common/lib/src/rpc_model/presubmit_guard.dart | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index 5ca4e1878..515fe18bc 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -1,4 +1,4 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// 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. diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index ed5d7e148..a59a684eb 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -1,4 +1,4 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// 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. diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart index 00ba29778..63758cb2c 100644 --- a/packages/cocoon_common/lib/guard_status.dart +++ b/packages/cocoon_common/lib/guard_status.dart @@ -1,4 +1,4 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// 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. diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart index d67c6c4a2..ca486311f 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart @@ -1,12 +1,13 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// 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:cocoon_common/guard_status.dart'; -import 'package:cocoon_common/task_status.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; +import '../../guard_status.dart'; +import '../../task_status.dart'; + part 'presubmit_guard.g.dart'; @immutable From 38ac290f262988744cd79199985c277615b6bf49 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 16:00:40 -0800 Subject: [PATCH 08/16] code health --- .../get_presubmit_guard_test.dart | 12 +++---- .../service/presubmit_guard_query_test.dart | 32 +++++++++---------- .../test/src/utilities/entity_generators.dart | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index a59a684eb..d47a126b9 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -65,7 +65,7 @@ void main() { test('consolidates multiple stages', () async { final slug = RepositorySlug('flutter', 'flutter'); const sha = 'abc'; - + final guard1 = generatePresubmitGuard( slug: slug, commitSha: sha, @@ -98,13 +98,13 @@ void main() { expect(result['author'], 'dash'); expect(result['check_run_id'], 1); expect(result['guard_status'], 'In Progress'); - + final stages = result['stages'] as List; expect(stages.length, 2); - + final fusionStage = stages.firstWhere((s) => (s as Map)['name'] == 'fusion') as Map; expect(fusionStage['builds'], {'test1': 'Succeeded'}); - + final engineStage = stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; expect(engineStage['builds'], {'engine1': 'In Progress'}); }); @@ -137,7 +137,7 @@ void main() { test('guardStatus is Succeeded if all stages are complete without failures', () async { final slug = RepositorySlug('flutter', 'flutter'); const sha = 'abc'; - + final guard = generatePresubmitGuard( slug: slug, commitSha: sha, @@ -162,7 +162,7 @@ void main() { test('guardStatus is New if all stages are waiting for backfill', () async { final slug = RepositorySlug('flutter', 'flutter'); const sha = 'abc'; - + final guard = generatePresubmitGuard( slug: slug, commitSha: sha, diff --git a/app_dart/test/service/presubmit_guard_query_test.dart b/app_dart/test/service/presubmit_guard_query_test.dart index 48f77e74b..d87a0c9fb 100644 --- a/app_dart/test/service/presubmit_guard_query_test.dart +++ b/app_dart/test/service/presubmit_guard_query_test.dart @@ -2,9 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; -import 'package:cocoon_service/src/model/firestore/presubmit_guard.dart'; -import 'package:cocoon_service/src/service/firestore.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; @@ -12,6 +11,8 @@ import '../src/service/fake_firestore_service.dart'; import '../src/utilities/entity_generators.dart'; void main() { + useTestLoggerPerTest(); + late FakeFirestoreService firestoreService; setUp(() { @@ -21,7 +22,7 @@ void main() { test('queryPresubmitGuards returns matching guards', () async { final slug = RepositorySlug('flutter', 'flutter'); const commitSha = 'abc'; - + final guard1 = generatePresubmitGuard( slug: slug, commitSha: commitSha, @@ -39,11 +40,7 @@ void main() { stage: CiStage.fusionTests, ); - firestoreService.putDocuments([ - guard1, - guard2, - otherGuard, - ]); + firestoreService.putDocuments([guard1, guard2, otherGuard]); final results = await firestoreService.queryPresubmitGuards( slug: slug, @@ -55,13 +52,16 @@ void main() { expect(results.any((g) => g.stage == CiStage.fusionEngineBuild), isTrue); }); - test('queryPresubmitGuards returns empty list when no guards found', () async { - final slug = RepositorySlug('flutter', 'flutter'); - final results = await firestoreService.queryPresubmitGuards( - slug: slug, - commitSha: 'non-existent', - ); + test( + 'queryPresubmitGuards returns empty list when no guards found', + () async { + final slug = RepositorySlug('flutter', 'flutter'); + final results = await firestoreService.queryPresubmitGuards( + slug: slug, + commitSha: 'non-existent', + ); - expect(results, isEmpty); - }); + expect(results, isEmpty); + }, + ); } diff --git a/app_dart/test/src/utilities/entity_generators.dart b/app_dart/test/src/utilities/entity_generators.dart index 87d692bc1..db320f0ac 100644 --- a/app_dart/test/src/utilities/entity_generators.dart +++ b/app_dart/test/src/utilities/entity_generators.dart @@ -5,9 +5,9 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_service/ci_yaml.dart'; +import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart'; import 'package:cocoon_service/src/model/firestore/github_build_status.dart'; -import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/firestore/github_gold_status.dart'; import 'package:cocoon_service/src/model/firestore/presubmit_guard.dart'; import 'package:cocoon_service/src/model/firestore/suppressed_test.dart'; From 98bd7c98d9e4660c6d54d1775d2836a0284bd291 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 16:02:28 -0800 Subject: [PATCH 09/16] trailing whitespace --- app_dart/test/request_handlers/get_presubmit_guard_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index d47a126b9..b04abf55d 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -112,7 +112,7 @@ void main() { test('guardStatus is Failed if any stage has failed builds', () async { final slug = RepositorySlug('flutter', 'flutter'); const sha = 'abc'; - + final guard = generatePresubmitGuard( slug: slug, commitSha: sha, From 34a263ba4dc765535a91b6182fb2776679739ff9 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 16:30:49 -0800 Subject: [PATCH 10/16] refactoring --- .../request_handlers/get_presubmit_guard.dart | 4 +++- app_dart/lib/src/service/firestore.dart | 18 ++---------------- .../service/firestore/unified_check_run.dart | 17 +++++++++++++++-- .../service/presubmit_guard_query_test.dart | 7 +++++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index 515fe18bc..a523bf083 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import '../../cocoon_service.dart'; import '../request_handling/api_request_handler.dart'; +import '../service/firestore/unified_check_run.dart'; @immutable final class GetPresubmitGuard extends ApiRequestHandler { @@ -33,7 +34,8 @@ final class GetPresubmitGuard extends ApiRequestHandler { final sha = request.uri.queryParameters[kShaParam]!; final slug = RepositorySlug.full(slugName); - final guards = await _firestore.queryPresubmitGuards( + final guards = await UnifiedCheckRun.getPresubmitGuardsForCommitSha( + firestoreService: _firestore, slug: slug, commitSha: sha, ); diff --git a/app_dart/lib/src/service/firestore.dart b/app_dart/lib/src/service/firestore.dart index 5b0682c7c..af00e78af 100644 --- a/app_dart/lib/src/service/firestore.dart +++ b/app_dart/lib/src/service/firestore.dart @@ -16,7 +16,7 @@ import '../../cocoon_service.dart'; import '../model/firestore/commit.dart'; import '../model/firestore/github_build_status.dart'; import '../model/firestore/github_gold_status.dart'; -import '../model/firestore/presubmit_guard.dart'; + import '../model/firestore/task.dart'; import 'firestore/commit_and_tasks.dart'; @@ -264,21 +264,7 @@ mixin FirestoreQueries { return tasks.isEmpty ? null : Task.fromDocument(tasks.first); } - /// Queries for [PresubmitGuard] records by [slug] and [commitSha]. - Future> queryPresubmitGuards({ - required RepositorySlug slug, - required String commitSha, - }) async { - final filterMap = { - '${PresubmitGuard.fieldSlug} =': slug.fullName, - '${PresubmitGuard.fieldCommitSha} =': commitSha, - }; - final documents = await query( - PresubmitGuard.collectionId, - filterMap, - ); - return [...documents.map(PresubmitGuard.fromDocument)]; - } + /// Queries the last updated build status for the [slug], [prNumber], and [head]. /// diff --git a/app_dart/lib/src/service/firestore/unified_check_run.dart b/app_dart/lib/src/service/firestore/unified_check_run.dart index 7ba322b90..5f341e620 100644 --- a/app_dart/lib/src/service/firestore/unified_check_run.dart +++ b/app_dart/lib/src/service/firestore/unified_check_run.dart @@ -230,6 +230,19 @@ final class UnifiedCheckRun { )).firstOrNull; } + /// Queries for [PresubmitGuard] records by [slug] and [commitSha]. + static Future> getPresubmitGuardsForCommitSha({ + required FirestoreService firestoreService, + required RepositorySlug slug, + required String commitSha, + }) async { + return await _queryPresubmitGuards( + firestoreService: firestoreService, + slug: slug, + commitSha: commitSha, + ); + } + static Future> _queryPresubmitGuards({ required FirestoreService firestoreService, Transaction? transaction, @@ -246,10 +259,10 @@ final class UnifiedCheckRun { int? limit, }) async { final filterMap = { - '${PresubmitGuard.fieldSlug} =': ?slug, + '${PresubmitGuard.fieldSlug} =': ?slug?.fullName, '${PresubmitGuard.fieldPullRequestId} =': ?pullRequestId, '${PresubmitGuard.fieldCheckRunId} =': ?checkRunId, - '${PresubmitGuard.fieldStage} =': ?stage, + '${PresubmitGuard.fieldStage} =': ?stage?.name, '${PresubmitGuard.fieldCreationTime} =': ?creationTime, '${PresubmitGuard.fieldAuthor} =': ?author, '${PresubmitGuard.fieldCommitSha} =': ?commitSha, diff --git a/app_dart/test/service/presubmit_guard_query_test.dart b/app_dart/test/service/presubmit_guard_query_test.dart index d87a0c9fb..884d601d1 100644 --- a/app_dart/test/service/presubmit_guard_query_test.dart +++ b/app_dart/test/service/presubmit_guard_query_test.dart @@ -4,6 +4,7 @@ import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; +import 'package:cocoon_service/src/service/firestore/unified_check_run.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; @@ -42,7 +43,8 @@ void main() { firestoreService.putDocuments([guard1, guard2, otherGuard]); - final results = await firestoreService.queryPresubmitGuards( + final results = await UnifiedCheckRun.getPresubmitGuardsForCommitSha( + firestoreService: firestoreService, slug: slug, commitSha: commitSha, ); @@ -56,7 +58,8 @@ void main() { 'queryPresubmitGuards returns empty list when no guards found', () async { final slug = RepositorySlug('flutter', 'flutter'); - final results = await firestoreService.queryPresubmitGuards( + final results = await UnifiedCheckRun.getPresubmitGuardsForCommitSha( + firestoreService: firestoreService, slug: slug, commitSha: 'non-existent', ); From f5adbb9854469cd3418607f456fd81a384c1cc6a Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Wed, 4 Feb 2026 17:17:26 -0800 Subject: [PATCH 11/16] dart format --- .../request_handlers/get_presubmit_guard.dart | 4 +- app_dart/lib/src/service/firestore.dart | 2 - .../get_presubmit_guard_test.dart | 57 ++++++++++--------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index a523bf083..c25a67b6b 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -50,7 +50,9 @@ final class GetPresubmitGuard extends ApiRequestHandler { final GuardStatus guardStatus; if (guards.any((g) => g.failedBuilds > 0)) { guardStatus = GuardStatus.failed; - } else if (guards.every((g) => g.failedBuilds == 0 && g.remainingBuilds == 0)) { + } 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; diff --git a/app_dart/lib/src/service/firestore.dart b/app_dart/lib/src/service/firestore.dart index af00e78af..b8ca80526 100644 --- a/app_dart/lib/src/service/firestore.dart +++ b/app_dart/lib/src/service/firestore.dart @@ -264,8 +264,6 @@ mixin FirestoreQueries { return tasks.isEmpty ? null : Task.fromDocument(tasks.first); } - - /// Queries the last updated build status for the [slug], [prNumber], and [head]. /// /// If not existing, returns a fresh new Build status. diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index b04abf55d..b67b720c0 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -102,10 +102,12 @@ void main() { final stages = result['stages'] as List; expect(stages.length, 2); - final fusionStage = stages.firstWhere((s) => (s as Map)['name'] == 'fusion') as Map; + final fusionStage = + stages.firstWhere((s) => (s as Map)['name'] == 'fusion') as Map; expect(fusionStage['builds'], {'test1': 'Succeeded'}); - final engineStage = stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; + final engineStage = + stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; expect(engineStage['builds'], {'engine1': 'In Progress'}); }); @@ -134,30 +136,33 @@ void main() { expect(result['guard_status'], 'Failed'); }); - test('guardStatus is Succeeded if all stages are complete without failures', () async { - final slug = RepositorySlug('flutter', 'flutter'); - const sha = 'abc'; - - final guard = generatePresubmitGuard( - slug: slug, - commitSha: sha, - builds: {'test1': TaskStatus.succeeded}, - ); - guard.failedBuilds = 0; - guard.remainingBuilds = 0; - - firestore.putDocuments([guard]); - - tester.request = FakeHttpRequest( - queryParametersValue: { - GetPresubmitGuard.kSlugParam: 'flutter/flutter', - GetPresubmitGuard.kShaParam: sha, - }, - ); - - final result = (await decodeHandlerBody>())!; - expect(result['guard_status'], 'Succeeded'); - }); + test( + 'guardStatus is Succeeded if all stages are complete without failures', + () async { + final slug = RepositorySlug('flutter', 'flutter'); + const sha = 'abc'; + + final guard = generatePresubmitGuard( + slug: slug, + commitSha: sha, + builds: {'test1': TaskStatus.succeeded}, + ); + guard.failedBuilds = 0; + guard.remainingBuilds = 0; + + firestore.putDocuments([guard]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuard.kSlugParam: 'flutter/flutter', + GetPresubmitGuard.kShaParam: sha, + }, + ); + + final result = (await decodeHandlerBody>())!; + expect(result['guard_status'], 'Succeeded'); + }, + ); test('guardStatus is New if all stages are waiting for backfill', () async { final slug = RepositorySlug('flutter', 'flutter'); From 8ea4884026bb478251935ed8fa56645b6dd6e519 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Thu, 5 Feb 2026 17:23:21 -0800 Subject: [PATCH 12/16] feat: Standardize `GuardStatus` and `TaskStatus` enum serialization, add documentation to presubmit guard RPC models, and improve error handling in `GetPresubmitGuard`. --- .../request_handlers/get_presubmit_guard.dart | 25 ++++++-- .../get_presubmit_guard_test.dart | 61 +++++++++++-------- packages/cocoon_common/lib/guard_status.dart | 29 +++------ .../lib/src/rpc_model/presubmit_guard.dart | 30 +++++++-- .../lib/src/rpc_model/presubmit_guard.g.dart | 22 +++---- packages/cocoon_common/lib/task_status.dart | 3 + 6 files changed, 101 insertions(+), 69 deletions(-) diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index c25a67b6b..dbe1b76eb 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:cocoon_common/guard_status.dart'; import 'package:cocoon_common/rpc_model.dart' as rpc_model; @@ -13,8 +14,14 @@ import '../../cocoon_service.dart'; import '../request_handling/api_request_handler.dart'; import '../service/firestore/unified_check_run.dart'; +/// Request handler for retrieving the aggregated presubmit guard status. +/// +/// This handler queries the presubmit guards for a specific commit SHA and +/// returns an aggregated response including the overall guard status and +/// individual stage statuses. @immutable final class GetPresubmitGuard extends ApiRequestHandler { + /// Defines the [GetPresubmitGuard] handler. const GetPresubmitGuard({ required super.config, required super.authenticationProvider, @@ -23,9 +30,16 @@ final class GetPresubmitGuard extends ApiRequestHandler { final FirestoreService _firestore; + /// The name of the query parameter for the repository slug (e.g. 'flutter/flutter'). static const String kSlugParam = 'slug'; + + /// The name of the query parameter for the commit SHA. static const String kShaParam = 'sha'; + /// Handles the HTTP GET request. + /// + /// Requires [kSlugParam] and [kShaParam] query parameters. + /// Returns a JSON response with the aggregated presubmit guard data. @override Future get(Request request) async { checkRequiredQueryParameters(request, [kSlugParam, kShaParam]); @@ -41,7 +55,9 @@ final class GetPresubmitGuard extends ApiRequestHandler { ); if (guards.isEmpty) { - return Response.json(null); + return Response.json({ + 'error': 'No guard found for slug $slug and sha $sha', + }, statusCode: HttpStatus.notFound); } // Consolidate metadata from the first record. @@ -66,16 +82,15 @@ final class GetPresubmitGuard extends ApiRequestHandler { author: first.author, guardStatus: guardStatus, stages: [ - ...guards.map( - (g) => rpc_model.PresubmitGuardStage( + for (final g in guards) + rpc_model.PresubmitGuardStage( name: g.stage.name, createdAt: g.creationTime, builds: g.builds, ), - ), ], ); - return Response.json(response.toJson()); + return Response.json(response); } } diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index b67b720c0..65e74e3e2 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -3,7 +3,10 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io'; +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/src/rpc_model/presubmit_guard.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; @@ -26,13 +29,21 @@ void main() { late GetPresubmitGuard handler; late FakeFirestoreService firestore; - Future decodeHandlerBody() async { - final body = await tester.get(handler); - return await utf8.decoder - .bind(body.body as Stream>) - .transform(json.decoder) - .single - as T?; + Future getResponse() async { + final response = await tester.get(handler); + if (response.statusCode != HttpStatus.ok) { + return null; + } + final responseBody = await utf8.decoder + .bind(response.body as Stream>) + .transform(json.decoder) + .single; + if (responseBody == null) { + return null; + } + return PresubmitGuardResponse.fromJson( + responseBody as Map, + ); } setUp(() { @@ -58,7 +69,7 @@ void main() { }, ); - final result = await decodeHandlerBody(); + final result = await getResponse(); expect(result, isNull); }); @@ -93,22 +104,20 @@ void main() { }, ); - final result = (await decodeHandlerBody>())!; - expect(result['pr_num'], 123); - expect(result['author'], 'dash'); - expect(result['check_run_id'], 1); - expect(result['guard_status'], 'In Progress'); + final result = (await getResponse())!; + expect(result.prNum, 123); + expect(result.author, 'dash'); + expect(result.checkRunId, 1); + expect(result.guardStatus, GuardStatus.inProgress); - final stages = result['stages'] as List; + final stages = result.stages; expect(stages.length, 2); - final fusionStage = - stages.firstWhere((s) => (s as Map)['name'] == 'fusion') as Map; - expect(fusionStage['builds'], {'test1': 'Succeeded'}); + final fusionStage = stages.firstWhere((s) => s.name == 'fusion'); + expect(fusionStage.builds, {'test1': TaskStatus.succeeded}); - final engineStage = - stages.firstWhere((s) => (s as Map)['name'] == 'engine') as Map; - expect(engineStage['builds'], {'engine1': 'In Progress'}); + final engineStage = stages.firstWhere((s) => s.name == 'engine'); + expect(engineStage.builds, {'engine1': TaskStatus.inProgress}); }); test('guardStatus is Failed if any stage has failed builds', () async { @@ -132,8 +141,8 @@ void main() { }, ); - final result = (await decodeHandlerBody>())!; - expect(result['guard_status'], 'Failed'); + final result = (await getResponse())!; + expect(result.guardStatus, GuardStatus.failed); }); test( @@ -159,8 +168,8 @@ void main() { }, ); - final result = (await decodeHandlerBody>())!; - expect(result['guard_status'], 'Succeeded'); + final result = (await getResponse())!; + expect(result.guardStatus, GuardStatus.succeeded); }, ); @@ -185,7 +194,7 @@ void main() { }, ); - final result = (await decodeHandlerBody>())!; - expect(result['guard_status'], 'New'); + final result = (await getResponse())!; + expect(result.guardStatus, GuardStatus.waitingForBackfill); }); } diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart index 63758cb2c..5f4c45333 100644 --- a/packages/cocoon_common/lib/guard_status.dart +++ b/packages/cocoon_common/lib/guard_status.dart @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:collection/collection.dart'; - /// Represents different states of a presubmit guard. +library; + +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum(valueField: 'value') enum GuardStatus { /// The guard is waiting for backfill. waitingForBackfill('New'), @@ -18,25 +21,9 @@ enum GuardStatus { /// The guard ran successfully. succeeded('Succeeded'); - const GuardStatus(this._schemaValue); - final String _schemaValue; - - /// Returns the status represented by the provided [value]. - factory GuardStatus.from(String value) { - return tryFrom(value) ?? (throw ArgumentError.value(value, 'value')); - } - - /// Returns the guard status represented by the provided [value]. - static GuardStatus? tryFrom(String value) { - return values.firstWhereOrNull((v) => v.value == value); - } - - /// The canonical string value representing `this`. - String get value => _schemaValue; + const GuardStatus(this.value); + final String value; /// Returns the JSON representation of `this`. - Object? toJson() => _schemaValue; - - @override - String toString() => _schemaValue; + Object? toJson() => value; } diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart index ca486311f..495df42ab 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart @@ -10,6 +10,9 @@ import '../../task_status.dart'; part 'presubmit_guard.g.dart'; +/// Response model for a Presubmit Guard. +/// +/// Contains the aggregated status and stages of presubmit checks for a specific commit. @immutable @JsonSerializable() final class PresubmitGuardResponse { @@ -21,25 +24,35 @@ final class PresubmitGuardResponse { required this.guardStatus, }); - factory PresubmitGuardResponse.fromJson(Map json) => - _$PresubmitGuardResponseFromJson(json); - + /// The pull request number. @JsonKey(name: 'pr_num') final int prNum; + /// The check run ID associated with the presubmit guard. @JsonKey(name: 'check_run_id') final int checkRunId; + /// The login name of the author of the commit or pull request. final String author; + /// The list of stages and their corresponding builds for this presubmit guard. final List stages; + /// The overall status of the presubmit guard across all stages. @JsonKey(name: 'guard_status') final GuardStatus guardStatus; + /// Creates a [PresubmitGuardResponse] from a JSON map. + factory PresubmitGuardResponse.fromJson(Map json) => + _$PresubmitGuardResponseFromJson(json); + + /// Converts this [PresubmitGuardResponse] to a JSON map. Map toJson() => _$PresubmitGuardResponseToJson(this); } +/// Represents a single stage in a presubmit guard. +/// +/// A stage groups related builds (tasks) together, for example, 'fusion' or 'engine'. @immutable @JsonSerializable() final class PresubmitGuardStage { @@ -49,15 +62,20 @@ final class PresubmitGuardStage { required this.builds, }); - factory PresubmitGuardStage.fromJson(Map json) => - _$PresubmitGuardStageFromJson(json); - + /// The name of the stage (e.g., 'fusion', 'engine'). final String name; + /// The creation timestamp of this stage in milliseconds since the epoch. @JsonKey(name: 'created_at') final int createdAt; + /// Map of build names to their current statuses. final Map builds; + /// Creates a [PresubmitGuardStage] from a JSON map. + factory PresubmitGuardStage.fromJson(Map json) => + _$PresubmitGuardStageFromJson(json); + + /// Converts this [PresubmitGuardStage] to a JSON map. Map toJson() => _$PresubmitGuardStageToJson(this); } diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart index 85c7deca9..25ff56afc 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.g.dart @@ -29,10 +29,10 @@ Map _$PresubmitGuardResponseToJson( }; const _$GuardStatusEnumMap = { - GuardStatus.waitingForBackfill: 'waitingForBackfill', - GuardStatus.inProgress: 'inProgress', - GuardStatus.failed: 'failed', - GuardStatus.succeeded: 'succeeded', + GuardStatus.waitingForBackfill: 'New', + GuardStatus.inProgress: 'In Progress', + GuardStatus.failed: 'Failed', + GuardStatus.succeeded: 'Succeeded', }; PresubmitGuardStage _$PresubmitGuardStageFromJson(Map json) => @@ -53,11 +53,11 @@ Map _$PresubmitGuardStageToJson( }; const _$TaskStatusEnumMap = { - TaskStatus.cancelled: 'cancelled', - TaskStatus.waitingForBackfill: 'waitingForBackfill', - TaskStatus.inProgress: 'inProgress', - TaskStatus.infraFailure: 'infraFailure', - TaskStatus.failed: 'failed', - TaskStatus.succeeded: 'succeeded', - TaskStatus.skipped: 'skipped', + TaskStatus.cancelled: 'Cancelled', + TaskStatus.waitingForBackfill: 'New', + TaskStatus.inProgress: 'In Progress', + TaskStatus.infraFailure: 'Infra Failure', + TaskStatus.failed: 'Failed', + TaskStatus.succeeded: 'Succeeded', + TaskStatus.skipped: 'Skipped', }; diff --git a/packages/cocoon_common/lib/task_status.dart b/packages/cocoon_common/lib/task_status.dart index fa933fd3c..993f5f2db 100644 --- a/packages/cocoon_common/lib/task_status.dart +++ b/packages/cocoon_common/lib/task_status.dart @@ -5,6 +5,9 @@ import 'package:collection/collection.dart'; /// Represents differerent states of a task, or an execution of a build target. +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum(valueField: 'value') enum TaskStatus { /// The task was cancelled. cancelled('Cancelled'), From e7f9f38a4fd869be266f864b0cc723aeaca2cbbc Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Thu, 5 Feb 2026 17:30:55 -0800 Subject: [PATCH 13/16] refactor: Configure JsonSerializable for automatic snake_case field renaming and remove redundant JsonKey annotations. --- .../cocoon_common/lib/src/rpc_model/presubmit_guard.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart index 495df42ab..7c1c0b4d7 100644 --- a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard.dart @@ -14,7 +14,7 @@ part 'presubmit_guard.g.dart'; /// /// Contains the aggregated status and stages of presubmit checks for a specific commit. @immutable -@JsonSerializable() +@JsonSerializable(fieldRename: FieldRename.snake) final class PresubmitGuardResponse { const PresubmitGuardResponse({ required this.prNum, @@ -25,11 +25,9 @@ final class PresubmitGuardResponse { }); /// The pull request number. - @JsonKey(name: 'pr_num') final int prNum; /// The check run ID associated with the presubmit guard. - @JsonKey(name: 'check_run_id') final int checkRunId; /// The login name of the author of the commit or pull request. @@ -39,7 +37,6 @@ final class PresubmitGuardResponse { final List stages; /// The overall status of the presubmit guard across all stages. - @JsonKey(name: 'guard_status') final GuardStatus guardStatus; /// Creates a [PresubmitGuardResponse] from a JSON map. @@ -54,7 +51,7 @@ final class PresubmitGuardResponse { /// /// A stage groups related builds (tasks) together, for example, 'fusion' or 'engine'. @immutable -@JsonSerializable() +@JsonSerializable(fieldRename: FieldRename.snake) final class PresubmitGuardStage { const PresubmitGuardStage({ required this.name, @@ -66,7 +63,6 @@ final class PresubmitGuardStage { final String name; /// The creation timestamp of this stage in milliseconds since the epoch. - @JsonKey(name: 'created_at') final int createdAt; /// Map of build names to their current statuses. From 61131cadce987d8e16a7bd973d81d9cedb17acc3 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Fri, 6 Feb 2026 16:58:01 -0800 Subject: [PATCH 14/16] feat: Update GetPresubmitChecks to extend ApiRequestHandler and accept an authentication provider. --- app_dart/lib/server.dart | 3 +++ .../lib/src/request_handlers/get_presubmit_checks.dart | 4 +++- .../test/request_handlers/get_presubmit_checks_test.dart | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart index abb44e07c..9017f35fa 100644 --- a/app_dart/lib/server.dart +++ b/app_dart/lib/server.dart @@ -188,8 +188,11 @@ Server createServer({ '/api/get-presubmit-guard': GetPresubmitGuard( config: config, authenticationProvider: authProvider, + firestore: firestore, + ), '/api/get-presubmit-checks': GetPresubmitChecks( config: config, + authenticationProvider: authProvider, firestore: firestore, ), '/api/update-suppressed-test': UpdateSuppressedTest( diff --git a/app_dart/lib/src/request_handlers/get_presubmit_checks.dart b/app_dart/lib/src/request_handlers/get_presubmit_checks.dart index 2ce6e4f97..af074107a 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_checks.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_checks.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:cocoon_common/rpc_model.dart'; import '../../cocoon_service.dart'; +import '../request_handling/api_request_handler.dart'; import '../service/firestore/unified_check_run.dart'; /// Returns all checks for a specific presubmit check run. @@ -30,9 +31,10 @@ import '../service/firestore/unified_check_run.dart'; /// "summary": "Check passed" /// } /// ] -final class GetPresubmitChecks extends RequestHandler { +final class GetPresubmitChecks extends ApiRequestHandler { const GetPresubmitChecks({ required super.config, + required super.authenticationProvider, required FirestoreService firestore, }) : _firestore = firestore; diff --git a/app_dart/test/request_handlers/get_presubmit_checks_test.dart b/app_dart/test/request_handlers/get_presubmit_checks_test.dart index e5a3104a1..ffc39bdeb 100644 --- a/app_dart/test/request_handlers/get_presubmit_checks_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_checks_test.dart @@ -14,6 +14,7 @@ import 'package:cocoon_service/src/request_handlers/get_presubmit_checks.dart'; import 'package:test/test.dart'; import '../src/fake_config.dart'; +import '../src/request_handling/fake_dashboard_authentication.dart'; import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; import '../src/service/fake_firestore_service.dart'; @@ -30,7 +31,11 @@ void main() { config = FakeConfig(); tester = RequestHandlerTester(); firestoreService = FakeFirestoreService(); - handler = GetPresubmitChecks(config: config, firestore: firestoreService); + handler = GetPresubmitChecks( + config: config, + firestore: firestoreService, + authenticationProvider: FakeDashboardAuthentication(), + ); }); Future?> getPresubmitCheckResponse( From 9020e1f91339c2bf3482152200611868a8409c90 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Fri, 6 Feb 2026 17:01:53 -0800 Subject: [PATCH 15/16] refactor: Reorder `get_presubmit_guard.dart` import in `server.dart`. --- app_dart/lib/server.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart index 9017f35fa..c7fb0212c 100644 --- a/app_dart/lib/server.dart +++ b/app_dart/lib/server.dart @@ -7,8 +7,8 @@ import 'dart:math'; import 'cocoon_service.dart'; import 'src/request_handlers/get_engine_artifacts_ready.dart'; -import 'src/request_handlers/get_presubmit_guard.dart'; import 'src/request_handlers/get_presubmit_checks.dart'; +import 'src/request_handlers/get_presubmit_guard.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'; From 04292f4fad063ff5c580b5a370e1d8e18fd76e09 Mon Sep 17 00:00:00 2001 From: Dmitry Grand Date: Mon, 9 Feb 2026 10:33:56 -0800 Subject: [PATCH 16/16] sort export order --- packages/cocoon_common/lib/rpc_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cocoon_common/lib/rpc_model.dart b/packages/cocoon_common/lib/rpc_model.dart index 7eba299c6..9de70abc0 100644 --- a/packages/cocoon_common/lib/rpc_model.dart +++ b/packages/cocoon_common/lib/rpc_model.dart @@ -34,10 +34,10 @@ export 'src/rpc_model/commit_status.dart' show CommitStatus; export 'src/rpc_model/content_hash_lookup.dart' show ContentHashLookup; export 'src/rpc_model/merge_group_hooks.dart' show MergeGroupHook, MergeGroupHooks; -export 'src/rpc_model/presubmit_guard.dart' - show PresubmitGuardResponse, PresubmitGuardStage; export 'src/rpc_model/presubmit_check_response.dart' show PresubmitCheckResponse; +export 'src/rpc_model/presubmit_guard.dart' + show PresubmitGuardResponse, PresubmitGuardStage; export 'src/rpc_model/suppressed_test.dart' show SuppressedTest, SuppressionUpdate; export 'src/rpc_model/task.dart' show Task;