From 50d40d18e8c5c502e83e57e5573b4c721b17bb6c Mon Sep 17 00:00:00 2001 From: "Dmitry Grand (dmgr)" Date: Thu, 12 Nov 2026 13:27:51 -0800 Subject: [PATCH 1/2] Added Partner Integration Guide --- rfc/610.0002-flutter-pluggable-ci.md | 331 ++++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 4 deletions(-) diff --git a/rfc/610.0002-flutter-pluggable-ci.md b/rfc/610.0002-flutter-pluggable-ci.md index 7b28ee0..e5c8c98 100644 --- a/rfc/610.0002-flutter-pluggable-ci.md +++ b/rfc/610.0002-flutter-pluggable-ci.md @@ -5,7 +5,7 @@ title: Flutter Pluggable CI Design description: Describes the integration with partner CI systems to trigger partner CI execution on post-submit PRs and display execution details on the post-submit dashboard. status: draft created: 2026-10-06T09:00:00Z -updated: 2026-11-10T21:43:02.541563Z +updated: 2026-11-11T18:08:18Z tags: - 600-infrastructure - 610-cicd @@ -413,7 +413,7 @@ Once a task transitions to any terminal state, the `partner_ci_status_sub` handl Because Cocoon and external Partner CI pipelines deploy on independent release schedules, versioning is enforced across both inbound and outbound communication: * **Outbound Versioning (Cocoon -> Partner):** Because Cocoon does not control the URI structure of partner-hosted endpoints, each partner entry in `partner_config` specifies an `api_version` field (e.g., `"v1"`), and Cocoon includes a top-level `"version"` claim (e.g., `"version": "v1"`) in outbound JWT payloads. Cocoon serializes outbound webhook requests and parses manual `GET /status` responses according to that partner's configured `api_version`. -* **Inbound Versioning (Partner -> Cocoon):** Cocoon versions its callback endpoint in the URI path (`/api/v1/partner/callback`). Because the JWT `"aud"` claim MUST match the exact target endpoint URL (`"https://cocoon.flutter.dev/api/v1/partner/callback"`), the API version is already cryptographically bound to the token signature without needing a separate `"version"` claim in the callback payload. When breaking changes are introduced, Cocoon can expose `/api/v2/partner/callback` alongside `/api/v1/partner/callback` so partners can migrate independently without downtime. +* **Inbound Versioning (Partner -> Cocoon):** Cocoon versions its callback endpoint in the URI path (`/api/v1/partner/callback`). Because the JWT `"aud"` claim MUST match the exact target endpoint URL (`"https://dashboard.flutter.dev/api/v1/partner/callback"`), the API version is already cryptographically bound to the token signature without needing a separate `"version"` claim in the callback payload. When breaking changes are introduced, Cocoon can expose `/api/v2/partner/callback` alongside `/api/v1/partner/callback` so partners can migrate independently without downtime. * **Forward & Backward Compatibility Rules:** * **Non-Breaking Changes (No Version Bump):** Adding new optional fields to `data` or `details[]`. Both Cocoon and Partner webhook parsers MUST ignore unrecognized JSON fields. * **Breaking Changes (Requires Major Version Bump, e.g., `v1` -> `v2`):** Removing or renaming existing fields, changing field data types, adding new required fields, or changing the JWT authentication envelope. @@ -769,7 +769,7 @@ After executing custom test suites on proprietary hardware, the partner sends th ```http POST /api/v1/partner/callback HTTP/1.1 -Host: cocoon.flutter.dev +Host: dashboard.flutter.dev Content-Type: application/jwt .. @@ -780,7 +780,7 @@ Content-Type: application/jwt ```json { "iss": "3p-partner-ci", - "aud": "https://cocoon.flutter.dev/api/v1/partner/callback", + "aud": "https://dashboard.flutter.dev/api/v1/partner/callback", "iat": 1774963500, "exp": 1774963800, "data": { @@ -919,3 +919,326 @@ Where: * `neutral` - Test failed but considered flaky (success state). * `skipped` - Test skipped (success state). * **url**: The publicly accessible task execution details or logs URL. + +--- + +## 9. Partner Integration Guide + +This section serves as a standalone reference for partner engineering teams integrating their CI systems with Flutter's Cocoon infrastructure. It covers only the external Cocoon-to-Partner communication flow, authentication requirements, and the endpoints that partners must host or call. + +### 9.1. Onboarding & Configuration Prerequisites + +To onboard a Partner CI integration, the partner must provide the following configuration values to the Flutter infrastructure team: + +| Parameter | Required | Description | +| --- | --- | --- | +| `partner` | Yes | Unique partner identifier (e.g., `3p-partner-ci`). MUST match the `iss` claim in all JWT callbacks sent by the partner to Cocoon. | +| `task_name` | Yes | Name of the post-submit task displayed on the Flutter build dashboard. | +| `slug` | Yes | Target GitHub repository to trigger on (e.g., `flutter/flutter`). | +| `branches` | Yes | List of target branch patterns to trigger on (e.g., `["master", "beta", "stable"]`). | +| `webhook_url` | Yes | Public `https://` URL hosted by the partner to receive CI trigger requests from Cocoon. | +| `keys_url` | Yes | Public, unauthenticated `https://` JWKS URL hosted by the partner to serve Ed25519 public keys. | +| `status_url` | No | Public `https://` URL hosted by the partner to handle manual on-demand status queries from Cocoon. | + +**Network & URL Security Constraints:** + +* All partner URLs (`webhook_url`, `keys_url`, `status_url`, and log `details[].url` links) MUST use the `https://` scheme and resolve to public IP addresses (private, loopback, and link-local IP ranges are blocked). +* Cocoon does not follow HTTP redirects to disallowed hosts or non-HTTPS schemes. +* Because the Flutter build dashboard is public, `message` summaries and `details[].url` links submitted to Cocoon MUST NOT contain Personally Identifiable Information (PII), internal hostnames, or confidential secrets. + +### 9.2. Authentication & JWT Verification + +All communication between Cocoon and Partner CI uses short-lived JSON Web Tokens (JWT) signed with asymmetric [Ed25519](https://www.rfc-editor.org/info/rfc8410/) keys (`alg: "EdDSA"`). Neither party shares private keys or relies on a central authorization server. + +* **JWT Header Format:** + + ```json + { + "alg": "EdDSA", + "typ": "JWT", + "kid": "" + } + ``` + +* **Verifying Requests from Cocoon (Partner Inbound):** + 1. Extract the JWT from the HTTP request body (`POST `) or the `Authorization: Bearer ` header (`GET `). + 2. Verify that the header specifies `"alg": "EdDSA"` and `"typ": "JWT"` ([RFC 8725](https://www.rfc-editor.org/info/rfc8725/#section-3)). + 3. Look up the public key matching the header's `kid` from Cocoon's public JWKS endpoint (caching the key set and refreshing on cache miss). + 4. Verify the Ed25519 signature ([RFC 7515](https://www.rfc-editor.org/info/rfc7515/#section-5.2)) and validate standard claims ([RFC 7519](https://www.rfc-editor.org/info/rfc7519/#section-7.2)): + * `iss` MUST equal `"cocoon"`. + * `aud` MUST match the exact partner endpoint URL (`webhook_url` or `status_url`). + * Current time MUST be before `exp` (and not before `iat`). + +* **Signing Callbacks to Cocoon (Partner Outbound):** + 1. Generate a new JWT per HTTP request attempt with a short expiration window (e.g., 5 minutes: `exp = iat + 300`). + 2. Set `"iss"` to your configured `partner` identifier and `"aud"` to `"https://dashboard.flutter.dev/api/v1/partner/callback"`. + 3. Sign the token using your Ed25519 private key and include the corresponding `"kid"` in the JWT header so Cocoon can verify it against your `keys_url` JWKS endpoint. + +### 9.3. Cocoon-to-Partner Communication Flow + +The following sequence diagram illustrates the complete external communication lifecycle between Cocoon and Partner CI across all supported workflows: + +```mermaid +sequenceDiagram + participant Cocoon as Cocoon Server + participant Partner as Partner CI + + Note over Cocoon,Partner: 1. Post-Submit Trigger (Cocoon -> Partner) + Cocoon->>+Partner: HTTP POST (Content-Type: application/jwt)
Signed JWT with uid=abcd, sha=wxyz, branch, pr, slug + opt Public key for kid not in Partner cache + Partner->>Cocoon: HTTP GET + Cocoon-->>Partner: 200 OK (Cocoon JWKS) + end + Note over Partner: Validate JWT & enqueue CI job asynchronously + Partner-->>-Cocoon: 200 OK (within 30s) + + Note over Partner: Start executing custom tests + + opt 2. Optional Start Notification (Partner -> Cocoon) + Partner->>+Cocoon: HTTP POST /api/v1/partner/callback (Content-Type: application/jwt)
Signed JWT with uid=abcd, status="in_progress", details[] + opt Public key for kid not in Cocoon cache + Cocoon->>Partner: HTTP GET + Partner-->>Cocoon: 200 OK (Partner JWKS) + end + Cocoon-->>-Partner: 200 OK + end + + opt 3. Optional Manual Status Check (Cocoon -> Partner) + Cocoon->>+Partner: HTTP GET ?uid=abcd
Authorization: Bearer + Partner-->>-Cocoon: 200 OK {"status": "in_progress", "message": "...", "details": [...]} + end + + Note over Partner: Finish executing custom tests + + Note over Cocoon,Partner: 4. Terminal Status Callback (Partner -> Cocoon) + Partner->>+Cocoon: HTTP POST /api/v1/partner/callback (Content-Type: application/jwt)
Signed JWT with uid=abcd, terminal status, message, details[] + opt Public key for kid not in Cocoon cache + Cocoon->>Partner: HTTP GET + Partner-->>Cocoon: 200 OK (Partner JWKS) + end + Cocoon-->>-Partner: 200 OK + + opt 5. Task Re-run Request (Cocoon -> Partner) + Cocoon->>+Partner: HTTP POST (Content-Type: application/jwt)
Signed JWT with NEW uid=efgh and same sha=wxyz + Partner-->>-Cocoon: 200 OK + Note over Partner: Execute tests for uid=efgh and report callback for uid=efgh + end +``` + +#### 9.3.1. Workflow Summary + +1. **Post-Submit Trigger (`Cocoon -> Partner`):** + * When a PR is merged into a configured branch on `flutter/flutter`, Cocoon sends an `HTTP POST` request with a signed JWT to the partner's `webhook_url`. + * The partner verifies the JWT signature using Cocoon's JWKS endpoint, enqueues the CI job for asynchronous execution, and immediately returns `200 OK` (within **30 seconds**). + * If the partner endpoint times out or responds with `5xx`, `408 Request Timeout`, or `429 Too Many Requests`, Cocoon automatically retries the request using exponential backoff (60s to 600s, up to 10 attempts). +2. **Optional Start Notification (`Partner -> Cocoon`):** + * When test execution starts for `uid`, the partner MAY send a signed JWT callback to `POST /api/v1/partner/callback` with `"status": "in_progress"` and optional `details[]` listing scheduled test suites and log URLs. +3. **Optional On-Demand Status Query (`Cocoon -> Partner`):** + * If the partner configures a `status_url`, users on the Flutter build dashboard can manually request a status refresh while waiting for results. Cocoon sends `GET ?uid=` with `Authorization: Bearer `, and the partner returns the current status JSON payload. +4. **Terminal Status Callback (`Partner -> Cocoon`):** + * Once test execution completes, the partner sends a signed JWT to `POST /api/v1/partner/callback` containing the final status (`succeeded`, `failed`, `infra_failure`, `cancelled`, `neutral`, or `skipped`), an optional summary `message`, and granular test `details[]`. + * If the callback times out or receives `5xx`, `408`, or `429`, the partner CI SHOULD retry with exponential backoff. +5. **Task Re-runs (`Cocoon -> Partner`):** + * If a task is re-run (automatically or manually from the dashboard), Cocoon dispatches a new `POST ` request for the same commit `sha` but with a **new unique `uid`**. + * Keying executions by `uid` ensures the partner starts a fresh test run rather than rejecting the re-run as a duplicate (`409 Conflict`). + +### 9.4. Endpoints Specification + +#### 9.4.1. Partner-Hosted Endpoints + +Partners MUST host the JWKS and Webhook endpoints, and MAY optionally host the Status endpoint. + +##### 9.4.1.1. Partner Public JWKS Endpoint (Required) + +* **URL:** `GET ` +* **Authentication:** None (publicly accessible over `https://`). +* **Constraints:** Must respond within **3 seconds**; maximum response body size is **64 KB**. +* **Response Format:** Standard JSON Web Key Set ([RFC 7517](https://www.rfc-editor.org/info/rfc7517)) containing the partner's active Ed25519 public key(s) and `kid`(s): + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + + { + "keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "use": "sig", + "alg": "EdDSA", + "kid": "partner-key-2026-10", + "x": "" + } + ] + } + ``` + +##### 9.4.1.2. Partner Webhook Listener Endpoint (Required) + +* **URL:** `POST ` +* **Authentication:** Request body is a raw JWT signed by Cocoon (`iss: "cocoon"`, `aud: ""`). +* **Constraints:** Partner MUST respond within **30 seconds** (acknowledge receipt before running tests); maximum response body size is **64 KB**. +* **Backward/Forward Compatibility:** Partner parsers MUST ignore unrecognized JSON fields in `data`. +* **HTTP Request:** + + ```http + POST /webhook HTTP/1.1 + Host: partner-ci.example.com + Content-Type: application/jwt + + .. + ``` + +* **Decoded JWT Payload (`v1`):** + + ```json + { + "iss": "cocoon", + "aud": "https://partner-ci.example.com/webhook", + "iat": 1774962000, + "exp": 1774962300, + "version": "v1", + "data": { + "uid": "0sAtLhwbmGz2Y0WBDzPL", + "slug": "flutter/flutter", + "branch": "master", + "pr": 12345, + "sha": "6f0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b" + } + } + ``` + +* **Expected Partner HTTP Response Codes:** + + | Status Code | Meaning | Cocoon Retry Behavior | + | --- | --- | --- | + | `200 OK` | Webhook accepted and CI execution queued. | Acknowledges dispatch; no retry. | + | `409 Conflict` | `uid` was already received/processed by the partner. | Logged as warning; acknowledges dispatch; no retry. | + | `400` / `401` / `403` / `404` | Client or authentication error. | Logged as error; acknowledges dispatch; no retry. | + | `408` / `429` / `5xx` / Timeout | Temporary overload, timeout, or server error. | Retries with exponential backoff (60s–600s, up to 10 attempts). | + +##### 3. Partner On-Demand Status Endpoint (Optional) + +* **URL:** `GET ?uid=` +* **Authentication:** `Authorization: Bearer ` signed by Cocoon. +* **Constraints:** Must respond within **30 seconds**; maximum response body size is **768 KB**. +* **HTTP Request:** + + ```http + GET /status?uid=0sAtLhwbmGz2Y0WBDzPL HTTP/1.1 + Host: partner-ci.example.com + Authorization: Bearer .. + ``` + +* **Decoded Bearer JWT Payload:** + + ```json + { + "iss": "cocoon", + "aud": "https://partner-ci.example.com/status", + "iat": 1774962600, + "exp": 1774962900, + "version": "v1", + "data": { + "uid": "0sAtLhwbmGz2Y0WBDzPL" + } + } + ``` + +* **HTTP Response (`200 OK`):** + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + + { + "status": "in_progress", + "message": "Running test suites on hardware pool A", + "details": [ + { + "name": "device_1_test_suite_1", + "status": "in_progress", + "url": "https://partner-ci.example.com/device_1_test_suite_1/log" + }, + { + "name": "device_1_test_suite_2", + "status": "succeeded", + "url": "https://partner-ci.example.com/device_1_test_suite_2/log" + } + ] + } + ``` + +#### 9.4.2. Cocoon-Hosted Endpoints Called by Partner + +##### 9.4.2.1. Cocoon Public JWKS Endpoint + +* **URL:** `GET ` +* **Purpose:** Fetched and cached by the partner to verify Ed25519 JWT signatures on inbound `POST ` and `GET ` requests from Cocoon. + +##### 9.4.2.2. Cocoon Status Callback Endpoint + +* **URL:** `POST https://dashboard.flutter.dev/api/v1/partner/callback` +* **Authentication:** Request body is a raw JWT signed with the partner's Ed25519 private key (`iss: ""`, `aud: "https://dashboard.flutter.dev/api/v1/partner/callback"`). +* **Constraints:** Maximum request body size is **768 KB**. All `details[].url` fields MUST use `https://`. +* **HTTP Request:** + + ```http + POST /api/v1/partner/callback HTTP/1.1 + Host: dashboard.flutter.dev + Content-Type: application/jwt + + .. + ``` + +* **Decoded JWT Payload:** + + ```json + { + "iss": "3p-partner-ci", + "aud": "https://dashboard.flutter.dev/api/v1/partner/callback", + "iat": 1774963500, + "exp": 1774963800, + "data": { + "uid": "0sAtLhwbmGz2Y0WBDzPL", + "status": "failed", + "message": "1 of 2 test suites failed", + "details": [ + { + "name": "device_1_test_suite_1", + "status": "failed", + "url": "https://partner-ci.example.com/device_1_test_suite_1/log" + }, + { + "name": "device_1_test_suite_2", + "status": "succeeded", + "url": "https://partner-ci.example.com/device_1_test_suite_2/log" + } + ] + } + } + ``` + +* **Allowed Status Values (`data.status` & `data.details[].status`):** + + | Status Value | Category | Description | + | --- | --- | --- | + | `in_progress` | Non-Terminal | Task or test is currently executing. Allowed only in optional start callbacks and `GET /status` responses. | + | `succeeded` | Terminal (Success) | All tests passed. | + | `neutral` | Terminal (Success) | Only flaky tests failed (non-failing outcome). | + | `skipped` | Terminal (Success) | Tests were skipped. | + | `failed` | Terminal (Failure) | One or more tests failed. `message` summary should be provided. | + | `infra_failure` | Terminal (Failure) | Infrastructure failure prevented test execution. `message` summary should be provided. | + | `cancelled` | Terminal (Failure) | Execution was cancelled by the Partner CI. | + +* **Cocoon HTTP Response Codes & Partner Retry Rules:** + + | Status Code | Meaning | Partner Action | + | --- | --- | --- | + | `200 OK` | Status update accepted by Cocoon. | Done; do not retry. | + | `400 Bad Request` | Payload schema validation error or non-HTTPS log URL. | Do not retry; fix payload construction. | + | `401 Unauthorized` | Unknown `kid`, expired token, or invalid JWT signature. | Do not retry; verify signing key and JWKS endpoint. | + | `404 Not Found` | `data.uid` does not exist in Cocoon. | Do not retry. | + | `409 Conflict` | Terminal status was already processed for `data.uid`. | Treat as idempotent success; do not retry. | + | `413 Payload Too Large` | Request body exceeds the 768 KB limit. | Do not retry; truncate `message` or `details[]`. | + | `408` / `429` / `5xx` / Timeout | Temporary server or network failure. | Retry with exponential backoff. | From a62bed90f260777ccc228c28e7d42c6641499254 Mon Sep 17 00:00:00 2001 From: "Dmitry Grand (dmgr)" Date: Thu, 12 Nov 2026 17:01:26 -0800 Subject: [PATCH 2/2] added Expected HTTP Response Codes for On-Demand Status Endpoint --- rfc/610.0002-flutter-pluggable-ci.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rfc/610.0002-flutter-pluggable-ci.md b/rfc/610.0002-flutter-pluggable-ci.md index e5c8c98..9de5544 100644 --- a/rfc/610.0002-flutter-pluggable-ci.md +++ b/rfc/610.0002-flutter-pluggable-ci.md @@ -1117,7 +1117,7 @@ Partners MUST host the JWKS and Webhook endpoints, and MAY optionally host the S | `400` / `401` / `403` / `404` | Client or authentication error. | Logged as error; acknowledges dispatch; no retry. | | `408` / `429` / `5xx` / Timeout | Temporary overload, timeout, or server error. | Retries with exponential backoff (60s–600s, up to 10 attempts). | -##### 3. Partner On-Demand Status Endpoint (Optional) +##### 9.4.1.3. Partner On-Demand Status Endpoint (Optional) * **URL:** `GET ?uid=` * **Authentication:** `Authorization: Bearer ` signed by Cocoon. @@ -1169,6 +1169,16 @@ Partners MUST host the JWKS and Webhook endpoints, and MAY optionally host the S } ``` +* **Expected Partner HTTP Response Codes:** + + | Status Code | Meaning | Cocoon Behavior | + | --- | --- | --- | + | `200 OK` | Status retrieved successfully (`{status, message, details}`). | Updates task state in Cocoon. | + | `400 Bad Request` | Malformed request (e.g., missing `uid` parameter). | Logged as error; surfaces failure to dashboard. | + | `401` / `403` | Authentication error (e.g., invalid or expired Bearer JWT). | Logged as error; surfaces failure to dashboard. | + | `404 Not Found` | `uid` not found in the partner CI system. | Logged as warning; surfaces `uid` not found to dashboard. | + | `408` / `429` / `5xx` / Timeout | Server error, rate limit, or request timeout. | Logged as error; surfaces failure to dashboard. | + #### 9.4.2. Cocoon-Hosted Endpoints Called by Partner ##### 9.4.2.1. Cocoon Public JWKS Endpoint