diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart index c7fb0212c..6457eee21 100644 --- a/app_dart/lib/server.dart +++ b/app_dart/lib/server.dart @@ -9,6 +9,7 @@ import 'cocoon_service.dart'; import 'src/request_handlers/get_engine_artifacts_ready.dart'; import 'src/request_handlers/get_presubmit_checks.dart'; import 'src/request_handlers/get_presubmit_guard.dart'; +import 'src/request_handlers/get_presubmit_guard_summaries.dart'; import 'src/request_handlers/get_tree_status_changes.dart'; import 'src/request_handlers/github_webhook_replay.dart'; import 'src/request_handlers/lookup_hash.dart'; @@ -190,6 +191,11 @@ Server createServer({ authenticationProvider: authProvider, firestore: firestore, ), + '/api/get-presubmit-guard-summaries': GetPresubmitGuardSummaries( + config: config, + authenticationProvider: authProvider, + firestore: firestore, + ), '/api/get-presubmit-checks': GetPresubmitChecks( config: config, authenticationProvider: authProvider, 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 dbe1b76eb..f52e7501f 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -63,18 +63,18 @@ final class GetPresubmitGuard extends ApiRequestHandler { // Consolidate metadata from the first record. final first = guards.first; - final GuardStatus guardStatus; - if (guards.any((g) => g.failedBuilds > 0)) { - guardStatus = GuardStatus.failed; - } else if (guards.every( - (g) => g.failedBuilds == 0 && g.remainingBuilds == 0, - )) { - guardStatus = GuardStatus.succeeded; - } else if (guards.every((g) => g.remainingBuilds == g.builds.length)) { - guardStatus = GuardStatus.waitingForBackfill; - } else { - guardStatus = GuardStatus.inProgress; - } + final totalFailed = guards.fold(0, (sum, g) => sum + g.failedBuilds); + final totalRemaining = guards.fold( + 0, + (sum, g) => sum + g.remainingBuilds, + ); + final totalBuilds = guards.fold(0, (sum, g) => sum + g.builds.length); + + final guardStatus = GuardStatus.calculate( + failedBuilds: totalFailed, + remainingBuilds: totalRemaining, + totalBuilds: totalBuilds, + ); final response = rpc_model.PresubmitGuardResponse( prNum: first.pullRequestId, diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart new file mode 100644 index 000000000..22b70cd44 --- /dev/null +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart @@ -0,0 +1,112 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart' as rpc_model; +import 'package:github/github.dart'; +import 'package:meta/meta.dart'; + +import '../../cocoon_service.dart'; +import '../model/firestore/presubmit_guard.dart'; +import '../request_handling/api_request_handler.dart'; +import '../service/firestore/unified_check_run.dart'; + +/// Request handler for retrieving all presubmit guards for a specific pull request. +/// +/// GET: /api/get-presubmit-guard-summaries +/// +/// Parameters: +/// repo: (string in query) required. The repository name (e.g., 'flutter'). +/// pr: (int in query) required. The pull request number. +/// owner: (string in query) optional. The repository owner (e.g., 'flutter'). +@immutable +final class GetPresubmitGuardSummaries extends ApiRequestHandler { + /// Defines the [GetPresubmitGuardSummaries] handler. + const GetPresubmitGuardSummaries({ + required super.config, + required super.authenticationProvider, + required FirestoreService firestore, + }) : _firestore = firestore; + + final FirestoreService _firestore; + + /// The name of the query parameter for the repository name (e.g. 'flutter'). + static const String kRepoParam = 'repo'; + + /// The name of the query parameter for the pull request number. + static const String kPRParam = 'pr'; + + /// The name of the query parameter for the repository owner (e.g. 'flutter'). + static const String kOwnerParam = 'owner'; + + @override + Future get(Request request) async { + checkRequiredQueryParameters(request, [kRepoParam, kPRParam]); + + final repo = request.uri.queryParameters[kRepoParam]!; + final prNumber = int.parse(request.uri.queryParameters[kPRParam]!); + final owner = request.uri.queryParameters[kOwnerParam] ?? 'flutter'; + + final slug = RepositorySlug(owner, repo); + final guards = await UnifiedCheckRun.getPresubmitGuardsForPullRequest( + firestoreService: _firestore, + slug: slug, + pullRequestId: prNumber, + ); + + if (guards.isEmpty) { + return Response.json({ + 'error': 'No guards found for PR $prNumber in $slug', + }, statusCode: HttpStatus.notFound); + } + + // Group guards by commitSha + final groupedGuards = >{}; + for (final guard in guards) { + groupedGuards.putIfAbsent(guard.commitSha, () => []).add(guard); + } + + final responseGuards = []; + for (final entry in groupedGuards.entries) { + final sha = entry.key; + final shaGuards = entry.value; + + final totalFailed = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.failedBuilds, + ); + final totalRemaining = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.remainingBuilds, + ); + final totalBuilds = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.builds.length, + ); + final earliestCreationTime = shaGuards.fold( + // assuming creation time is always in the past :) + DateTime.now().millisecondsSinceEpoch, + (int curr, PresubmitGuard g) => min(g.creationTime, curr), + ); + + responseGuards.add( + rpc_model.PresubmitGuardSummary( + commitSha: sha, + creationTime: earliestCreationTime, + guardStatus: GuardStatus.calculate( + failedBuilds: totalFailed, + remainingBuilds: totalRemaining, + totalBuilds: totalBuilds, + ), + ), + ); + } + + return Response.json(responseGuards); + } +} 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 771e722b3..6b33b9a8d 100644 --- a/app_dart/lib/src/service/firestore/unified_check_run.dart +++ b/app_dart/lib/src/service/firestore/unified_check_run.dart @@ -243,6 +243,19 @@ final class UnifiedCheckRun { ); } + /// Queries for [PresubmitGuard] records by [slug] and [pullRequestId]. + static Future> getPresubmitGuardsForPullRequest({ + required FirestoreService firestoreService, + required RepositorySlug slug, + required int pullRequestId, + }) async { + return await _queryPresubmitGuards( + firestoreService: firestoreService, + slug: slug, + pullRequestId: pullRequestId, + ); + } + static Future> _queryPresubmitGuards({ required FirestoreService firestoreService, Transaction? transaction, diff --git a/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart new file mode 100644 index 000000000..bc0fa002d --- /dev/null +++ b/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart @@ -0,0 +1,129 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart' as rpc_model; +import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; +import 'package:cocoon_server_test/test_logging.dart'; +import 'package:cocoon_service/src/request_handlers/get_presubmit_guard_summaries.dart'; +import 'package:cocoon_service/src/request_handling/exceptions.dart'; +import 'package:github/github.dart'; +import 'package:test/test.dart'; + +import '../src/request_handling/request_handler_tester.dart'; + +void main() { + useTestLoggerPerTest(); + + late RequestHandlerTester tester; + late GetPresubmitGuardSummaries handler; + late FakeFirestoreService firestore; + + 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; + return (responseBody as List) + .map( + (e) => rpc_model.PresubmitGuardSummary.fromJson( + e as Map, + ), + ) + .toList(); + } + + setUp(() { + firestore = FakeFirestoreService(); + tester = RequestHandlerTester(); + handler = GetPresubmitGuardSummaries( + 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: { + GetPresubmitGuardSummaries.kRepoParam: 'flutter', + GetPresubmitGuardSummaries.kPRParam: '123', + }, + ); + + final response = await tester.get(handler); + expect(response.statusCode, HttpStatus.notFound); + }); + + test('returns multiple guards grouped by commitSha', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const prNumber = 123; + + // SHA1: Two stages, both succeeded. + final guard1a = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha1', + checkRun: generateCheckRun(1), + creationTime: 100, + builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, + ); + + final guard1b = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha1', + checkRun: generateCheckRun(2), + creationTime: 110, + builds: {'test2': TaskStatus.succeeded}, + remainingBuilds: 0, + ); + + // SHA2: One stage, failed. + final guard2 = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha2', + checkRun: generateCheckRun(3), + creationTime: 200, + builds: {'test3': TaskStatus.failed}, + failedBuilds: 1, + remainingBuilds: 0, + ); + + firestore.putDocuments([guard1a, guard1b, guard2]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuardSummaries.kRepoParam: 'flutter', + GetPresubmitGuardSummaries.kPRParam: prNumber.toString(), + }, + ); + + final result = (await getResponse())!; + expect(result.length, 2); + + final item1 = result.firstWhere((g) => g.commitSha == 'sha1'); + expect(item1.creationTime, 100); // earliest + expect(item1.guardStatus, GuardStatus.succeeded); + + final item2 = result.firstWhere((g) => g.commitSha == 'sha2'); + expect(item2.creationTime, 200); + expect(item2.guardStatus, GuardStatus.failed); + }); +} 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 87b6f2c77..b57f740fb 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -78,9 +78,8 @@ void main() { commitSha: sha, stage: CiStage.fusionTests, builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, ); - guard1.failedBuilds = 0; - guard1.remainingBuilds = 0; final guard2 = generatePresubmitGuard( slug: slug, @@ -88,8 +87,6 @@ void main() { stage: CiStage.fusionEngineBuild, builds: {'engine1': TaskStatus.inProgress}, ); - guard2.failedBuilds = 0; - guard2.remainingBuilds = 1; firestore.putDocuments([guard1, guard2]); @@ -124,9 +121,9 @@ void main() { slug: slug, commitSha: sha, builds: {'test1': TaskStatus.failed}, + failedBuilds: 1, + remainingBuilds: 0, ); - guard.failedBuilds = 1; - guard.remainingBuilds = 0; firestore.putDocuments([guard]); @@ -151,9 +148,8 @@ void main() { slug: slug, commitSha: sha, builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, ); - guard.failedBuilds = 0; - guard.remainingBuilds = 0; firestore.putDocuments([guard]); @@ -178,8 +174,6 @@ void main() { commitSha: sha, builds: {'test1': TaskStatus.waitingForBackfill}, ); - guard.failedBuilds = 0; - guard.remainingBuilds = 1; firestore.putDocuments([guard]); diff --git a/conductor/tracks/get_presubmit_checks_20260205/index.md b/conductor/archive/get_presubmit_check_20260205/index.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/index.md rename to conductor/archive/get_presubmit_check_20260205/index.md diff --git a/conductor/tracks/get_presubmit_checks_20260205/metadata.json b/conductor/archive/get_presubmit_check_20260205/metadata.json similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/metadata.json rename to conductor/archive/get_presubmit_check_20260205/metadata.json diff --git a/conductor/tracks/get_presubmit_checks_20260205/plan.md b/conductor/archive/get_presubmit_check_20260205/plan.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/plan.md rename to conductor/archive/get_presubmit_check_20260205/plan.md diff --git a/conductor/tracks/get_presubmit_checks_20260205/spec.md b/conductor/archive/get_presubmit_check_20260205/spec.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/spec.md rename to conductor/archive/get_presubmit_check_20260205/spec.md diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/index.md b/conductor/archive/get_presubmit_guard_summaries_20260211/index.md new file mode 100644 index 000000000..02fa3b963 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/index.md @@ -0,0 +1,5 @@ +# Track get_presubmit_guards_20260211 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json b/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json new file mode 100644 index 000000000..7502f3f68 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "get_presubmit_guards_20260211", + "type": "feature", + "status": "new", + "created_at": "2026-02-11T12:00:00Z", + "updated_at": "2026-02-11T12:00:00Z", + "description": "Implement getPresubmitGuardsForPullRequest request handler" +} diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md b/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md new file mode 100644 index 000000000..bc447f41e --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md @@ -0,0 +1,46 @@ +# Implementation Plan: Implement `getPresubmitGuardsForPullRequest` Request Handler + +This plan outlines the steps to refactor `GuardStatus` logic and implement the `getPresubmitGuardsForPullRequest` request handler in `app_dart`. + +## Phase 1: Refactor GuardStatus Logic [checkpoint: 9cb5948] +- [x] Task: Create failing tests for `GuardStatus.calculate` in `packages/cocoon_common`. +- [x] Task: Implement `GuardStatus.calculate` in `packages/cocoon_common/lib/guard_status.dart`. +- [x] Task: Update `GetPresubmitGuard` in `app_dart` to use the new `GuardStatus.calculate`. +- [x] Task: Verify existing tests for `GetPresubmitGuard` pass. +- [x] Task: Conductor - User Manual Verification 'Refactor GuardStatus Logic' (Protocol in workflow.md) + +## Phase 2: Implement `GetPresubmitGuards` Handler [checkpoint: ab5947b] +- [x] Task: Create failing tests for `GetPresubmitGuards` in `app_dart/test/request_handlers/`. +- [x] Task: Implement `GetPresubmitGuards` class in `app_dart/lib/src/request_handlers/get_presubmit_guards.dart`. +- [x] Task: Register the new handler in `app_dart/bin/server.dart`. +- [x] Task: Create RPC model `PresubmitGuardsResponse` in `packages/cocoon_common`. +- [x] Task: Update `GetPresubmitGuards` to use the new RPC model. +- [x] Task: Verify all tests pass and coverage is > 95%. +- [x] Task: Conductor - User Manual Verification 'Implement GetPresubmitGuards Handler' (Protocol in workflow.md) + +## Phase 3: Group Guards by Commit SHA [checkpoint: c1e36b1] +- [x] Task: Update `PresubmitGuardItem` in `packages/cocoon_common` to remove `checkRunId`. +- [x] Task: Update `GetPresubmitGuards` in `app_dart` to group guards by `commitSha` and aggregate status. +- [x] Task: Update `GetPresubmitGuards` tests to verify grouping logic. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Group Guards by Commit SHA' (Protocol in workflow.md) + +## Phase 4: Refactor to PresubmitGuardSummary [checkpoint: 722a108] +- [x] Task: Rename `PresubmitGuardItem` to `PresubmitGuardSummary` and remove `PresubmitGuardsResponse` wrapper in `packages/cocoon_common`. +- [x] Task: Update `GetPresubmitGuards` in `app_dart` to return `List`. +- [x] Task: Update `GetPresubmitGuards` tests for the new response format. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Refactor to PresubmitGuardSummary' (Protocol in workflow.md) + +## Phase 5: Rename Handler to GetPresubmitGuardSummaries [checkpoint: 0223684] +- [x] Task: Rename `GetPresubmitGuards` class and file to `GetPresubmitGuardSummaries`. +- [x] Task: Update registration in `app_dart/lib/server.dart`. +- [x] Task: Update tests for the renamed handler. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Rename Handler to GetPresubmitGuardSummaries' (Protocol in workflow.md) + +## Phase 6: Update API URL +- [x] Task: Update API URL in `app_dart/lib/server.dart` to `/api/get-presubmit-guard-summaries`. +- [x] Task: Update documentation in `GetPresubmitGuardSummaries` handler. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Update API URL' (Protocol in workflow.md) diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md b/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md new file mode 100644 index 000000000..07ce7a903 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md @@ -0,0 +1,34 @@ +# Specification: Implement `getPresubmitGuardSummaries` Request Handler + +## Overview +This track involves implementing a new request handler in the `app_dart` service to retrieve a summary of presubmit guards for a specific pull request, grouped by commit SHA. It also includes refactoring the `GuardStatus` calculation logic and creating a shared RPC model. + +## Functional Requirements +- **Refactoring:** + - Moved `GuardStatus` calculation logic to `packages/cocoon_common/lib/guard_status.dart` as a static `calculate` method. + - Updated existing `GetPresubmitGuard` handler to use the centralized logic. +- **RPC Model:** + - Created `PresubmitGuardSummary` model in `packages/cocoon_common` to represent the aggregated status of guards for a single commit SHA. +- **Endpoint:** Created a new GET endpoint: `/api/get-presubmit-guard-summaries`. +- **Input Parameters:** + - `repo`: The GitHub repository name (required). + - `pr`: The pull request number (required). + - `owner`: The GitHub repository owner (optional, defaults to "flutter"). +- **Service Integration:** Uses `UnifiedCheckRun.getPresubmitGuardsForPullRequest` to fetch all guards for the PR. +- **Grouping Logic:** Guards are grouped by `commit_sha`. For each group, the status is aggregated using `GuardStatus.calculate` based on the sum of failed, remaining, and total builds across all stages for that SHA. +- **Response Data:** A JSON array of `PresubmitGuardSummary` objects: + - `commit_sha`: The SHA of the commit. + - `creation_time`: The latest creation timestamp among all guards for that SHA. + - `guard_status`: The aggregated status (`New`, `In Progress`, `Failed`, `Succeeded`). +- **Error Handling:** + - Returns **400 Bad Request** if `repo` or `pr` are missing. + - Returns **404 Not Found** if no guards are found for the PR. + +## Acceptance Criteria +- [x] `GuardStatus.calculate` implemented and tested. +- [x] `GetPresubmitGuard` refactored to use `GuardStatus.calculate`. +- [x] `PresubmitGuardSummary` RPC model created and exported. +- [x] `GetPresubmitGuardSummaries` handler implemented with grouping and aggregation logic. +- [x] Endpoint registered at `/api/get-presubmit-guard-summaries`. +- [x] Unit tests cover all requirements and grouping logic. +- [x] Code coverage > 95%. diff --git a/conductor/product-guidelines.md b/conductor/product-guidelines.md index 5bf6e82eb..64ec2f483 100644 --- a/conductor/product-guidelines.md +++ b/conductor/product-guidelines.md @@ -9,6 +9,15 @@ * **Layered Complexity:** While the primary view should be dense and informative, use visual cues (like color-coding and iconography) to provide immediate high-level status (Red/Green/Yellow). Detailed logs and historical data should be easily accessible but secondary to the main grid. * **Performance First:** Given the volume of data in Flutter's CI, the UI must remain responsive. Lazy loading, efficient data fetching, and minimal re-renders are critical for a smooth monitoring experience. +## Visual Identity & UX +* **Design System:** Standard Material Design 3. The dashboard will strictly follow the latest Material Design specifications to ensure a modern, consistent, and recognizable Flutter user experience. +* **Layout:** Prioritize clarity and logical flow. Use Material components (Scaffolds, Cards, Buttons, etc.) to structure information predictably. + +## Accessibility (a11y) +* **Compliance:** Strict adherence to WCAG 2.1 Level AA standards. +* **Implementation:** All UI elements must have appropriate semantic labels, maintain high contrast ratios, and provide full support for screen readers. +* **Focus Management:** Ensure a logical focus order for keyboard navigation across all interactive elements. + ## Reliability & Error Handling * **Resilience through Automation:** The system must automatically handle transient failures (e.g., GitHub API timeouts or LUCI build-bucket flakes) using robust retry logic to maintain high data fidelity. * **Graceful Degradation:** The dashboard must remain functional even if partial data is missing. If a specific service (like BigQuery or a specific LUCI project) is down, the UI should clearly indicate the limitation while still serving available information. diff --git a/conductor/product.md b/conductor/product.md index 8411b2b2b..d217d0874 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -17,9 +17,13 @@ Cocoon is the CI coordination and orchestration system for the Flutter project. ## Key Features * **Offline Integration Testing:** A dedicated testing environment that simulates all backend services (GitHub, Firestore, BigQuery, LUCI) with functional fakes, enabling deterministic, offline verification of frontend and backend logic. * **Tree Status Dashboard:** A Flutter-based web application that provides a visual overview of build health across various commits and branches. -* **Presubmit Check Details:** APIs to retrieve detailed attempt history and status for specific presubmit checks, aiding in debugging and visibility. +* **Presubmit Check Details:** Backend APIs to retrieve detailed attempt history and status for specific presubmit checks, aiding in debugging and visibility. +* **Presubmit Guard Summaries:** Backend APIs to retrieve summaries of all presubmit checks (Presubmit Guards) of the provided pull request to the dashboard. +* **Presubmit Guard Details:** Backend APIs to retrieve detailed information about a specific presubmit check (Presubmit Guard) to the dashboard. * **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. + +## Visual Aesthetic & UX +The dashboard adheres to **Material Design** principles, providing a standard, familiar, and polished Flutter user experience. The interface focuses on clarity and accessibility, ensuring that critical build information is easy to find and interpret. \ No newline at end of file diff --git a/conductor/tracks.md b/conductor/tracks.md index 6ee7e7968..8e99eb01f 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,4 +1,6 @@ # Tracks Registry +<<<<<<< 176990-guards-for-pr +======= --- @@ -10,3 +12,4 @@ - [~] **Track: Build a Merge Queue Dashboard** *Link: [./tracks/merge_queue_dashboard_20260205/](./tracks/merge_queue_dashboard_20260205/)* +>>>>>>> main diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart index 5f4c45333..af3998c5e 100644 --- a/packages/cocoon_common/lib/guard_status.dart +++ b/packages/cocoon_common/lib/guard_status.dart @@ -26,4 +26,21 @@ enum GuardStatus { /// Returns the JSON representation of `this`. Object? toJson() => value; + + /// Calculates the [GuardStatus] based on build counts. + static GuardStatus calculate({ + required int failedBuilds, + required int remainingBuilds, + required int totalBuilds, + }) { + if (failedBuilds > 0) { + return GuardStatus.failed; + } else if (failedBuilds == 0 && remainingBuilds == 0) { + return GuardStatus.succeeded; + } else if (remainingBuilds == totalBuilds) { + return GuardStatus.waitingForBackfill; + } else { + return GuardStatus.inProgress; + } + } } diff --git a/packages/cocoon_common/lib/rpc_model.dart b/packages/cocoon_common/lib/rpc_model.dart index 9de70abc0..99bcad1e9 100644 --- a/packages/cocoon_common/lib/rpc_model.dart +++ b/packages/cocoon_common/lib/rpc_model.dart @@ -38,6 +38,7 @@ export 'src/rpc_model/presubmit_check_response.dart' show PresubmitCheckResponse; export 'src/rpc_model/presubmit_guard.dart' show PresubmitGuardResponse, PresubmitGuardStage; +export 'src/rpc_model/presubmit_guard_summary.dart' show PresubmitGuardSummary; 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_summary.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.dart new file mode 100644 index 000000000..1b6a54fc5 --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.dart @@ -0,0 +1,37 @@ +// 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:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../../guard_status.dart'; + +part 'presubmit_guard_summary.g.dart'; + +/// Represents a summary of presubmit guard for a specific commit. +@immutable +@JsonSerializable(fieldRename: FieldRename.snake) +final class PresubmitGuardSummary { + const PresubmitGuardSummary({ + required this.commitSha, + required this.creationTime, + required this.guardStatus, + }); + + /// The commit SHA. + final String commitSha; + + /// The creation timestamp in microseconds since epoch. + final int creationTime; + + /// The status of the guard. + final GuardStatus guardStatus; + + /// Creates a [PresubmitGuardSummary] from a JSON map. + factory PresubmitGuardSummary.fromJson(Map json) => + _$PresubmitGuardSummaryFromJson(json); + + /// Converts this [PresubmitGuardSummary] to a JSON map. + Map toJson() => _$PresubmitGuardSummaryToJson(this); +} diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart new file mode 100644 index 000000000..d4acbe96d --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'presubmit_guard_summary.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PresubmitGuardSummary _$PresubmitGuardSummaryFromJson( + Map json, +) => PresubmitGuardSummary( + commitSha: json['commit_sha'] as String, + creationTime: (json['creation_time'] as num).toInt(), + guardStatus: $enumDecode(_$GuardStatusEnumMap, json['guard_status']), +); + +Map _$PresubmitGuardSummaryToJson( + PresubmitGuardSummary instance, +) => { + 'commit_sha': instance.commitSha, + 'creation_time': instance.creationTime, + 'guard_status': instance.guardStatus, +}; + +const _$GuardStatusEnumMap = { + GuardStatus.waitingForBackfill: 'New', + GuardStatus.inProgress: 'In Progress', + GuardStatus.failed: 'Failed', + GuardStatus.succeeded: 'Succeeded', +}; diff --git a/packages/cocoon_common/lib/task_status.dart b/packages/cocoon_common/lib/task_status.dart index 993f5f2db..36ff18a5d 100644 --- a/packages/cocoon_common/lib/task_status.dart +++ b/packages/cocoon_common/lib/task_status.dart @@ -30,8 +30,7 @@ enum TaskStatus { /// The task was skipped instead of being executed. skipped('Skipped'); - const TaskStatus(this._schemaValue); - final String _schemaValue; + const TaskStatus(this.value); /// Returns the status represented by the provided [value]. /// @@ -50,7 +49,7 @@ enum TaskStatus { /// The canonical string value representing `this`. /// /// This is the inverse of [TaskStatus.from] or [TaskStatus.tryFrom]. - String get value => _schemaValue; + final String value; /// Whether the status represents a completed task reaching a terminal state. bool get isComplete => _complete.contains(this); @@ -87,8 +86,8 @@ enum TaskStatus { bool get isBuildCompleted => isBuildSuccessed || isBuildFailed; /// Returns the JSON representation of `this`. - Object? toJson() => _schemaValue; + Object? toJson() => value; @override - String toString() => _schemaValue; + String toString() => value; } diff --git a/packages/cocoon_common/test/guard_status_test.dart b/packages/cocoon_common/test/guard_status_test.dart new file mode 100644 index 000000000..b7d10e7a9 --- /dev/null +++ b/packages/cocoon_common/test/guard_status_test.dart @@ -0,0 +1,54 @@ +// 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:test/test.dart'; + +void main() { + group('GuardStatus.calculate', () { + test('returns failed if there are failed builds', () { + expect( + GuardStatus.calculate( + failedBuilds: 1, + remainingBuilds: 0, + totalBuilds: 10, + ), + GuardStatus.failed, + ); + }); + + test('returns succeeded if no failures and no remaining builds', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 0, + totalBuilds: 10, + ), + GuardStatus.succeeded, + ); + }); + + test('returns waitingForBackfill if all builds are remaining', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 10, + totalBuilds: 10, + ), + GuardStatus.waitingForBackfill, + ); + }); + + test('returns inProgress if some builds are done and no failures yet', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 5, + totalBuilds: 10, + ), + GuardStatus.inProgress, + ); + }); + }); +} diff --git a/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart b/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart index fd765fb87..c37a9595a 100644 --- a/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart +++ b/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart @@ -319,7 +319,8 @@ PresubmitGuard generatePresubmitGuard({ String commitSha = 'abc', int creationTime = 1, String author = 'dash', - int buildCount = 1, + int remainingBuilds = -1, + int failedBuilds = 0, Map? builds, }) { return PresubmitGuard( @@ -330,8 +331,10 @@ PresubmitGuard generatePresubmitGuard({ commitSha: commitSha, creationTime: creationTime, author: author, - remainingBuilds: buildCount, - failedBuilds: 0, + remainingBuilds: remainingBuilds >= 0 + ? remainingBuilds + : (builds?.length ?? 0), + failedBuilds: failedBuilds, builds: builds, ); }