Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 337 additions & 4 deletions rfc/610.0002-flutter-pluggable-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

<Base64 Encoded JWT Header>.<Base64 Encoded JWT Payload>.<Signature>
Expand All @@ -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": {
Expand Down Expand Up @@ -919,3 +919,336 @@ 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey, should this be a separate document that links back to the RFC? I think this would make it a little nicer for partners to read this content, but I don't feel too strongly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jtmcdole say it should be part of RFC

@loic-sharma loic-sharma Nov 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK sounds good 👍


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. |

@loic-sharma loic-sharma Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doc uses both webhook_url/keys_url/status_url and partner_webhook_url/partner_keys_url/partner_status_url terminology. What do you think of being consistent and using the partner_ prefix everywhere? This also has the benefit of making it easier for partners to keep track of which URLs are partner URLs, and which URLs are Cocoon URLs.


**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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We haven't explained what message and details[].url are yet. What do you think of moving this content to both the partner status URL section and the Cocoon callback endpoint section?


### 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": "<key-id>"
}
```

* **Verifying Requests from Cocoon (Partner Inbound):**
1. Extract the JWT from the HTTP request body (`POST <webhook_url>`) or the `Authorization: Bearer <Signed JWT>` header (`GET <status_url>`).
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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we provide guidance to partners on cache miss cooldown? If a partner forgets this, an attacker could send random kids to the partner to make the partner hammer Cocoon's endpoint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added JWKS Caching & Cache-Miss Cooldown (DoS Prevention) recomendation

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 <webhook_url> (Content-Type: application/jwt)<br/>Signed JWT with uid=abcd, sha=wxyz, branch, pr, slug
opt Public key for kid not in Partner cache
Partner->>Cocoon: HTTP GET <cocoon_keys_url>
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)<br/>Signed JWT with uid=abcd, status="in_progress", details[]
opt Public key for kid not in Cocoon cache
Cocoon->>Partner: HTTP GET <partner_keys_url>
Partner-->>Cocoon: 200 OK (Partner JWKS)
end
Cocoon-->>-Partner: 200 OK
end

opt 3. Optional Manual Status Check (Cocoon -> Partner)
Cocoon->>+Partner: HTTP GET <status_url>?uid=abcd<br/>Authorization: Bearer <Signed JWT>
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)<br/>Signed JWT with uid=abcd, terminal status, message, details[]
opt Public key for kid not in Cocoon cache
Cocoon->>Partner: HTTP GET <partner_keys_url>
Partner-->>Cocoon: 200 OK (Partner JWKS)
end
Cocoon-->>-Partner: 200 OK

opt 5. Task Re-run Request (Cocoon -> Partner)
Cocoon->>+Partner: HTTP POST <webhook_url> (Content-Type: application/jwt)<br/>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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I really like this section, it's a great intro to the protocol. What do you think of making this the second section, after the configuration section? I think that might help a reader get a good high-level understanding before we drill down into specifics like authentication. Just a thought, up to you.


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`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we quickly mention the uid here? Steps 2/3/5 mention the uid, but it might not be obvious to partners that this is where that uid first comes from.

* 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 <status_url>?uid=<uid>` with `Authorization: Bearer <Signed JWT>`, 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 <webhook_url>` 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 <partner_keys_url>`
* **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": "<Base64url-encoded Ed25519 public key>"
}
]
}
```

##### 9.4.1.2. Partner Webhook Listener Endpoint (Required)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you skip the workflow summary section, it might not be immediately obvious what this endpoint should do. Could we add a * **Summary:** ... to each endpoint that copies the relevant information from the workflow summary section?


* **URL:** `POST <partner_webhook_url>`
* **Authentication:** Request body is a raw JWT signed by Cocoon (`iss: "cocoon"`, `aud: "<partner_webhook_url>"`).
* **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`.

@loic-sharma loic-sharma Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we specify that partners must fail if the version is unrecognized? This would make it easier for us to do breaking changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added

* **HTTP Request:**

```http
POST /webhook HTTP/1.1
Host: partner-ci.example.com
Content-Type: application/jwt

<Base64Url JWT Header>.<Base64Url JWT Payload>.<Base64Url Signature>
```

* **Decoded JWT Payload (`v1`):**

```json
{
"iss": "cocoon",
"aud": "https://partner-ci.example.com/webhook",
"iat": 1774962000,
"exp": 1774962300,
"version": "v1",
"data": {

@loic-sharma loic-sharma Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For all keys under the data object can we document:

  • The type of the key (string, integer, etc)
  • Whether the key is required or optional. (Is pr optional?)
  • A quick description of the value
  • If the value is an enum, the allowed values, and what the partner should do if the value is unknown

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same comment for all other endpoints :)

"uid": "0sAtLhwbmGz2Y0WBDzPL",
"slug": "flutter/flutter",
"branch": "master",
"pr": 12345,
"sha": "6f0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}
}
```

* **Expected Partner HTTP Response Codes:**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we specify that the partner should return a HTTP status 400 if it doesn't not recognize the version?

@loic-sharma loic-sharma Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we specify the HTTP response body as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be nice if partners can use the body to provide debugging information in the event of failures? Something like:

{
  "message": "Hello world"
}

This would be a slight amendment to the RFC, but it seems like a change worth considering.


| 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). |

##### 9.4.1.3. Partner On-Demand Status Endpoint (Optional)

* **URL:** `GET <partner_status_url>?uid=<uid>`
* **Authentication:** `Authorization: Bearer <Signed JWT>` 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 <Base64Url JWT Header>.<Base64Url JWT Payload>.<Base64Url Signature>
```

* **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`):**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What are the response codes here for different failure scenarios?

  • uid doesn't exist
  • Client or authentication error
  • Temporary overload, timeout, or server error.

Also, it might be worth specifying that you should return a 200 even if the test failed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added Expected Partner HTTP Response Codes section


```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"
}
]
}
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we repeat the PII / public logs warning here as well?

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added


* **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

* **URL:** `GET <cocoon_keys_url>`

@loic-sharma loic-sharma Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we add a real URL here? Maybe something like GET https://dashboard.flutter.dev/api/v1/partner/keys?

* **Purpose:** Fetched and cached by the partner to verify Ed25519 JWT signatures on inbound `POST <webhook_url>` and `GET <status_url>` 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: "<partner>"`, `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

<Base64Url JWT Header>.<Base64Url JWT Payload>.<Base64Url Signature>
```

* **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. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If flaky tests failed and tests were skipped, should the partner return neutral or skipped?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

its up to partner, I think

| `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. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The partner can send a start status callback with the in_progress status. What happens if they send multiple in_progress statuses? Will we accept just the first one and return 409 Conflicts for subsequent callbacks? If yes, let's call that out here as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added: or a duplicate in_progress status

| `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. |
Loading