diff --git a/docs.json b/docs.json index e6e9e950..0f0029fa 100644 --- a/docs.json +++ b/docs.json @@ -830,6 +830,89 @@ } ] }, + { + "tab": "Merge Queue API v2", + "hidden": true, + "groups": [ + { + "group": "Merge Queue API (v2)", + "root": "merge-queue/reference/api-v2", + "openapi": { + "source": "/openapi-v2.json", + "directory": "/merge-queue/reference/api-v2" + }, + "pages": [ + "merge-queue/reference/api-v2/migrate-from-v1", + { + "group": "Merge queues", + "pages": [ + "GET /v2/merge-queues", + "POST /v2/merge-queues", + "GET /v2/merge-queues/{id}", + "PATCH /v2/merge-queues/{id}/config", + "POST /v2/merge-queues/{id}/switch-modes", + "POST /v2/merge-queues/{id}/pause", + "POST /v2/merge-queues/{id}/resume", + "POST /v2/merge-queues/{id}/drain", + "DELETE /v2/merge-queues/{id}" + ] + }, + { + "group": "Queue order", + "pages": [ + "GET /v2/merge-queues/{id}/order", + "GET /v2/merge-queues/{id}/graph" + ] + }, + { + "group": "Pull requests", + "pages": [ + "POST /v2/merge-queues/{id}/pull-requests", + "GET /v2/merge-queues/{id}/pull-requests", + "GET /v2/pull-requests", + "GET /v2/pull-requests/{id}", + "POST /v2/pull-requests/{id}/reprioritize", + "PATCH /v2/pull-requests/{id}/queue-entry", + "POST /v2/pull-requests/{id}/restart-tests", + "POST /v2/pull-requests/{id}/dequeue", + "GET /v2/pull-requests/{id}/queue-entry-history", + "GET /v2/queue-entry-history" + ] + }, + { + "group": "Testing", + "pages": [ + "GET /v2/merge-queues/{id}/verification-runs", + "GET /v2/pull-requests/{id}/verification-runs", + "GET /v2/merge-queue-verification-runs/{id}", + "GET /v2/merge-queues/{id}/testing-results" + ] + }, + { + "group": "Impacted targets", + "pages": [ + "PUT /v2/impacted-targets", + "merge-queue/reference/api-v2/github-actions" + ] + }, + { + "group": "Metrics", + "pages": [ + "GET /v2/merge-queues/metrics", + "GET /v2/merge-queues/{id}/metrics" + ] + }, + { + "group": "Repositories", + "pages": [ + "GET /v2/repositories", + "GET /v2/repositories/{id}" + ] + } + ] + } + ] + }, { "tab": "Changelog", "hidden": true, diff --git a/merge-queue/reference/api-v2.mdx b/merge-queue/reference/api-v2.mdx new file mode 100644 index 00000000..ac8418b9 --- /dev/null +++ b/merge-queue/reference/api-v2.mdx @@ -0,0 +1,196 @@ +--- +title: "Merge Queue API (v2)" +description: "REST API for merge queues, the pull requests in them, and the testing they run." +og:title: "Trunk Merge Queue API v2 reference" +--- +The v2 Merge Queue API lets you manage merge queues and the pull requests in them, read the order the queue will test them in, and inspect every verification run. It is an HTTP REST API hosted at `https://api.trunk.io/v2` that returns JSON and uses standard HTTP status codes. + +v2 addresses everything by ID rather than by repository and branch coordinates, pages every list the same way, and returns one error shape. It sits alongside the [v1 API](/merge-queue/reference/merge), which keeps working unchanged. To move existing automation or dashboards to v2, see [Migrate from the v1 API](/merge-queue/reference/api-v2/migrate-from-v1). + +## Authentication + +Authenticate automation with an API key, sent as `Authorization: Bearer `: + +```bash +curl https://api.trunk.io/v2/merge-queues \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + +To upload impacted targets from CI, you don't need an API key at all: a GitHub Actions job can [log in with the run's own GitHub credential](/merge-queue/reference/api-v2/github-actions), pull requests from forks included. + +### Create an API key + +An organization admin creates API keys in the [Trunk app](https://app.trunk.io): + +1. Go to **Settings** → **Developer** → **API Keys** and click **Create Key**. +2. Name the key after what will use it, and choose the permissions it holds. +3. Copy the key. It is shown only once — if you lose it, revoke it and create another. + +You can change a key's permissions later, and the change applies to the next request that uses it. + +### Permissions + +A key with no permissions can read merge queues but change nothing. Grant only what the automation needs: + +| Permission | Allows | +| --- | --- | +| **Merge and cancel PRs** | Submit pull requests to the queue, restart their tests, remove any entry from it, and upload impacted targets from CI. | +| **Set priority on PRs** | Move a pull request ahead of others in the queue. | +| **Skip batching** | Opt a pull request out of batching so it gets a test run of its own. | +| **Merge PRs with direct to queue** | Send a pull request to the queue before branch protection is satisfied. The queue's own tests still run and still gate the merge. | +| **Update Merge Queue settings** | Change queue settings; pause, resume, or drain a queue; and create or delete queues. | + +A request the key isn't permitted returns `403 INSUFFICIENT_PERMISSIONS`. + + +These permissions apply to merge queues only. Every API key can use the Flaky Tests and Dynamic CI endpoints, including the ones that make changes, whatever permissions it holds. + + +## Finding IDs + +Every merge queue, pull request, repository, and verification run has an `id`, and endpoints address resources by it. Resolve IDs once and reuse them: + +1. **Queue:** [`GET /v2/merge-queues`](/merge-queue/reference/api-v2/mergequeues/list-merge-queues) needs no arguments and returns every queue in your organization, each with its `id`, `repositoryId`, `repositoryUrl`, and `targetBranch`. To find one repository's queues, first resolve the repository with [`GET /v2/repositories?providerUrl=…`](/merge-queue/reference/api-v2/repositories/resolve-a-repository-by-its-provider-url). +2. **Pull request:** [`GET /v2/pull-requests?repositoryId=…&number=…`](/merge-queue/reference/api-v2/pullrequests/resolve-pull-requests-by-repository-coordinates) resolves up to 50 pull request numbers at once, or a head `branch`. + +```bash +curl "https://api.trunk.io/v2/pull-requests?repositoryId=7c9e6679-7425-40de-944b-e07fc1f90ae7&number=4901" \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + +Submitting a pull request is the exception: it takes the pull request's `number` in the body, so you only need the queue's ID. + +```bash +curl -X POST https://api.trunk.io/v2/merge-queues/xK9mP2nQ/pull-requests \ + -H "Authorization: Bearer $TRUNK_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "number": 4901 }' +``` + +## Pagination + +Every list endpoint returns the same envelope: + +```json +{ + "data": [], + "nextCursor": "AQEKZm9vYmFy", + "hasMore": true +} +``` + +Pass `nextCursor` back verbatim as `cursor` to get the next page, until `hasMore` is `false`. Cursors are opaque, forward-only, and bound to the endpoint and filters they came from. `limit` sets the page size (1–100). Set `includeTotal=true` to add a `total` count, which costs an extra query and is off by default. + +**List order is not queue order.** A queue's pull request list is most recently enqueued first. For the order the queue will test pull requests in, use [`GET /v2/merge-queues/{id}/order`](/merge-queue/reference/api-v2/mergequeues/get-a-merge-queues-dispatch-order). + +## Errors + +Every 4xx and 5xx response has the same shape: + +```json +{ + "error": { + "code": "MERGE_QUEUE_NOT_FOUND", + "message": "No merge queue found with ID xK9mP2nQ" + } +} +``` + +Switch on `code`, which is stable. `message` is for people and may change at any time. + +- **`404` also means "not yours".** A read of a resource in another organization returns `404`, not `403`, so the API never reveals whether it exists. +- **`403 INSUFFICIENT_PERMISSIONS`** means your credential is valid but not permitted to take that action. +- **`429`** means you exceeded the rate limit of 100 requests per second per organization. Retry with backoff. A `429` may not carry the error envelope, so treat any `429` as rate limiting. + +Every response carries an `X-Request-Id` header. Include it when you contact support. + +## Retrying writes + +`Idempotency-Key` is not honored yet. If a write times out or its outcome is unknown, read the resource back before retrying — a blind retry can, for example, restart a pull request's tests twice. Each write's reference page describes how to reconcile it. + +## Compatibility + +Within v2, Trunk may add endpoints, optional request parameters, optional response fields, and new enum values — including new error codes and queue-entry states. **Tolerate values and fields you don't recognize.** A client that switches exhaustively over an enum will break when a member is added. Removals and changes in meaning require a new major version. + +## Limitations + +- Merge queues, verification runs, and checks are **GitHub-only**. Impacted-target uploads also require a `github.com` repository named `owner/repo`. +- The API has no webhooks of its own: monitor it by polling, or use [Merge Queue webhooks](/merge-queue/webhooks). + +## Endpoints + +### Merge queues + + + + + + + + + + + + + +### Queue order + +Which pull requests the queue will test next, and which ones it considers related. Use the order to answer "what runs next"; the graph explains why pull requests depend on each other, but not when they run. + + + + + + +### Pull requests + + + + + + + + + + + + + + +A pull request's queue entry moves through the states `notReady`, `pending`, `testing`, `testsPassed`, `pendingFailure`, and ends in `merged`, `failed`, or `cancelled`. To see why an entry changed state — for example, why it keeps being re-tested — read its queue-entry history and inspect each transition's `reason`. + +### Testing + +A verification run is one CI run the queue started to test one pull request or a batch of them. + + + + + + + + +### Impacted targets + +For queues running [parallel queues](/merge-queue/optimizations/parallel-queues), CI uploads the build targets each pull request impacts. + + + + + + +### Metrics + +Prometheus text exposition, with the same metric names and labels as v1's [`getMergeQueueMetrics`](/merge-queue/reference/merge/get-prometheus-format-metrics-for-a-merge-queue) — an existing scrape config or dashboard needs only its URL and credential changed. See [Prometheus metrics](/merge-queue/administration/metrics#prometheus-metrics-endpoint) for the metrics and example queries. + + + + + + +### Repositories + + + + + diff --git a/merge-queue/reference/api-v2/github-actions.mdx b/merge-queue/reference/api-v2/github-actions.mdx new file mode 100644 index 00000000..a6593b85 --- /dev/null +++ b/merge-queue/reference/api-v2/github-actions.mdx @@ -0,0 +1,77 @@ +--- +title: "Upload impacted targets from GitHub Actions" +description: "Authenticate a GitHub Actions job to the v2 API with no Trunk secret, pull requests from forks included." +og:title: "Upload impacted targets from GitHub Actions with the Trunk Merge Queue API" +--- +A GitHub Actions job can [upload a pull request's impacted targets](/merge-queue/reference/api-v2/impactedtargets/upload-a-pull-requests-impacted-targets) without a Trunk API key. The [`trunk-io/login`](https://github.com/trunk-io/login) action exchanges the run's own GitHub credential for a short-lived Trunk login, so there is no secret to create, store, or rotate — and it works on pull requests from forks, where repository secrets are not available. + +The login can **only** upload impacted targets, and only for the repository the run belongs to. Every other endpoint refuses it. For anything else, use an [API key](/merge-queue/reference/api-v2#create-an-api-key). + +## Set up the workflow + +```yaml +on: + pull_request: + +permissions: + contents: read + id-token: write # the run's OIDC token, for pull requests from this repository + +jobs: + impacted-targets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # ... compute the impacted targets into targets.txt, one per line ... + + - uses: trunk-io/login@v1 + + - run: trunk mergequeue upload-impacted-targets --targets-file targets.txt + env: + GITHUB_TOKEN: ${{ github.token }} # lets a long fork-PR job renew its login +``` + +`trunk-io/login` installs the `trunk` CLI, puts it on `PATH`, and logs in. On a pull request, `trunk mergequeue upload-impacted-targets` takes the repository, pull request number, head commit, and target branch from the run. + +Declare the pull request's impact with one of: + +| Option | Uploads | +| --- | --- | +| `--targets-file ` | The target names in the file, one per line. A file with no targets is refused, because an empty file is more often a failed computation than a real answer. | +| `--none` | An empty list: the pull request impacts nothing. This still satisfies the queue's impacted-targets check. | +| `--all` | The pull request impacts every target. | + +## How the job authenticates + +The CLI picks the credential from the run. + +| Run | Credential | Needs | +| --- | --- | --- | +| `pull_request` from this repository, `merge_group`, `push`, `workflow_dispatch` | The run's GitHub OIDC token | `permissions: id-token: write` | +| `pull_request` from a fork, `pull_request_target` | The job's `GITHUB_TOKEN`, bound to that pull request and its head commit | **Fork PR CI access** turned on for the repository in Trunk, by an organization admin (**Settings** → **Repositories**) | + +- The CLI sends `GITHUB_TOKEN` to Trunk only on a pull request from a fork. +- A fork pull request's login is bound to that pull request and its head commit, and can upload only for them. +- Other events, such as `issue_comment`, `workflow_run`, and `schedule`, are refused: some of them let someone outside the repository run code with the repository's OIDC token. +- GitHub may hold a first-time contributor's run until a maintainer approves it. + +## Security + +- Nothing is exported to the job's environment or to later steps. The login is saved to the CLI's credential file (mode `0600`) and used only by the upload. +- The login expires after at most 15 minutes. The CLI renews it when it is close to expiring. On a fork pull request, renewing needs `GITHUB_TOKEN` in the upload step's `env`, as in the example above. +- Each login is tied to the run that made it, so a later job on a reused self-hosted runner does not pick it up. + +## Action inputs + +| Input | Default | Description | +| --- | --- | --- | +| `api-url` | `https://api.trunk.io/v2` | The Trunk API root. | +| `cli-version` | Pinned per release | The `trunk` CLI version to install. | +| `cli-channel` | `prod` | The release channel `cli-version` is published on. | +| `github-token` | `${{ github.token }}` | The job's GitHub token. Sent to Trunk only on a pull request from a fork. | + +## Requirements + +- Linux (x64 or arm64) or macOS (arm64) runners. Windows is not supported. +- The repository is connected to Trunk: the Trunk GitHub App is installed on it. diff --git a/merge-queue/reference/api-v2/migrate-from-v1.mdx b/merge-queue/reference/api-v2/migrate-from-v1.mdx new file mode 100644 index 00000000..cbc51deb --- /dev/null +++ b/merge-queue/reference/api-v2/migrate-from-v1.mdx @@ -0,0 +1,338 @@ +--- +title: "Migrate from the v1 API" +description: "Move Merge Queue automation and dashboards from the v1 API to v2, endpoint by endpoint." +og:title: "Migrate from the Trunk Merge Queue v1 API to v2" +--- +If you automate Merge Queue — submitting pull requests from CI, pausing queues during an incident, or building dashboards from queue metrics — with the [v1 API](/merge-queue/reference/merge), this guide shows how to do each of those things with [v2](/merge-queue/reference/api-v2). + +Every v1 capability has a v2 equivalent. Most migrations are four changes: a new credential, a new base URL, IDs instead of repository coordinates, and camelCase enum values. + +## Migrate with an agent + +This page is written so a coding agent can do the migration for you. Give your agent this prompt from the repository that calls the v1 API: + +```text +Migrate this codebase's Trunk Merge Queue API calls from v1 (https://api.trunk.io/v1) to v2 (https://api.trunk.io/v2). + +1. Read the migration guide: https://docs.trunk.io/merge-queue/reference/api-v2/migrate-from-v1.md +2. Find every v1 call in this codebase. Look for api.trunk.io/v1, the x-api-token header, and the v1 endpoint names in the guide's endpoint map. +3. Port each call using the endpoint map. Before writing each v2 request, read that endpoint's reference page as Markdown for the exact parameters and response fields: the guide's links are relative to https://docs.trunk.io, so fetch https://docs.trunk.io.md. +4. Port response parsing, not just requests. v1's flat pull request record is split in v2: pull request fields at the top level, queue fields under queueEntry, and id now identifies the pull request. Timelines and testing results also change shape. Use the guide's "Renamed values" section. +5. Use the guide's tables for enum values and renamed fields. Never derive a v2 enum value by changing the case of a v1 value. +6. Every v2 list is paginated with a default page size of 25. Set limit explicitly (up to 100) and follow nextCursor until hasMore is false, including for batch lookups of up to 50 IDs. +7. Resolve queue and pull request IDs once and reuse them, rather than looking them up on every request. +8. When you're done, list the API key permissions the migrated code needs, using the guide's permissions table, and work through the guide's checklist. +``` + +You can also open this page in Claude or ChatGPT from the page menu, or copy it as Markdown. Every page on this site is available as Markdown by appending `.md` to its URL, and each endpoint's Markdown includes its OpenAPI definition. + +## What changes + +| | v1 | v2 | +| --- | --- | --- | +| Base URL | `https://api.trunk.io/v1` | `https://api.trunk.io/v2` | +| Credential | `x-api-token` header | An API key, sent as `Authorization: Bearer ` | +| Addressing | `repo` + `targetBranch` + `pr.number` in every request body | IDs in the path: a queue ID, a pull request ID | +| Methods | `POST` for everything except metrics | `GET` to read, `POST`/`PATCH`/`PUT`/`DELETE` to change | +| Pull request records | One flat record whose `id` is the queue entry | Pull request fields at the top level, whose `id` is the pull request; queue fields under `queueEntry` | +| Enum values | `snake_case` (`tests_passed`, `merge_commit`), and uppercase was accepted | camelCase only (`testsPassed`, `mergeCommit`) | +| Lists | Endpoint-specific `take`, `offset`, or `cursor` | One envelope: `data`, `nextCursor`, `hasMore`, with `limit` (default 25, up to 100) and `cursor` | +| Errors | Varies by endpoint | Always `{ "error": { "code", "message" } }` | + +## Step 1: Create an API key + +An organization admin creates API keys in the [Trunk app](https://app.trunk.io) under **Settings** → **Developer** → **API Keys** → **Create Key**. The key is shown once, so store it in your secret manager right away. See [Create an API key](/merge-queue/reference/api-v2#create-an-api-key) for details. + +A key holds only the permissions you choose, so grant the ones your v1 calls need: + +| If your code calls | The key needs | +| --- | --- | +| Only reads: `getSubmittedPullRequest(s)`, `listPullRequests`, `getQueue`, `getMergeQueueTestingDetails`, `listMergeQueueTestingResults`, `getMergeQueueMetrics` | No permissions | +| `submitPullRequest`, `cancelPullRequest`, `restartTestsOnPullRequest`, `setImpactedTargets` | **Merge and cancel PRs** | +| `submitPullRequest` with `priority`; in v2, also `reprioritize`, or `PATCH …/queue-entry` with a priority | **Set priority on PRs** | +| `submitPullRequest` with `noBatch: true`; in v2, also `PATCH …/queue-entry` with `allowBatching` | **Skip batching** | +| `direct` on a v2 submit or queue-entry update (new in v2) | **Merge PRs with direct to queue** | +| `createQueue`, `updateQueue`, `deleteQueue` | **Update Merge Queue settings** | + +A dashboard needs a key with no permissions at all. If CI only uploads impacted targets from GitHub Actions, it doesn't need a key: [log in with the run's own credential](/merge-queue/reference/api-v2/github-actions) instead, which also works on pull requests from forks. + +## Step 2: Switch the credential and base URL + + + +```bash v1 +curl -X POST https://api.trunk.io/v1/getQueue \ + -H "Content-Type: application/json" \ + -H "x-api-token: $TRUNK_TOKEN" \ + -d '{ "repo": { "host": "github.com", "owner": "acme", "name": "widgets" }, "targetBranch": "main" }' +``` + +```bash v2 +curl https://api.trunk.io/v2/merge-queues/xK9mP2nQ \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + + + +## Step 3: Resolve IDs once + +v2 addresses queues and pull requests by ID. Look each one up once and store it — IDs don't change. + +**Queue and repository IDs.** List your organization's queues. Each carries `repositoryUrl` and `targetBranch`, so you can match the repository and branch your v1 code sends, and its `repositoryId`, which you'll need for pull request lookups: + +```bash +curl "https://api.trunk.io/v2/merge-queues?limit=100" \ + -H "Authorization: Bearer $TRUNK_API_KEY" | + jq '.data[] | select(.repositoryUrl == "https://github.com/acme/widgets" and .targetBranch == "main") | {id, repositoryId}' +``` + +If `hasMore` is `true`, follow `nextCursor`, or narrow the list to one repository: resolve its ID with [`GET /v2/repositories?providerUrl=https://github.com/acme/widgets`](/merge-queue/reference/api-v2/repositories/resolve-a-repository-by-its-provider-url) (a list; the ID is `.data[0].id`), then call `GET /v2/merge-queues?repositoryId=…&targetBranch=main`. + +**Pull request ID.** Resolve a number within the repository — or up to 50 comma-separated numbers, with `limit` set to at least as many: + +```bash +curl "https://api.trunk.io/v2/pull-requests?repositoryId=$REPOSITORY_ID&number=4901" \ + -H "Authorization: Bearer $TRUNK_API_KEY" | + jq -r '.data[0].id' +``` + +Submitting a pull request is the exception: it takes the pull request's `number` in the body, so you only need the queue ID. The response is the pull request, including its `id`, so a CI job can poll it without a separate lookup. + +## Endpoint map + +| v1 | v2 | Notes | +| --- | --- | --- | +| `POST /submitPullRequest` | [`POST /v2/merge-queues/{id}/pull-requests`](/merge-queue/reference/api-v2/mergequeues/submit-a-pull-request-to-a-merge-queue) | Body: `number`, plus optional `priorityValue` or `priorityName`, `allowBatching`, `direct`. Resubmitting a queued pull request returns `409 PR_ALREADY_ENQUEUED` instead of updating it — see [behavior changes](#behavior-changes). | +| `POST /cancelPullRequest` | [`POST /v2/pull-requests/{id}/dequeue`](/merge-queue/reference/api-v2/pullrequests/dequeue-a-pull-request) | An entry that already merged, failed, or was cancelled returns `409 PR_NOT_IN_ACTIVE_STATE`. | +| `POST /restartTestsOnPullRequest` | [`POST /v2/pull-requests/{id}/restart-tests`](/merge-queue/reference/api-v2/pullrequests/restart-a-queued-pull-requests-tests) | | +| `POST /getSubmittedPullRequest` | [`GET /v2/pull-requests?repositoryId=…&number=…`](/merge-queue/reference/api-v2/pullrequests/resolve-pull-requests-by-repository-coordinates), then [`GET /v2/pull-requests/{id}`](/merge-queue/reference/api-v2/pullrequests/get-a-pull-request) | By head branch: `?repositoryId=…&branch=…&baseBranch=…`. The queue state is `queueEntry.state`. A pull request that was never submitted is still found, without `queueEntry` — see [behavior changes](#behavior-changes). | +| `POST /getSubmittedPullRequest` with `includeTimeline` | [`GET /v2/pull-requests/{id}/queue-entry-history?direction=newest`](/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-queue-entry-state-history) | `direction=newest` gives v1's order. The default is oldest first. | +| `POST /getSubmittedPullRequests` | [`GET /v2/pull-requests?repositoryId=…&number=1,2,3`](/merge-queue/reference/api-v2/pullrequests/resolve-pull-requests-by-repository-coordinates) | Up to 50 numbers. Set `limit=50` (the default page is 25) and still follow `nextCursor`. There is no `notFound` list — see [behavior changes](#behavior-changes). | +| `POST /getSubmittedPullRequests` with `includeTimeline` | [`GET /v2/queue-entry-history?pullRequestIds=…&direction=newest`](/merge-queue/reference/api-v2/pullrequests/list-queue-entry-state-history-for-several-pull-requests) | Up to 50 pull request IDs, each with its complete timeline. `limit` counts pull requests and defaults to 25, so set `limit=50` to get them in one call. | +| `POST /listPullRequests` | [`GET /v2/merge-queues/{id}/pull-requests`](/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-pull-requests) | `state` takes comma-separated camelCase values. `since`/`until` become `concludedAfter`/`concludedBefore`. `take` becomes `limit`. | +| `POST /getQueue` (state and configuration) | [`GET /v2/merge-queues/{id}`](/merge-queue/reference/api-v2/mergequeues/get-a-merge-queue) | Configuration is under `config`. | +| `POST /getQueue` (`enqueuedPullRequests`) | [`GET /v2/merge-queues/{id}/order`](/merge-queue/reference/api-v2/mergequeues/get-a-merge-queues-dispatch-order) | Split into `batches`, `pending`, and `awaitingAdmission`, with only each pull request's ID, number, title, and head commit — see [behavior changes](#behavior-changes). | +| `POST /updateQueue` (settings) | [`PATCH /v2/merge-queues/{id}/config`](/merge-queue/reference/api-v2/mergequeues/update-a-merge-queues-configuration) | Send only the settings you change; any other key, including `repo` or `targetBranch`, is a `400`. See [renamed settings](#renamed-settings). | +| `POST /updateQueue` with `state` | [`POST /v2/merge-queues/{id}/pause`](/merge-queue/reference/api-v2/mergequeues/pause-a-merge-queue), [`/resume`](/merge-queue/reference/api-v2/mergequeues/resume-a-merge-queue), or [`/drain`](/merge-queue/reference/api-v2/mergequeues/drain-a-merge-queue) | `running` is `/resume`. | +| `POST /updateQueue` with `mode` | [`POST /v2/merge-queues/{id}/switch-modes`](/merge-queue/reference/api-v2/mergequeues/switch-a-merge-queues-mode) | `single` is `queue`; `parallel` is `graph`. | +| `POST /createQueue` | [`POST /v2/merge-queues`](/merge-queue/reference/api-v2/mergequeues/create-a-merge-queue) | Body: `repositoryId`, `targetBranch`, and optional settings such as `mode` (`queue` or `graph`) and `concurrency`. | +| `POST /deleteQueue` | [`DELETE /v2/merge-queues/{id}`](/merge-queue/reference/api-v2/mergequeues/delete-a-merge-queue) | The queue must still be empty. | +| `POST /getMergeQueueTestingDetails` | [`GET /v2/merge-queue-verification-runs/{id}`](/merge-queue/reference/api-v2/mergequeueverificationruns/get-a-verification-run) | v1's test run is a verification run, with the same ID — see [finding a verification run](#finding-a-verification-run). | +| `POST /listMergeQueueTestingResults` | [`GET /v2/merge-queues/{id}/testing-results?since=…&until=…`](/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-completed-testing) | At most 45 days per request. `offset`/`take` become `cursor`/`limit`. Result fields are renamed — see [testing results](#testing-results). | +| `GET /getMergeQueueMetrics` | [`GET /v2/merge-queues/metrics`](/merge-queue/reference/api-v2/mergequeues/get-prometheus-metrics-for-every-merge-queue) or [`GET /v2/merge-queues/{id}/metrics`](/merge-queue/reference/api-v2/mergequeues/get-prometheus-metrics-for-a-merge-queue) | Same metric names and labels. `repo` and `repoHost` become `repositoryId`. Metrics have their own limit of 5 requests per second per organization. | +| `POST /setImpactedTargets` | [`PUT /v2/impacted-targets`](/merge-queue/reference/api-v2/impactedtargets/upload-a-pull-requests-impacted-targets) | Body: `repositoryId`, `targetBranch`, `number`, `sha`, and `impactedTargets` — or `impactsAllTargets: true` instead of `"ALL"`. Returns `202`. | +| `POST /setImpactedTargets` from a fork, with `x-forked-workflow-run-id` | [GitHub Actions login](/merge-queue/reference/api-v2/github-actions) | No header or secret: `trunk-io/login` authenticates the fork's run. | + +## Examples + +### Automate queue actions + +Submit a pull request with a priority, from CI: + + + +```bash v1 +curl -X POST https://api.trunk.io/v1/submitPullRequest \ + -H "Content-Type: application/json" \ + -H "x-api-token: $TRUNK_TOKEN" \ + -d '{ + "repo": { "host": "github.com", "owner": "acme", "name": "widgets" }, + "targetBranch": "main", + "pr": { "number": 4901 }, + "priority": "high" + }' +``` + +```bash v2 +curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pull-requests \ + -H "Authorization: Bearer $TRUNK_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "number": 4901, "priorityName": "high" }' +``` + + + +v1's `priority` took either a number or a name. In v2, send a number as `priorityValue` or a name as `priorityName`. + +Pause a queue during an incident, and resume it afterward: + + + +```bash v1 +curl -X POST https://api.trunk.io/v1/updateQueue \ + -H "Content-Type: application/json" \ + -H "x-api-token: $TRUNK_TOKEN" \ + -d '{ "repo": { "host": "github.com", "owner": "acme", "name": "widgets" }, "targetBranch": "main", "state": "paused" }' +``` + +```bash v2 +curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pause \ + -H "Authorization: Bearer $TRUNK_API_KEY" + +curl -X POST https://api.trunk.io/v2/merge-queues/$QUEUE_ID/resume \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + + + +Check whether a pull request merged: + +```bash +curl https://api.trunk.io/v2/pull-requests/$PULL_REQUEST_ID \ + -H "Authorization: Bearer $TRUNK_API_KEY" | + jq -r '.queueEntry.state' # notReady, pending, testing, testsPassed, pendingFailure, merged, failed, or cancelled +``` + +### Build dashboards + +**Prometheus.** Point your scrape job at the v2 URL and send the API key. The metric names and labels are unchanged, so existing dashboards and alerts keep working. Metrics are limited to 5 requests per second per organization, so prefer one organization-wide target over one per queue: + +```yaml +scrape_configs: + - job_name: trunk-merge-queue + scheme: https + metrics_path: /v2/merge-queues/metrics + authorization: + type: Bearer + credentials_file: /etc/prometheus/trunk-api-key + static_configs: + - targets: ["api.trunk.io"] +``` + +**Completed testing over a time range.** Page through `testing-results` with `nextCursor`. Each row carries `pullRequestNumber`, `testingDurationMs`, `outcome`, and the `verificationRunId` for its details. For more than 45 days, split the range into several requests: + +```bash +curl "https://api.trunk.io/v2/merge-queues/$QUEUE_ID/testing-results?since=2026-09-01T00:00:00Z&until=2026-09-08T00:00:00Z&limit=100" \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + +**Pull requests that merged or failed in a window.** `concludedAfter` and `concludedBefore` replace v1's `since` and `until`: + +```bash +curl "https://api.trunk.io/v2/merge-queues/$QUEUE_ID/pull-requests?state=merged,failed&concludedAfter=2026-09-01T00:00:00Z&limit=100" \ + -H "Authorization: Bearer $TRUNK_API_KEY" +``` + +**Paging.** Every v2 list pages the same way. Repeat the request with `cursor` set to the previous response's `nextCursor` until `hasMore` is `false`. Cursors are tied to the filters they came from, so keep the other parameters the same. + +## Renamed values + +v2 enum values are camelCase. Most follow mechanically from v1, but some drop or change words, so map them with these tables rather than by changing case. + +**Queue entry states.** v1 accepted these in either case; v2 accepts only the camelCase value: + +| v1 | v2 | +| --- | --- | +| `not_ready` | `notReady` | +| `pending` | `pending` | +| `testing` | `testing` | +| `tests_passed` | `testsPassed` | +| `pending_failure` | `pendingFailure` | +| `merged` | `merged` | +| `failed` | `failed` | +| `cancelled` | `cancelled` | + +v2 rejects any other spelling — including `TESTS_PASSED` or `tests_passed` — with `400`. + +**Queue.** State `switching_modes` becomes `switchingModes`; `running`, `paused`, and `draining` are unchanged. Mode `single` becomes `queue`, and `parallel` becomes `graph` — mode is a write, so a wrong value reconfigures the queue rather than failing. + +### Pull request records + +v1's `getSubmittedPullRequest` returned one flat record. v2 splits it in two: + +- **Top level — the pull request.** `id` is the pull request's ID. `prNumber` → `number`, `prTitle` → `title`, `prSha` → `sha`, `prBaseBranch` → `baseBranch`, `prAuthor` → `author`. `isCurrentlySubmittedToQueue` is unchanged. +- **`queueEntry` — its place in the queue.** v1's `id` is `queueEntry.id`. `state`, `stateChangedAt`, `priorityValue`, `priorityName`, `usedDefaultPriorityName`, and `readiness` move here unchanged, and `skipTheLine` → `skipsTheLine` (read-only; send `priorityValue: 0` to skip the line), `forceEnqueued` → `direct`, `verifiedByTestRun` → `verifiedByVerificationRunId`, `noBatch` → `allowBatching` (inverted: `noBatch: true` is `allowBatching: false`). +- **Readiness.** `readiness.gitHubMergeability` → `readiness.mergeability`, with values `unspecified`, `inProgress`, `mergeable`, `notMergeable`. The new `readiness.mergeabilityDetails.reasons[]` explains why, using the same `code` values as `blockReasons` below. + +### Timelines + +v1's `timeline` is now the [queue-entry history](/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-queue-entry-state-history). Its shape changes as well as its values, so a ported parser compiles and then silently misses: + +| v1 | v2 | +| --- | --- | +| `timestamp` | `changedAt` | +| `failure` | `failed` | +| Each state's `reason` and `actor` (`notReady.reason`, `cancellation.reason`, `merged.actor`, …) | `reason` and `actor` on the transition itself. The `cancellation` and `merged` objects are gone. | +| `testRun` (branch, sha, status, required statuses, tested pull requests) and `testsPassed.testRunId`, `testBranchSha`, `testBranchUrl` | `verificationRunId` on the transition. Read the run with [`GET /v2/merge-queue-verification-runs/{id}`](/merge-queue/reference/api-v2/mergequeueverificationruns/get-a-verification-run) — one request per run. | +| `failureCausingStatuses` | `failureCausingChecks` | +| `notReady.noBatch` | `notReady.allowBatching` (inverted) | +| `prNumber` and `prUrl` in `waitingFor`, `otherTestedPullRequests`, `prerequisitePullRequests`, `conflictCulprit` | `number` and `url`, plus the pull request's `pullRequestId` | +| `blockReasons[].kind`: `behind_base`, `merge_conflict`, `failing_required_check`, `awaiting_required_check`, `undetermined` | `blockReasons[].code`: `behind`, `conflict`, `failingCheck`, `awaitingCheck`, `other` | +| `conflictCulprit.type`: `queue_pr`, `base_branch`, `stacked_pr` | `conflictCulprit.kind`: `queuePullRequest`, `baseBranch`, `stackedPullRequest` | +| `testsPassed.successorMergeItemId` | `testsPassed.successorPullRequestId` — a pull request ID, not a queue entry ID | +| `reason` values, lowercase (`prerequisite_merge_item_failed`) | Uppercase (`PREREQUISITE_MERGE_ITEM_FAILED`) — the one v2 enum that isn't camelCase | + +### Testing results + +| v1 `listMergeQueueTestingResults` | v2 `testing-results` | +| --- | --- | +| `testingResults` | `data` | +| `prNumber` | `pullRequestNumber` | +| `cycleConclusion` | `entryConclusion` | +| `bisection: "in_bisection"` | `isBisection: true` | +| `bisection: "entered_bisection"` | `startedBisection: true` (`isBisection` may also be `true`) | +| `testRunId` | `verificationRunId` | +| `totalItems` | `total`, only when you send `includeTotal=true` | + +### Verification runs + +v1's testing details are a v2 verification run: + +- `checkSuites[].checkRuns[]` and `statusChecks[]` become one flat `checks` list. Each check's `kind` says which it was, and GitHub's raw values are in `providerStatus` and `providerConclusion`. A status check's `context` is its `name`. +- `dependentPrs` becomes `dependentPullRequests`. +- `impactedTargets` and `impactedTargetsForTestedPrs` become `impactedTargets.all` and `impactedTargets.testedPullRequestsOnly`. Each is an object — `impactsAllTargets`, `impactedTargets`, `totalImpactedTargets` — rather than a list. +- `status` and `requiredStatusesSource` values are camelCase. + +### Renamed settings + +`PATCH /v2/merge-queues/{id}/config` takes the same settings as v1's `updateQueue`, with these changes: + +| v1 | v2 | +| --- | --- | +| `batch` | `allowBatching` | +| `notReadyTimeoutHours: n` | `notReadyTimeoutMinutes: n * 60` — must be a multiple of 60 | +| `commentsEnabled`, `commandsEnabled` | `areCommentsEnabled`, `areCommandsEnabled` | +| `labelCommandsEnabled`, `stateLabelsEnabled` | `areLabelCommandsEnabled`, `areStateLabelsEnabled` | +| `statusCheckEnabled`, `extensionEnabled` | `isStatusCheckEnabled`, `isExtensionEnabled` | +| `createPrsForTestingBranches` | `createsPrsForTestingBranches` | +| `deleteRequiredStatuses: true` | `requiredStatuses: []` | +| `mergeMethod`: `merge_commit` | `mergeCommit` | +| `optimizationMode`: `bisection_skip_redundant_tests` | `bisectionSkipRedundantTests` | +| `testBranchConstructionMode`: `rename_temp_branch`, `push_new_branch_from_temp` | `renameTempBranch`, `pushNewBranchFromTemp` | +| `state`, `mode` | Their own endpoints — see the [endpoint map](#endpoint-map) | + +The same names appear under `config` when you read a queue. v2 also bounds values v1 accepted without limit, such as `concurrency` (at most 1000) and list settings (at most 100 entries), so a queue configured past a bound through v1 can't be written back unchanged. + +## Behavior changes + +- **Resubmitting doesn't update a queued pull request.** v1's `submitPullRequest` rewrote the priority and batching of a pull request already in the queue. v2 returns `409 PR_ALREADY_ENQUEUED` and changes nothing. Use [`POST /v2/pull-requests/{id}/reprioritize`](/merge-queue/reference/api-v2/pullrequests/reprioritize-a-queued-pull-request) or [`PATCH /v2/pull-requests/{id}/queue-entry`](/merge-queue/reference/api-v2/pullrequests/update-a-queued-pull-requests-entry) instead. +- **`requiredStatuses: []` clears the list.** In v1 an empty list was ignored, and you cleared it with `deleteRequiredStatuses: true`. +- **The queue's pull requests come in three lists.** v1's `getQueue` returned one `enqueuedPullRequests` array. `/order` returns `batches` (what is testing, each with its `pullRequests`), `pending` (admitted and waiting, in the order the queue will consider them), and `awaitingAdmission`. To rebuild v1's flat list, concatenate them in that order. Pull requests that aren't ready yet are in none of them, as in v1. Each entry carries only the pull request's `id`, `number`, `title`, `sha`, and `url` — for state, priority, or author, match by `id` against [`GET /v2/merge-queues/{id}/pull-requests?state=pending,testing,testsPassed,pendingFailure`](/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-pull-requests). `awaitingAdmission` is truncated; `awaitingAdmissionTotal` is the full count. +- **List order is not queue order.** `GET /v2/merge-queues/{id}/pull-requests` is most recently enqueued first. Use `/order` for queue position. +- **Lookups don't tell you "not in this queue".** v1's `getSubmittedPullRequest` returned `404`, and `getSubmittedPullRequests` a `notFound` list, for pull requests never submitted to that queue. `GET /v2/pull-requests` finds any pull request Trunk knows in the repository. Treat a result with no `queueEntry`, or whose `queueEntry.mergeQueueId` is another queue, as not found — and for a batch, also compare the numbers you sent with the ones returned. +- **Timelines default to oldest first.** Send `direction=newest` for v1's order, where the current state comes first. +- **A missing resource and another organization's resource are both `404`.** `403 INSUFFICIENT_PERMISSIONS` means the API key lacks a permission for that request. +- **Retry by reading back.** v2 doesn't honor `Idempotency-Key` yet. If a write times out, read the resource before retrying. +- **Rate limits.** Requests are limited to 100 per second per organization, and metrics to 5 per second. Store IDs rather than resolving them on every request. + +### Finding a verification run + +v1's `getMergeQueueTestingDetails` took a `testRunId`. A `testRunId` you stored from v1 is the same ID in v2, except that v2 doesn't accept v1's `-bisection` suffix: a bisection is its own verification run, with its own ID. In v2, take the ID from the response you already have: + +- `verificationRunId` on a [testing result](/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-completed-testing), a queue-entry history transition, or a batch in `/order` +- `queueEntry.verifiedByVerificationRunId` on a merged pull request +- `id` on each run from [a pull request's](/merge-queue/reference/api-v2/pullrequests/list-a-pull-requests-verification-runs) or [a queue's](/merge-queue/reference/api-v2/mergequeues/list-a-merge-queues-verification-runs) verification runs + +## Checklist + +- [ ] An API key exists with the [permissions](#step-1-create-an-api-key) your calls need, stored as a secret. +- [ ] No request uses `api.trunk.io/v1` or the `x-api-token` header. +- [ ] Queue and pull request IDs are resolved once and stored, not looked up per request. +- [ ] Every enum value comes from the tables above, including values stored in your own database or config. +- [ ] Every list request sets `limit` and follows `nextCursor` until `hasMore` is `false` — including batch lookups. +- [ ] Response parsing reads queue fields from `queueEntry`, and no code stores the pull request's `id` where it stored v1's queue entry `id`. +- [ ] Timeline and testing-result parsing uses the v2 field names above. +- [ ] `notReadyTimeoutHours` values are converted to minutes. +- [ ] Code that resubmitted a queued pull request to change its priority now calls `reprioritize` or `PATCH …/queue-entry`. +- [ ] Code that sent `requiredStatuses: []` meant to clear the list. +- [ ] Error handling switches on `error.code`, and treats `404` as possibly "not yours". diff --git a/openapi-v2.json b/openapi-v2.json index 0fe54746..d13265d0 100644 --- a/openapi-v2.json +++ b/openapi-v2.json @@ -3,7 +3,7 @@ "info": { "title": "Trunk API", "version": "2.0.0", - "description": "The Trunk public API. Every endpoint is scoped to a single organization and requires a credential.\n\n## Authentication\n\nSend **either** an org API key as `Authorization: Bearer `, **or** a short-lived first-party token in the `x-trunk-token` header. Existing v1 API tokens work unchanged as org API keys.\n\nThe two are not interchangeable for writes. A first-party token obtained by **exchanging an org API key** reads merge queues but cannot mutate them — a merge-queue mutation forwards the key itself to the merge service, and an exchanged token carries no copy of it, so those requests answer `403 INSUFFICIENT_PERMISSIONS`. A machine caller that mutates merge queues should send its org API key directly on `Authorization` for every request. Tokens obtained through the CLI device flow are unaffected.\n\n**Merge-queue authorization is broad today.** The API does not offer an enqueue-only capability: organization members and org API keys authorized for merge-queue writes can also change configuration (including `requiredStatuses`), pause, drain and delete queues. Use such credentials as privileged operators, not narrowly scoped CI submission tokens.\n\n## Errors\n\nEvery 4xx and 5xx response is a `ErrorResponse`: `{ \"error\": { \"code\", \"message\" } }`. Switch on `code`, which is stable; `message` is prose and may change at any time.\n\n**An authorization denial on a read is reported as `404`, not `403`** — whether a resource exists must not leak across organizations, so a resource in another organization is indistinguishable from one that does not exist. `403 INSUFFICIENT_PERMISSIONS` means the credential is valid but is not permitted the action (or carries no organization).\n\nEvery response carries an `X-Request-Id` header. Quote it in support requests.\n\n## Rate limiting\n\nRequests are limited per organization (currently 100 requests/second). Exceeding it returns `429` with `RATE_LIMIT_EXCEEDED`; retry with backoff. Note that the limit is enforced at the edge, which does not yet wrap its `429` in the `ErrorResponse` envelope described above — treat any `429` as rate limiting regardless of body.\n\n## Compatibility\n\nWithin v2, Trunk may add: new endpoints, new **optional** response fields, new members to any enum (including new `code` values, new queue-entry states, and new providers), and new optional request parameters. **Clients must tolerate values and fields they do not recognize** rather than failing closed — a client that switches exhaustively over an enum will break when a member is added. Removals and semantic changes require a new major version.\n\n## Provider support\n\nMerge queues, verification runs and checks are **GitHub-only** today; `Check.provider` has a single member for that reason. Impacted-target uploads additionally require a `github.com` repository named `owner/repo`. The `Provider` enum lists values the repository index can store, not surfaces that are fully supported.\n\n## Not yet available\n\nA pull request's **position** in its queue is not exposed, and list responses are not ordered by queue position. There are no webhooks — monitoring is by polling. `Idempotency-Key` is not yet honored, so a mutation that times out must be reconciled by reading the resource back rather than blindly retried.", + "description": "The Trunk public API. Every endpoint is scoped to a single organization and requires a credential.\n\n## Authentication\n\nFor automation, send an API key as `Authorization: Bearer `. An organization admin creates one in the Trunk app under **Settings → Developer → API Keys**, and chooses the merge-queue permissions it holds. A key with no permissions can read merge queues but change nothing. Those permissions scope merge queue only: every key can use the Flaky Tests and Dynamic CI endpoints, including their writes, whatever permissions it holds.\n\nA short-lived first-party token in the `x-trunk-token` header is what the `trunk` CLI sends after `trunk auth login`.\n\nA GitHub Actions job can upload impacted targets with no Trunk secret: the `trunk-io/login` action exchanges the run's own GitHub credential (its OIDC token, or on a pull request from a fork its `GITHUB_TOKEN`) for a short-lived first-party token. That token is accepted only by `PUT /v2/impacted-targets`, for the run's own repository — and on a fork pull request, only for that pull request and its head sha.\n\n## Errors\n\nEvery 4xx and 5xx response is a `ErrorResponse`: `{ \"error\": { \"code\", \"message\" } }`. Switch on `code`, which is stable; `message` is prose and may change at any time.\n\n**An authorization denial on a read is reported as `404`, not `403`** — whether a resource exists must not leak across organizations, so a resource in another organization is indistinguishable from one that does not exist. `403 INSUFFICIENT_PERMISSIONS` means the credential is valid but is not permitted the action (or carries no organization).\n\nEvery response carries an `X-Request-Id` header. Quote it in support requests.\n\n## Rate limiting\n\nRequests are limited per organization (currently 100 requests/second). Exceeding it returns `429` with `RATE_LIMIT_EXCEEDED`; retry with backoff. Note that the limit is enforced at the edge, which does not yet wrap its `429` in the `ErrorResponse` envelope described above — treat any `429` as rate limiting regardless of body.\n\n## Compatibility\n\nWithin v2, Trunk may add: new endpoints, new **optional** response fields, new members to any enum (including new `code` values, new queue-entry states, and new providers), and new optional request parameters. **Clients must tolerate values and fields they do not recognize** rather than failing closed — a client that switches exhaustively over an enum will break when a member is added. Removals and semantic changes require a new major version.\n\n## Provider support\n\nMerge queues, verification runs and checks are **GitHub-only** today; `Check.provider` has a single member for that reason. Impacted-target uploads additionally require a `github.com` repository named `owner/repo`. The `Provider` enum lists values the repository index can store, not surfaces that are fully supported.\n\n## Not yet available\n\nA pull request's numeric **position** in its queue, and any wait estimate, are not exposed, and list responses are not ordered by queue position. `GET /v2/merge-queues/{id}/order` returns the order the engine will consider pull requests in, but an array index there is not a wait: use a pending entry's `directlyAhead` rather than counting. This API delivers no events of its own — poll it, or subscribe to [Merge Queue's webhooks](https://docs.trunk.io/merge-queue/webhooks). `Idempotency-Key` is not yet honored, so a mutation that times out must be reconciled by reading the resource back rather than blindly retried.", "contact": { "name": "Trunk Support", "url": "https://docs.trunk.io" @@ -17,7 +17,7 @@ ], "security": [ { - "orgKey": [] + "apiKey": [] }, { "trunkToken": [] @@ -59,16 +59,16 @@ ], "components": { "securitySchemes": { - "orgKey": { + "apiKey": { "type": "http", "scheme": "bearer", - "description": "An org-scoped API key, sent as `Authorization: Bearer `. Existing v1 tokens work here unchanged. There is no enqueue-only or read/write scope: an org key authorized for merge-queue writes can invoke every merge-queue mutation, including changing merge-protection configuration and pausing, draining or deleting a queue." + "description": "An API key, sent as `Authorization: Bearer `. An organization admin creates one in the Trunk app under **Settings → Developer → API Keys** and chooses the merge-queue permissions it holds; a key with none can read merge queues but change nothing. Those permissions scope merge queue only: every key can use the Flaky Tests and Dynamic CI endpoints, including their writes, whatever permissions it holds." }, "trunkToken": { "type": "apiKey", "in": "header", "name": "x-trunk-token", - "description": "A short-lived first-party token, sent in the `x-trunk-token` header. Obtain one through the CLI device-authentication flow, or by exchanging an org API key (`POST /v2/auth/api-key/login`).\n\n**A token exchanged from an org API key reads merge queues but cannot mutate them.** A merge-queue mutation forwards the org API key itself to the merge service, and an exchanged token carries no copy of it, so those requests answer `403 INSUFFICIENT_PERMISSIONS`. A machine caller that mutates merge queues should send the org API key directly as `Authorization: Bearer ` rather than exchanging it. Tokens from the device flow are unaffected." + "description": "A short-lived first-party token, sent in the `x-trunk-token` header. The `trunk` CLI sends one after `trunk auth login`, and the `trunk-io/login` action obtains one for a GitHub Actions run.\n\n**A token from a GitHub Actions run can only upload impacted targets** (`PUT /v2/impacted-targets`), for the repository the run belongs to; every other endpoint refuses it." } }, "schemas": { @@ -212,7 +212,7 @@ "ManagedState": { "type": "string", "enum": ["manual", "terraform", "terraformWithDrift"], - "description": "Who owns the queue's configuration. `terraform` — the last write came from Terraform, which only the v1 API detects today (a `terraform`/`opentofu` user agent or `x-source` header on `/createQueue` and `/updateQueue`). `terraformWithDrift` — Terraform owned it and a later write was not identified as Terraform's, so the live configuration may differ from the plan. Any such write counts, including pause/resume and, until this API forwards the Terraform signal, every write made through v2 — so v2 automation on a Terraform-owned queue reads as drift. `manual` — never written by Terraform." + "description": "Who owns the queue's configuration. `terraform` — the last write came from Terraform, identified on any queue write (create, config, pause/resume/drain, mode switch) by a `User-Agent` or `X-Source` header containing `terraform` or `opentofu`, case-insensitively — the same rule as v1. `terraformWithDrift` — Terraform owned it and a later write was not identified as Terraform's, so the live configuration may differ from the plan. Any such write counts, including pause/resume from other automation. `manual` — never written by Terraform." }, "MergeQueueConfig": { "type": "object", @@ -466,6 +466,7 @@ "PR_NOT_IN_ACTIVE_STATE", "MERGE_QUEUE_NOT_CREATABLE", "PR_TESTS_NOT_RESTARTABLE", + "ALLOWED_BOT_SUBMITTERS_FROZEN", "PROVIDER_URL_NOT_FOUND", "CI_HOST_UNSUPPORTED", "CI_SCOPE_NOT_FOUND", @@ -514,6 +515,411 @@ "additionalProperties": false, "description": "Omitting `mode` or `concurrency` applies the merge service's own defaults rather than a schema default, because what an omission means belongs to that service." }, + "PrometheusExposition": { + "type": "string", + "description": "Prometheus text exposition format 0.0.4. The metric names, help text and labels are v1's `getMergeQueueMetrics`, so an existing scrape config or dashboard needs only its URL and credential changed. Every series carries `repo` (the repository's name), `branch` and `queue_type` (`main` or `bisection`); `mq_pr_conclusions_1h_total` and `mq_pr_time_in_queue_1h_seconds*` add `conclusion`, and `mq_pr_state_duration_1h_seconds*` adds `state`. Gauges report the queue now; `_1h` series cover the last hour.", + "example": "# HELP mq_depth_current Number of PRs currently in the queue (in any non-terminal state).\n# TYPE mq_depth_current gauge\nmq_depth_current{repo=\"acme/widgets\",branch=\"main\",queue_type=\"main\"} 4\n" + }, + "MergeQueueOrder": { + "type": "object", + "properties": { + "mode": { + "$ref": "#/components/schemas/MergeQueueMode" + }, + "queueState": { + "$ref": "#/components/schemas/MergeQueueState" + }, + "batchingEnabled": { + "type": "boolean", + "description": "Batching is configured. When false every pull request forms its own predicted batch, and `directlyAhead` is empty throughout.", + "example": true + }, + "isolationResolved": { + "type": "boolean", + "description": "False when an `ISOLATE` batching rule could apply to this queue. Resolving those costs one impacted-target lookup per pull request, so this read skips it — which can move an isolated pull request into an earlier predicted batch, and so earlier in `pending`, than the engine will actually consider it.", + "example": true + }, + "hasCycle": { + "type": "boolean", + "description": "The queue's graph has a cycle and the engine fell back to a degraded ordering. `directlyAhead` and `heldBy` are empty, and a held pull request reports as `schedulable`.", + "example": false + }, + "batches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderedBatch" + }, + "maxItems": 10000, + "description": "Batches that have a verification run, in dependency order — a batch appears after the batches it builds on, with bisections last. **Not start order**, and not all of them are still testing: read each batch's `state`." + }, + "pending": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderedPendingItem" + }, + "maxItems": 10000, + "description": "Admitted pull requests, in the order the engine will consider them; held ones come after schedulable ones. A `schedulable` entry's index **is** the number of admitted, schedulable pull requests the engine will consider before it — with batching off that is exactly the index — but that is not a wait: see `predictedBatch`." + }, + "awaitingAdmission": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueuedPullRequest" + }, + "maxItems": 10000, + "description": "Pull requests past the engine's graph soft cap, which hold no position yet. Ordered by a DIFFERENT comparator than `pending`, so the two lists are not one sequence, and truncated — see `awaitingAdmissionTotal`." + }, + "awaitingAdmissionTotal": { + "type": "integer", + "description": "How many are awaiting admission in total. The engine truncates the list, so compare this with `awaitingAdmission.length` to detect it; read a specific pull request through `GET /v2/pull-requests/{id}` rather than scanning.", + "example": 0 + } + }, + "required": [ + "mode", + "queueState", + "batchingEnabled", + "isolationResolved", + "hasCycle", + "batches", + "pending", + "awaitingAdmission", + "awaitingAdmissionTotal" + ], + "description": "The queue's dispatch order: what is testing, what is admitted and waiting, and what has not been admitted. Pull requests that are not ready are deliberately absent — they hold no position — and are readable through `GET /v2/pull-requests/{id}`." + }, + "OrderedBatch": { + "type": "object", + "properties": { + "verificationRunId": { + "type": "string", + "description": "The run testing this batch — pass it to `GET /v2/merge-queue-verification-runs/{id}`.", + "example": "4f3e2d1c-0b9a-4876-9543-210fedcba987" + }, + "state": { + "$ref": "#/components/schemas/QueueEntryState" + }, + "isBisection": { + "type": "boolean", + "description": "This batch is a bisection, splitting a failed batch to find the culprit. Bisections dispatch on their own concurrency budget, so one can start ahead of a lower-numbered `predictedBatch`.", + "example": false + }, + "pullRequests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueuedPullRequest" + }, + "maxItems": 10000, + "description": "The pull requests this run is testing together." + } + }, + "required": [ + "verificationRunId", + "state", + "isBisection", + "pullRequests" + ], + "description": "A batch with a verification run — testing, passed, merging, or holding others back after a failure. `state` says which." + }, + "QueueEntryState": { + "type": "string", + "enum": [ + "notReady", + "pending", + "testing", + "testsPassed", + "merged", + "failed", + "cancelled", + "pendingFailure" + ], + "description": "Where a pull request sits in its queue's lifecycle.\n\n| Value | Meaning |\n| --- | --- |\n| `notReady` | Enqueued but not yet eligible to test — a readiness condition is unmet (see `QueueEntry.readiness`). |\n| `pending` | Ready and waiting for a test slot, or waiting on an earlier entry. |\n| `testing` | A verification run is in flight for this entry. |\n| `testsPassed` | Verification succeeded; waiting to merge. |\n| `merged` | Terminal — merged into the target branch. |\n| `failed` | Terminal — verification or the merge itself failed. |\n| `cancelled` | Terminal — removed from the queue (dequeued, or cancelled by the engine). |\n| `pendingFailure` | A failure is expected because an entry ahead failed, but this entry has not been re-tested yet. Not terminal: it usually returns to `pending` or `testing`. |\n\nNew members may be added within v2.", + "example": "testing" + }, + "QueuedPullRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The pull request's id in this API — pass it to `GET /v2/pull-requests/{id}`.", + "example": "6f1d2c3b-4a59-4e6f-8a7b-9c0d1e2f3a4b" + }, + "number": { + "type": "integer", + "example": 4901 + }, + "sha": { + "type": "string", + "description": "The head commit that was included in this run. Compare with the pull request's current `sha` to detect that its branch moved since.", + "example": "9c58bf40e5c1a2b3d4e5f60718293a4b5c6d7e8f" + }, + "title": { + "type": "string", + "example": "Add the widget endpoint" + }, + "url": { + "type": "string", + "description": "The pull request on its provider. Absent until Trunk's sync has populated it.", + "example": "https://github.com/acme/widgets/pull/4901" + } + }, + "required": ["id", "number", "sha", "title"], + "description": "A pull request in the queue's dispatch order." + }, + "OrderedPendingItem": { + "type": "object", + "properties": { + "pullRequest": { + "$ref": "#/components/schemas/QueuedPullRequest" + }, + "schedulability": { + "$ref": "#/components/schemas/PendingSchedulability" + }, + "predictedBatch": { + "type": "integer", + "description": "1-based, and REPEATS across every pull request predicted to start testing together. Absent for a held item, which is excluded from the prediction. This is an order of CONSIDERATION, not of execution: several predicted batches commonly start at once, so a lower number is not a shorter wait.", + "example": 1 + }, + "directlyAhead": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueuedPullRequest" + }, + "maxItems": 10000, + "description": "The pull requests immediately in front of this one in the queue's graph. Reduced-DAG adjacency, not a conflict set: edges encode impacted-target overlap, so an indirect overlap is represented through the item between them. Empty when `batchingEnabled` is false or `hasCycle` is true." + }, + "heldBy": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueuedPullRequest" + }, + "maxItems": 10000, + "description": "The failing pull requests holding this one back, when `schedulability` is `heldBehindFailure`." + } + }, + "required": [ + "pullRequest", + "schedulability", + "directlyAhead", + "heldBy" + ], + "description": "A pull request admitted to the queue but not yet testing." + }, + "PendingSchedulability": { + "type": "string", + "enum": ["schedulable", "heldBehindFailure"], + "description": "`schedulable` means eligible for the next scheduling pass, NOT that it will run next — the pass is still gated by `queueState`, the queue's testing-concurrency cap, and predecessors lacking a usable test branch. `heldBehindFailure` means a `PENDING_FAILURE` ancestor is holding it; `heldBy` names which." + }, + "MergeGraph": { + "type": "object", + "properties": { + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MergeGraphNode" + }, + "maxItems": 10000 + }, + "edges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MergeGraphEdge" + }, + "maxItems": 10000, + "description": "Transitively reduced, so an indirect dependency is represented through the node between its endpoints rather than as its own edge." + } + }, + "required": ["nodes", "edges"], + "description": "The queue's dependency graph. **The dispatch order is NOT derivable from this** — the engine schedules on a cascaded priority and a pending-failure gate, neither of which is in this payload; use `GET /v2/merge-queues/{id}/order`. It also contains only pull requests the engine has admitted to its graph, so anything awaiting admission, or not yet ready, is absent." + }, + "MergeGraphNode": { + "type": "object", + "properties": { + "pullRequestNumber": { + "type": "integer", + "description": "Identifies this node within the graph, and resolves to a pull request through `GET /v2/pull-requests` with the queue's `repositoryId`. A merge queue holds at most one entry per pull request, so the number is unique here.", + "example": 4901 + }, + "title": { + "type": "string", + "example": "Add the widget endpoint" + }, + "author": { + "type": "string", + "example": "octocat" + }, + "state": { + "$ref": "#/components/schemas/QueueEntryState" + }, + "priorityValue": { + "type": "integer", + "description": "The pull request's OWN priority. Not the cascaded value the engine schedules on — that is computed per pass and is not exposed here.", + "example": 100 + }, + "priorityName": { + "type": "string", + "example": "medium" + }, + "skipsTheLine": { + "type": "boolean", + "example": false + }, + "allowsBatching": { + "type": "boolean", + "example": true + }, + "verificationRunId": { + "type": "string", + "description": "The run testing this node, when it is testing.", + "example": "4f3e2d1c-0b9a-4876-9543-210fedcba987" + }, + "isBisection": { + "type": "boolean", + "example": false + }, + "impactedTargets": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 10000, + "description": "Present only under `includeImpactedTargets`. Truncated — see `totalImpactedTargets`." + }, + "totalImpactedTargets": { + "type": "integer", + "example": 12 + } + }, + "required": [ + "pullRequestNumber", + "title", + "author", + "state", + "priorityValue", + "skipsTheLine", + "allowsBatching", + "isBisection" + ], + "description": "One pull request in the queue's graph." + }, + "MergeGraphEdge": { + "type": "object", + "properties": { + "fromPullRequestNumber": { + "type": "integer", + "example": 4901 + }, + "toPullRequestNumber": { + "type": "integer", + "example": 4902 + }, + "commonImpactedTargets": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 10000, + "description": "Why the edge exists: the targets both pull requests impact. Present only under `includeImpactedTargets`, and truncated." + }, + "totalCommonImpactedTargets": { + "type": "integer", + "example": 3 + } + }, + "required": ["fromPullRequestNumber", "toPullRequestNumber"], + "description": "A dependency between two pull requests, derived from overlapping impacted targets." + }, + "TestingResultList": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestingResult" + }, + "maxItems": 100 + }, + "nextCursor": { + "type": ["string", "null"], + "description": "Opaque cursor for the next page, or null on the last page. Pass it back verbatim as `cursor`; do not construct or inspect one.", + "example": "AQEKZm9vYmFy" + }, + "hasMore": { + "type": "boolean", + "description": "True when another page is available.", + "example": true + }, + "total": { + "type": "integer", + "description": "Total rows matching the filter. Present only when the request set `includeTotal=true`.", + "example": 1247 + } + }, + "required": ["data", "nextCursor", "hasMore"] + }, + "TestingResult": { + "type": "object", + "properties": { + "pullRequestNumber": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "The pull request's number on the provider.", + "example": 4231 + }, + "endedAt": { + "type": "string", + "format": "date-time", + "description": "When this stretch of testing ended. The end of the testing, not of the queue entry — see `entryConclusion`.", + "example": "2026-09-21T17:04:11.000Z" + }, + "testingDurationMs": { + "type": "integer", + "minimum": 0, + "description": "How long this stretch of testing lasted. Recorded to whole seconds, so always a multiple of 1000.", + "example": 754000 + }, + "outcome": { + "$ref": "#/components/schemas/TestingOutcome" + }, + "entryConclusion": { + "$ref": "#/components/schemas/QueueEntryConclusion" + }, + "isBisection": { + "type": "boolean", + "description": "This testing was itself part of a bisection.", + "example": false + }, + "startedBisection": { + "type": "boolean", + "description": "This failure caused the pull request's batch to be split. Can be true together with `isBisection`, when a bisection step splits again.", + "example": false + }, + "verificationRunId": { + "type": "string", + "description": "The run this testing belongs to — pass it to `GET /v2/merge-queue-verification-runs/{id}`. Absent on results recorded before runs were tracked on them.", + "example": "4f3e2d1c-0b9a-4876-9543-210fedcba987" + } + }, + "required": [ + "pullRequestNumber", + "endedAt", + "testingDurationMs", + "isBisection", + "startedBisection" + ], + "description": "One pull request's stretch of testing in a merge queue. A pull request appears once per attempt, so several times if its testing restarted, and a batched run appears once per pull request it tested. `outcome` and `entryConclusion` are absent when Trunk reports no classification." + }, + "TestingOutcome": { + "type": "string", + "enum": ["passed", "failed", "interrupted", "cancelled"], + "description": "How this stretch of testing ended. `passed` — the run passed, including a passing bisection step. `failed` — the run failed, or the pull request entered bisection. `interrupted` — testing was cut short and will be retried (a restart, a preemption, or a base branch change). `cancelled` — the pull request left the queue while testing.", + "example": "passed" + }, + "QueueEntryConclusion": { + "type": "string", + "enum": ["merged", "failed", "cancelled"], + "description": "How the queue entry this testing belonged to ended. A pull request that fails and is resubmitted has a new entry.", + "example": "merged" + }, "PullRequest": { "type": "object", "properties": { @@ -579,7 +985,7 @@ }, "viewerIsAuthor": { "type": "boolean", - "description": "Whether the authenticated user authored this pull request. Provided on the queue pull-request list (`GET /merge-queues/{id}/pull-requests`); absent elsewhere and for machine (org-token) principals.", + "description": "Whether the authenticated user authored this pull request. Provided on the queue pull-request list (`GET /merge-queues/{id}/pull-requests`); absent elsewhere and when the caller is not a signed-in user.", "example": true } }, @@ -649,9 +1055,9 @@ "description": "Whether this entry may be verified in a batch with others. The queue's own `config.allowBatching` is the ceiling — an entry cannot batch when the queue forbids it.", "example": true }, - "isForceEnqueued": { + "direct": { "type": "boolean", - "description": "Whether the entry was force-enqueued past an unmet readiness condition (`direct` on submit, `isForceEnqueued` on `PATCH /v2/pull-requests/{id}/queue-entry`). It did not skip verification.", + "description": "Whether the entry was sent directly to the queue past an unmet readiness condition (`direct` on submit or on `PATCH /v2/pull-requests/{id}/queue-entry`). It did not skip the queue's own tests.", "example": false }, "verifiedByVerificationRunId": { @@ -682,28 +1088,13 @@ "stateChangedAt", "skipsTheLine", "allowBatching", - "isForceEnqueued", + "direct", "readiness", "createdAt", "updatedAt" ], "description": "A pull request's queue membership. There is at most one ever — re-submitting the same pull request mutates this record rather than adding another." }, - "QueueEntryState": { - "type": "string", - "enum": [ - "notReady", - "pending", - "testing", - "testsPassed", - "merged", - "failed", - "cancelled", - "pendingFailure" - ], - "description": "Where a pull request sits in its queue's lifecycle.\n\n| Value | Meaning |\n| --- | --- |\n| `notReady` | Enqueued but not yet eligible to test — a readiness condition is unmet (see `QueueEntry.readiness`). |\n| `pending` | Ready and waiting for a test slot, or waiting on an earlier entry. |\n| `testing` | A verification run is in flight for this entry. |\n| `testsPassed` | Verification succeeded; waiting to merge. |\n| `merged` | Terminal — merged into the target branch. |\n| `failed` | Terminal — verification or the merge itself failed. |\n| `cancelled` | Terminal — removed from the queue (dequeued, or cancelled by the engine). |\n| `pendingFailure` | A failure is expected because an entry ahead failed, but this entry has not been re-tested yet. Not terminal: it usually returns to `pending` or `testing`. |\n\nNew members may be added within v2.", - "example": "testing" - }, "Readiness": { "type": "object", "properties": { @@ -714,7 +1105,7 @@ }, "hasImpactedTargets": { "type": "boolean", - "description": "ABSENT, not false, in three cases: on a list read (which includes a coordinates lookup addressed by more than one `number`), for org-token callers, and on the 201 from a submit whose post-write lookup failed. Absence means 'not answered here' — never 'no targets'.", + "description": "Whether impacted targets are uploaded for the entry's commit — for a stacked pull request, for every pull request its queued stack covers — or not needed (queue mode, or a pull request predating the queue's switch to graph mode). ABSENT, not false, in two cases: on a list read (which includes a coordinates lookup addressed by more than one `number`), and on the response to a write whose post-write lookup failed. Absence means 'not answered here' — never 'no targets'.", "example": true }, "doesBaseBranchMatch": { @@ -848,25 +1239,13 @@ }, "direct": { "type": "boolean" - }, - "force": { - "type": "boolean", - "deprecated": true, - "description": "Deprecated compatibility alias for `direct`; do not send both fields." } }, "required": ["number"], "additionalProperties": false, - "description": "Omitting `allowBatching` allows batching. `priorityValue` and `priorityName` are mutually exclusive; `priorityValue: 0` skips the line. `direct` sends the pull request directly to the queue past unmet branch protection / failing required checks, merging via Trunk's bypass app (it does not skip the queue's own tests); an org API key is trusted, while a user token requires repository-admin permission. `force` is a deprecated compatibility alias for `direct`; do not send both fields.", + "description": "Omitting `allowBatching` allows batching. `priorityValue` and `priorityName` are mutually exclusive; `priorityValue: 0` skips the line. `direct` sends the pull request directly to the queue past unmet branch protection / failing required checks, merging via Trunk's bypass app (it does not skip the queue's own tests); it needs the **Merge PRs with direct to queue** permission, and a signed-in user also needs repository-admin permission.", "not": { - "anyOf": [ - { - "required": ["priorityValue", "priorityName"] - }, - { - "required": ["direct", "force"] - } - ] + "required": ["priorityValue", "priorityName"] } }, "PullRequestList": { @@ -1065,7 +1444,8 @@ }, "sha": { "type": "string", - "minLength": 1 + "pattern": "^[A-Za-z0-9]+$", + "example": "3f2c9a1b7e4d" }, "number": { "type": "integer", @@ -1194,6 +1574,9 @@ }, "testBranchPullRequest": { "$ref": "#/components/schemas/TestBranchPullRequest" + }, + "impactedTargets": { + "$ref": "#/components/schemas/VerificationRunImpactedTargets" } }, "required": [ @@ -1396,13 +1779,56 @@ "type": "integer", "example": 5002 }, - "url": { - "type": "string", - "example": "https://github.com/acme/widgets/pull/5002" + "url": { + "type": "string", + "example": "https://github.com/acme/widgets/pull/5002" + } + }, + "required": ["number", "url"], + "description": "The draft pull request the queue opened for the test branch, where its checks and logs are. Absent unless the queue is configured to create one." + }, + "VerificationRunImpactedTargets": { + "type": "object", + "properties": { + "all": { + "$ref": "#/components/schemas/ImpactedTargetSet" + }, + "testedPullRequestsOnly": { + "$ref": "#/components/schemas/ImpactedTargetSet" + } + }, + "required": ["all"], + "description": "The impacted targets uploaded for the run's commits (`PUT /impacted-targets`). Returned only when a single run is fetched by ID, never in a list, and absent when no commit on the run has an upload. `all` spans every commit the run carried: the pull requests under test and the commits merged in beneath them to build the predicted base. `testedPullRequestsOnly` spans the tested pull requests' commits alone, and is absent when none of them uploaded. A commit with no upload contributes nothing to either set." + }, + "ImpactedTargetSet": { + "type": "object", + "properties": { + "impactsAllTargets": { + "type": "boolean", + "description": "True when any commit in this set uploaded `impactsAllTargets: true`, which makes the explicit lists of the others moot.", + "example": false + }, + "impactedTargets": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 100000, + "description": "The union of the commits' uploaded targets, sorted and deduplicated. Empty when `impactsAllTargets` is true, and also when every upload was an explicit empty list — the commits impact nothing. Truncated to the first 100,000 targets; `totalImpactedTargets` says whether it was.", + "example": ["//lib/db", "//svc/api"] + }, + "totalImpactedTargets": { + "type": "integer", + "description": "The size of the union before truncation. 0 when `impactsAllTargets` is true.", + "example": 2 } }, - "required": ["number", "url"], - "description": "The draft pull request the queue opened for the test branch, where its checks and logs are. Absent unless the queue is configured to create one." + "required": [ + "impactsAllTargets", + "impactedTargets", + "totalImpactedTargets" + ], + "description": "The impacted targets of a set of commits on a verification run." }, "VerificationRunList": { "type": "object", @@ -1479,7 +1905,7 @@ }, "reason": { "type": ["string", "null"], - "description": "Why the entry entered this state. `null` for `testing` and `pendingFailure`, which carry no reason.\n\nThe values below are the vocabulary in use today, grouped by the state they accompany. **This is an open set** — it is the engine's own vocabulary and new values may appear within v2, so match on the values you care about and treat anything else as unclassified rather than failing closed.\n\n**Why an entry is waiting (`notReady`)** — `USER_REQUESTED`, `TRUNK_ADMIN_REQUESTED`, `API_REQUESTED`.\n\n**Why an entry (re)entered the line (`pending`)** — these are what distinguish a restart's cause:\n\n| Value | Meaning |\n| --- | --- |\n| `TESTS_RESTARTED_BY_USER` | A person restarted the tests. |\n| `TESTS_RESTARTED_BY_API_REQUEST` | Restarted through the API — e.g. `pullRequests.restartTests`. |\n| `MERGE_ITEM_NOW_READY` | A readiness condition became satisfied. |\n| `MANUAL_PUSH_TO_TARGET_BRANCH` | Someone pushed to the target branch, invalidating the tested state. |\n| `PREREQUISITE_MERGE_ITEM_FAILED` | An entry ahead of this one failed, so this one must re-test. |\n| `PREREQUISITE_MERGE_ITEM_CANCELLED` | An entry ahead was removed. |\n| `PREREQUISITE_MERGE_ITEM_MANUALLY_MERGED` | An entry ahead was merged outside the queue. |\n| `SKIP_THE_LINE_INTERRUPTED` | A higher-priority entry preempted this one. |\n| `BISECTION_REQUIRED` | A batch failed and the engine is bisecting to find the culprit. |\n| `BISECTION_TEST_RUN_PASSED` | A bisection run cleared this entry. |\n\n**Why verification passed (`testsPassed`)** — `TEST_RUN_PASSED`, `SUCCESSOR_TEST_RUN_PASSED`, `PASSED_BY_OPTIMIZATION`.\n\n**Why an entry failed (`failed`)** — `TEST_RUN_REQUIRED_STATUS_FAILED`, `TEST_RUN_TIMEOUT`, `TEST_RUN_FAILED_BY_OPTIMIZATION`, `START_TEST_RUN_MERGE_CONFLICT`, `START_TEST_RUN_RETRY_EXHAUSTED`, `START_TEST_RUN_DRAFT_PRS_NOT_ALLOWED`, `START_TEST_RUN_PERMISSIONS_BLOCK_TEST_BRANCH`, `START_TEST_RUN_NO_REQUIRED_STATUSES`, `START_TEST_RUN_CONFIG_NOT_FOUND`, `START_TEST_RUN_CONFIG_PARSING_FAILURE`, `START_TEST_RUN_CONFIG_BAD_VERSION`, `START_TEST_RUN_CONFIG_BAD_REQUIRED_STATUSES`, `START_TEST_RUN_CONFIG_MISSING_REQUIRED_STATUSES`, `START_TEST_RUN_INVALID_EVENT`, `PR_UPDATED_AT_MERGE_TIME`, `GITHUB_API_MERGE_PR_FAILED`, `GITHUB_API_DIRECT_MERGE_PR_FAILED`, `GITHUB_API_BRANCH_PROTECTION_UNMET`, `GITHUB_API_MERGE_ALREADY_IN_PROGRESS`, `GITHUB_API_RATE_LIMIT_EXCEEDED`, `GITHUB_API_REPEATED_SERVER_ERRORS`.\n\n**Why an entry was removed (`cancelled`)** — `USER_REQUESTED`, `TRUNK_ADMIN_REQUESTED`, `PR_CLOSED`, `PR_PUSHED_TO`, `PR_BASE_BRANCH_CHANGED`, `PR_CONVERTED_TO_DRAFT`, `NOT_READY_TIMEOUT_EXPIRED`, `STACKED_PR_MEMBER_PUSHED_TO`, `STACKED_PR_MEMBER_CLOSED`, `STACKED_PR_MEMBER_MERGED`, `STACKED_PR_MEMBER_BASE_BRANCH_CHANGED`, `STACKED_PR_STACK_CHANGED`, `STACKED_PR_GROUP_CANCELLED_AT_MERGE_TIME`.\n\n**Why an entry merged (`merged`)** — `MERGE_GRAPH_MERGE`, `MERGE_GRAPH_DIRECT_MERGE`, `MANUAL_MERGE`.", + "description": "Why the entry entered this state. `null` for `testing` and `pendingFailure`, which carry no reason.\n\nThe values below are the vocabulary in use today, grouped by the state they accompany. **This is an open set** — it is the engine's own vocabulary and new values may appear within v2, so match on the values you care about and treat anything else as unclassified rather than failing closed.\n\n**Why an entry is waiting (`notReady`)** — `USER_REQUESTED`, `TRUNK_ADMIN_REQUESTED`, `API_REQUESTED`.\n\n**Why an entry (re)entered the line (`pending`)** — these are what distinguish a restart's cause:\n\n| Value | Meaning |\n| --- | --- |\n| `TESTS_RESTARTED_BY_USER` | A person restarted the tests. |\n| `TESTS_RESTARTED_BY_API_REQUEST` | Restarted through the API — e.g. `pullRequests.restartTests`. |\n| `MERGE_ITEM_NOW_READY` | A readiness condition became satisfied. |\n| `MANUAL_PUSH_TO_TARGET_BRANCH` | Someone pushed to the target branch, invalidating the tested state. |\n| `PREREQUISITE_MERGE_ITEM_FAILED` | An entry ahead of this one failed, so this one must re-test. |\n| `PREREQUISITE_MERGE_ITEM_CANCELLED` | An entry ahead was removed. |\n| `PREREQUISITE_MERGE_ITEM_MANUALLY_MERGED` | An entry ahead was merged outside the queue. |\n| `SKIP_THE_LINE_INTERRUPTED` | A higher-priority entry preempted this one. |\n| `BISECTION_REQUIRED` | A batch failed and the engine is bisecting to find the culprit. |\n| `BISECTION_TEST_RUN_PASSED` | A bisection run cleared this entry. |\n\n**Why verification passed (`testsPassed`)** — `TEST_RUN_PASSED`, `SUCCESSOR_TEST_RUN_PASSED`, `PASSED_BY_OPTIMIZATION`.\n\n**Why an entry failed (`failed`)** — `TEST_RUN_REQUIRED_STATUS_FAILED`, `TEST_RUN_TIMEOUT`, `TEST_RUN_FAILED_BY_OPTIMIZATION`, `START_TEST_RUN_MERGE_CONFLICT`, `START_TEST_RUN_RETRY_EXHAUSTED`, `START_TEST_RUN_DRAFT_PRS_NOT_ALLOWED`, `START_TEST_RUN_PERMISSIONS_BLOCK_TEST_BRANCH`, `START_TEST_RUN_NO_REQUIRED_STATUSES`, `START_TEST_RUN_CONFIG_NOT_FOUND`, `START_TEST_RUN_CONFIG_PARSING_FAILURE`, `START_TEST_RUN_CONFIG_BAD_VERSION`, `START_TEST_RUN_CONFIG_BAD_REQUIRED_STATUSES`, `START_TEST_RUN_CONFIG_MISSING_REQUIRED_STATUSES`, `START_TEST_RUN_INVALID_EVENT`, `PR_UPDATED_AT_MERGE_TIME`, `GITHUB_API_MERGE_PR_FAILED`, `GITHUB_API_DIRECT_MERGE_PR_FAILED`, `GITHUB_API_BRANCH_PROTECTION_UNMET`, `GITHUB_API_MERGE_ALREADY_IN_PROGRESS`, `GITHUB_API_RATE_LIMIT_EXCEEDED`, `GITHUB_API_REPEATED_SERVER_ERRORS`.\n\n**Why an entry was removed (`cancelled`)** — `USER_REQUESTED`, `TRUNK_ADMIN_REQUESTED`, `PR_CLOSED`, `PR_MERGED_INTO_OTHER_BRANCH`, `PR_PUSHED_TO`, `PR_BASE_BRANCH_CHANGED`, `PR_CONVERTED_TO_DRAFT`, `NOT_READY_TIMEOUT_EXPIRED`, `STACKED_PR_MEMBER_PUSHED_TO`, `STACKED_PR_MEMBER_CLOSED`, `STACKED_PR_MEMBER_MERGED`, `STACKED_PR_MEMBER_BASE_BRANCH_CHANGED`, `STACKED_PR_STACK_CHANGED`, `STACKED_PR_GROUP_CANCELLED_AT_MERGE_TIME`.\n\n**Why an entry merged (`merged`)** — `MERGE_GRAPH_MERGE`, `MERGE_GRAPH_DIRECT_MERGE`, `MANUAL_MERGE`.", "example": "TEST_RUN_REQUIRED_STATUS_FAILED" }, "verificationRunId": { @@ -1812,7 +2238,7 @@ "$ref": "#/components/schemas/QueueEntryStateTransition" }, "maxItems": 10000, - "description": "Its queue entry's state transitions, oldest first — the same order and shape the single-pull-request endpoint serves. Empty when the pull request was never enqueued, or is not yours to read: this endpoint reports neither as an error, so diff the ids you asked for against the groups you got." + "description": "Its queue entry's state transitions, oldest first by default and newest first under `direction=newest` — the same order and shape the single-pull-request endpoint serves. Empty when the pull request was never enqueued, or is not yours to read: this endpoint reports neither as an error, so diff the ids you asked for against the groups you got." } }, "required": ["pullRequestId", "transitions"], @@ -1854,14 +2280,14 @@ "type": "string", "minLength": 1 }, - "isForceEnqueued": { + "direct": { "type": "boolean", - "description": "Enqueue the pull request even though a readiness condition is unmet, letting the queue test it immediately instead of holding it in `notReady`. It does NOT bypass verification — required checks must still pass to merge. Honoured on both lanes: a user token requires repository-admin permission in trunk1, while an org API key is trusted. Reflected as `QueueEntry.isForceEnqueued`.", + "description": "Send the pull request directly to the queue past unmet readiness (branch protection, failing required checks), merging via Trunk's bypass app. It does not skip the queue's own tests. It needs the **Merge PRs with direct to queue** permission, and a signed-in user also needs repository-admin permission. `false` clears it. Reflected as `QueueEntry.direct`.", "example": false }, "allowBatching": { "type": "boolean", - "description": "Whether this entry may be tested in a batch with others. `false` gives it a test run of its own, which is why it costs the same permission as changing priority. Omit to leave the entry's current setting alone.", + "description": "Whether this entry may be tested in a batch with others. `false` gives it a test run of its own. Sending this field at all needs the **Skip batching** permission. Omit to leave the entry's current setting alone.", "example": false } }, @@ -2032,6 +2458,16 @@ "description": "`github.event_name`.", "example": "pull_request" }, + "prLabels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256 + }, + "maxItems": 1000, + "description": "The pull request's label names, from `github.event.pull_request.labels`. While the repository is in Learning mode, a pull request carrying a label on the repository's managed allow list (matched case-insensitively) is gated as if Dynamic CI were enabled.", + "example": ["bug", "ready-for-ci"] + }, "jobKeys": { "type": "array", "items": { @@ -2051,6 +2487,9 @@ "description": "Signals to drop from the tally. An unrecognized identifier is rejected, so a typo surfaces.", "example": ["estimated-cost"] }, + "changedFiles": { + "$ref": "#/components/schemas/ChangedFiles" + }, "workflowPath": { "type": "string", "description": "From `github.workflow_ref`; identifies the workflow whose jobs are in scope.", @@ -2088,6 +2527,100 @@ }, "required": ["owner", "name"] }, + "ChangedFiles": { + "type": "object", + "properties": { + "base": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "The commit or ref the diff was taken from.", + "example": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d" + }, + "totalFiles": { + "type": "integer", + "minimum": 0, + "description": "How many files changed, before the 200-file cap.", + "example": 1 + }, + "totalAdditions": { + "type": "integer", + "minimum": 0, + "description": "Lines added across every changed file, before the cap.", + "example": 12 + }, + "totalDeletions": { + "type": "integer", + "minimum": 0, + "description": "Lines removed across every changed file, before the cap.", + "example": 3 + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChangedFile" + }, + "maxItems": 200, + "description": "The first 200 changed files. Must hold exactly `min(totalFiles, 200)` entries." + } + }, + "required": [ + "base", + "totalFiles", + "totalAdditions", + "totalDeletions", + "files" + ], + "description": "The files changed between `base` and `commitSha`, diffed from their merge base (`base...commitSha`). No file contents. **Omit it if you cannot compute the diff; never send an empty list in its place.** An empty list says nothing changed, which is the strongest evidence to skip. When it is absent, Trunk fetches the diff through your GitHub App installation." + }, + "ChangedFile": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "example": "src/cache.ts" + }, + "previousPath": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "The old path, for a `renamed` or `copied` file.", + "example": "src/lru.ts" + }, + "status": { + "$ref": "#/components/schemas/ChangedFileStatus" + }, + "additions": { + "type": "integer", + "minimum": 0, + "description": "Lines added. 0 for a binary file.", + "example": 12 + }, + "deletions": { + "type": "integer", + "minimum": 0, + "description": "Lines removed. 0 for a binary file.", + "example": 3 + } + }, + "required": ["path", "status", "additions", "deletions"] + }, + "ChangedFileStatus": { + "type": "string", + "enum": [ + "added", + "modified", + "removed", + "renamed", + "copied", + "changed", + "unchanged" + ], + "description": "How the file changed. The values GitHub reports, so its `status` can be passed through. `changed` (a type change, such as a file becoming a symlink) is read as `modified`.", + "example": "modified" + }, "BuildkitePlanRequest": { "type": "object", "properties": { @@ -2133,6 +2666,16 @@ "description": "What triggered the build — `BUILDKITE_SOURCE`.", "example": "webhook" }, + "prLabels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256 + }, + "maxItems": 1000, + "description": "`BUILDKITE_PULL_REQUEST_LABELS`, split on commas. While the repository is in Learning mode, a pull request carrying a label on the repository's managed allow list (matched case-insensitively) is gated as if Dynamic CI were enabled.", + "example": ["bug", "ready-for-ci"] + }, "jobKeys": { "type": "array", "items": { @@ -2152,6 +2695,9 @@ "description": "Signals to drop from the tally. An unrecognized identifier is rejected, so a typo surfaces.", "example": ["estimated-cost"] }, + "changedFiles": { + "$ref": "#/components/schemas/ChangedFiles" + }, "buildkiteOrganizationSlug": { "type": "string", "minLength": 1, @@ -3343,40 +3889,365 @@ "schema": { "type": "string", "format": "uuid", - "description": "Only queues belonging to this repository.", + "description": "Only queues belonging to this repository.", + "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7" + }, + "required": false, + "description": "Only queues belonging to this repository.", + "name": "repositoryId", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Only the queue for this target branch. Requires `repositoryId`.", + "example": "main" + }, + "required": false, + "description": "Only the queue for this target branch. Requires `repositoryId`.", + "name": "targetBranch", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A page of merge queues", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeQueueList" + } + } + } + }, + "400": { + "description": "`VALIDATION_FAILED` | `INVALID_CURSOR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "`RATE_LIMIT_EXCEEDED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "`INTERNAL_ERROR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "operationId": "mergeQueues.create", + "tags": ["mergeQueues"], + "summary": "Create a merge queue", + "description": "Creates a merge queue for a repository and target branch. At most one queue exists per pair, so a second create returns `QUEUE_ALREADY_EXISTS`.\n\n**This operation is not atomic.** `bisectionConcurrency` and `enqueueingLabel` are applied by a second write, so **an error response does not guarantee that no queue was created** — if the first write succeeded and the second failed, the queue exists with those two fields at their defaults and this call still returns the second write's error. Do not blindly retry: a retry returns `409 QUEUE_ALREADY_EXISTS`. Instead read the queue back (`GET /v2/merge-queues?repositoryId=…&targetBranch=…`) and, if it exists, finish the configuration with `PATCH /v2/merge-queues/{id}/config`.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMergeQueueRequest" + } + } + } + }, + "responses": { + "201": { + "description": "The created merge queue", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeQueue" + } + } + } + }, + "400": { + "description": "`VALIDATION_FAILED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "`REPOSITORY_NOT_FOUND`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "`QUEUE_ALREADY_EXISTS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "`MERGE_QUEUE_NOT_CREATABLE`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "`RATE_LIMIT_EXCEEDED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "`INTERNAL_ERROR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "`UPSTREAM_ERROR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v2/merge-queues/metrics": { + "get": { + "operationId": "mergeQueues.organizationMetrics", + "tags": ["mergeQueues"], + "summary": "Get Prometheus metrics for every merge queue", + "description": "Returns Prometheus metrics for every merge queue in your organization, in the text exposition format, for a Prometheus scrape job. `repositoryId` narrows them to one repository's queues, across all its target branches, and is a 404 when your organization has no such repository. The metrics match v1's `getMergeQueueMetrics`.", + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid", + "description": "Only this repository's queues.", "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }, "required": false, - "description": "Only queues belonging to this repository.", + "description": "Only this repository's queues.", "name": "repositoryId", "in": "query" + } + ], + "responses": { + "200": { + "description": "The organization's merge-queue metrics", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PrometheusExposition" + } + } + } + }, + "400": { + "description": "`VALIDATION_FAILED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "`REPOSITORY_NOT_FOUND`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "`RATE_LIMIT_EXCEEDED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, + "500": { + "description": "`INTERNAL_ERROR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/v2/merge-queues/{id}/metrics": { + "get": { + "operationId": "mergeQueues.metrics", + "tags": ["mergeQueues"], + "summary": "Get Prometheus metrics for a merge queue", + "description": "Returns Prometheus metrics for one merge queue, in the text exposition format, for a Prometheus scrape job. The series and labels are those of `GET /v2/merge-queues/metrics`, restricted to this queue.", + "parameters": [ { "schema": { "type": "string", - "minLength": 1, - "description": "Only the queue for this target branch. Requires `repositoryId`.", - "example": "main" + "description": "The merge queue's id.", + "example": "xK9mP2nQ" }, - "required": false, - "description": "Only the queue for this target branch. Requires `repositoryId`.", - "name": "targetBranch", - "in": "query" + "required": true, + "description": "The merge queue's id.", + "name": "id", + "in": "path" } ], "responses": { "200": { - "description": "A page of merge queues", + "description": "The merge queue's metrics", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PrometheusExposition" + } + } + } + }, + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MergeQueueList" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "`VALIDATION_FAILED` | `INVALID_CURSOR`", + "404": { + "description": "`MERGE_QUEUE_NOT_FOUND`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "`RATE_LIMIT_EXCEEDED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "`INTERNAL_ERROR`", "content": { "application/json": { "schema": { @@ -3384,6 +4255,39 @@ } } } + } + } + } + }, + "/v2/merge-queues/{id}": { + "get": { + "operationId": "mergeQueues.get", + "tags": ["mergeQueues"], + "summary": "Get a merge queue", + "description": "Returns one merge queue, including its full configuration object.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The merge queue's id.", + "example": "xK9mP2nQ" + }, + "required": true, + "description": "The merge queue's id.", + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The merge queue", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeQueue" + } + } + } }, "401": { "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", @@ -3395,8 +4299,8 @@ } } }, - "403": { - "description": "`INSUFFICIENT_PERMISSIONS`", + "404": { + "description": "`MERGE_QUEUE_NOT_FOUND`", "content": { "application/json": { "schema": { @@ -3427,34 +4331,70 @@ } } }, - "post": { - "operationId": "mergeQueues.create", + "delete": { + "operationId": "mergeQueues.delete", "tags": ["mergeQueues"], - "summary": "Create a merge queue", - "description": "Creates a merge queue for a repository and target branch. At most one queue exists per pair, so a second create returns `QUEUE_ALREADY_EXISTS`.\n\n**This operation is not atomic.** `bisectionConcurrency` and `enqueueingLabel` are applied by a second write, so **an error response does not guarantee that no queue was created** — if the first write succeeded and the second failed, the queue exists with those two fields at their defaults and this call still returns the second write's error. Do not blindly retry: a retry returns `409 QUEUE_ALREADY_EXISTS`. Instead read the queue back (`GET /v2/merge-queues?repositoryId=…&targetBranch=…`) and, if it exists, finish the configuration with `PATCH /v2/merge-queues/{id}/config`.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMergeQueueRequest" - } - } + "summary": "Delete a merge queue", + "description": "**Destructive and not reversible.** Deletes a merge queue. The queue must be empty — drain or dequeue its pull requests first, or the call returns `QUEUE_HAS_ACTIVE_ENTRIES`. Historical queue entries and verification runs stay readable by id afterwards.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The merge queue's id.", + "example": "xK9mP2nQ" + }, + "required": true, + "description": "The merge queue's id.", + "name": "id", + "in": "path" } - }, + ], "responses": { - "201": { - "description": "The created merge queue", + "204": { + "description": "The queue was deleted" + }, + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "`MERGE_QUEUE_NOT_FOUND`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "`QUEUE_HAS_ACTIVE_ENTRIES`", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MergeQueue" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "`VALIDATION_FAILED`", + "429": { + "description": "`RATE_LIMIT_EXCEEDED`", "content": { "application/json": { "schema": { @@ -3463,8 +4403,8 @@ } } }, - "401": { - "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "500": { + "description": "`INTERNAL_ERROR`", "content": { "application/json": { "schema": { @@ -3473,8 +4413,8 @@ } } }, - "403": { - "description": "`INSUFFICIENT_PERMISSIONS`", + "502": { + "description": "`UPSTREAM_ERROR`", "content": { "application/json": { "schema": { @@ -3482,9 +4422,42 @@ } } } + } + } + } + }, + "/v2/merge-queues/{id}/order": { + "get": { + "operationId": "mergeQueues.order", + "tags": ["mergeQueues"], + "summary": "Get a merge queue's dispatch order", + "description": "Returns the queue's batches with a verification run, what is admitted and waiting in the order the engine will consider it, and what has not been admitted yet.\n\n**A batch in `batches` is not necessarily still testing** — one that has passed, is merging, or is holding others back after a failure still has a run and is still listed. Read its `state`.\n\n**The order is one of consideration, not of execution, and array position is not a wait estimate.** `predictedBatch` repeats across every pull request predicted to start testing together, and in `graph` mode several predicted batches commonly start at once — so the pull requests ahead of yours may all start with it, and may sit on unrelated branches that never block yours. `directlyAhead` is what actually precedes a pull request; use it rather than counting.\n\nIt is a point-in-time prediction. A new submission, a priority change or a failure regroups it, and four things outside this response still gate dispatch: `queueState: paused` stops it entirely, the queue's testing-concurrency cap bounds how many start, a batch whose predecessors have no usable test branch is skipped over rather than blocking, and bisections dispatch on their own budget.\n\nPull requests that are not ready are deliberately absent — they are not admitted, so they hold no position. Read one through `GET /v2/pull-requests/{id}`.\n\nUnpaginated: the engine caps its own graph, so the admitted set is bounded. `awaitingAdmission` is not, and is truncated.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The merge queue's id.", + "example": "xK9mP2nQ" + }, + "required": true, + "description": "The merge queue's id.", + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The queue's dispatch order", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeQueueOrder" + } + } + } }, - "404": { - "description": "`REPOSITORY_NOT_FOUND`", + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", "content": { "application/json": { "schema": { @@ -3493,8 +4466,8 @@ } } }, - "409": { - "description": "`QUEUE_ALREADY_EXISTS`", + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", "content": { "application/json": { "schema": { @@ -3503,8 +4476,8 @@ } } }, - "422": { - "description": "`MERGE_QUEUE_NOT_CREATABLE`", + "404": { + "description": "`MERGE_QUEUE_NOT_FOUND`", "content": { "application/json": { "schema": { @@ -3546,12 +4519,12 @@ } } }, - "/v2/merge-queues/{id}": { + "/v2/merge-queues/{id}/graph": { "get": { - "operationId": "mergeQueues.get", + "operationId": "mergeQueues.graph", "tags": ["mergeQueues"], - "summary": "Get a merge queue", - "description": "Returns one merge queue, including its full configuration object.", + "summary": "Get a merge queue's dependency graph", + "description": "Returns the queue's dependency graph: which pull requests the engine considers related, and why.\n\n**This does not tell you what runs next.** The engine schedules on a priority that cascades backwards through the graph and on a pending-failure gate, and neither is in this payload — `priorityValue` here is each pull request's own value, not the cascaded one. For dispatch order use `GET /v2/merge-queues/{id}/order`.\n\nEdges are transitively reduced, so an indirect dependency appears through the node between its endpoints rather than as its own edge. They mean overlapping impacted targets, not a stacked-branch relationship.\n\nOnly pull requests the engine has admitted to its graph appear: anything past the graph's soft cap, or not yet ready, is absent. `includeImpactedTargets` adds the target lists that explain each node and edge; it is off by default because populating it costs a lookup per distinct commit.", "parameters": [ { "schema": { @@ -3563,15 +4536,37 @@ "description": "The merge queue's id.", "name": "id", "in": "path" + }, + { + "schema": { + "type": "boolean", + "default": false, + "description": "Include the impacted-target lists on nodes and edges. Off by default: resolving them costs one lookup per distinct commit in the graph.", + "example": false + }, + "required": false, + "description": "Include the impacted-target lists on nodes and edges. Off by default: resolving them costs one lookup per distinct commit in the graph.", + "name": "includeImpactedTargets", + "in": "query" } ], "responses": { "200": { - "description": "The merge queue", + "description": "The queue's dependency graph", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MergeQueue" + "$ref": "#/components/schemas/MergeGraph" + } + } + } + }, + "400": { + "description": "`VALIDATION_FAILED`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3586,6 +4581,16 @@ } } }, + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "404": { "description": "`MERGE_QUEUE_NOT_FOUND`", "content": { @@ -3615,14 +4620,26 @@ } } } + }, + "502": { + "description": "`UPSTREAM_ERROR`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - }, - "delete": { - "operationId": "mergeQueues.delete", + } + }, + "/v2/merge-queues/{id}/testing-results": { + "get": { + "operationId": "mergeQueues.listTestingResults", "tags": ["mergeQueues"], - "summary": "Delete a merge queue", - "description": "**Destructive and not reversible.** Deletes a merge queue. The queue must be empty — drain or dequeue its pull requests first, or the call returns `QUEUE_HAS_ACTIVE_ENTRIES`. Historical queue entries and verification runs stay readable by id afterwards.", + "summary": "List a merge queue's completed testing", + "description": "Lists the stretches of testing the queue finished between `since` and `until`, newest first. One entry per pull request per attempt: a pull request appears several times if its testing restarted, and a batched run appears once per pull request it tested. Testing still in progress is not included. Pass an entry's `verificationRunId` to `GET /v2/merge-queue-verification-runs/{id}` for the run's checks and tested pull requests.\n\nThe range may span at most 45 days, which is also how long results are kept. When `until` is omitted it is fixed at the time of the first page, so later pages are not shifted by testing that finishes while you page.", "parameters": [ { "schema": { @@ -3634,14 +4651,82 @@ "description": "The merge queue's id.", "name": "id", "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25, + "description": "Page size, between 1 and 100.", + "example": 25 + }, + "required": false, + "description": "Page size, between 1 and 100.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Opaque cursor from a previous response's `nextCursor`. A cursor is bound to the operation and filter it was minted for.", + "example": "AQEKZm9vYmFy" + }, + "required": false, + "description": "Opaque cursor from a previous response's `nextCursor`. A cursor is bound to the operation and filter it was minted for.", + "name": "cursor", + "in": "query" + }, + { + "schema": { + "type": "boolean", + "default": false, + "description": "Set true to include `total` in the response. Costs an extra COUNT query, so it is off by default.", + "example": false + }, + "required": false, + "description": "Set true to include `total` in the response. Costs an extra COUNT query, so it is off by default.", + "name": "includeTotal", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only testing that ended at or after this instant (ISO-8601). At most 45 days before `until`.", + "example": "2026-09-01T00:00:00Z" + }, + "required": true, + "description": "Only testing that ended at or after this instant (ISO-8601). At most 45 days before `until`.", + "name": "since", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only testing that ended at or before this instant (ISO-8601). Defaults to the time of the first page.", + "example": "2026-09-08T00:00:00Z" + }, + "required": false, + "description": "Only testing that ended at or before this instant (ISO-8601). Defaults to the time of the first page.", + "name": "until", + "in": "query" } ], "responses": { - "204": { - "description": "The queue was deleted" + "200": { + "description": "A page of testing results", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestingResultList" + } + } + } }, - "401": { - "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", + "400": { + "description": "`VALIDATION_FAILED` | `INVALID_CURSOR`", "content": { "application/json": { "schema": { @@ -3650,8 +4735,8 @@ } } }, - "403": { - "description": "`INSUFFICIENT_PERMISSIONS`", + "401": { + "description": "`AUTHENTICATION_REQUIRED` | `INVALID_TOKEN`", "content": { "application/json": { "schema": { @@ -3660,8 +4745,8 @@ } } }, - "404": { - "description": "`MERGE_QUEUE_NOT_FOUND`", + "403": { + "description": "`INSUFFICIENT_PERMISSIONS`", "content": { "application/json": { "schema": { @@ -3670,8 +4755,8 @@ } } }, - "409": { - "description": "`QUEUE_HAS_ACTIVE_ENTRIES`", + "404": { + "description": "`MERGE_QUEUE_NOT_FOUND`", "content": { "application/json": { "schema": { @@ -3849,7 +4934,7 @@ "operationId": "mergeQueues.listPullRequests", "tags": ["mergeQueues"], "summary": "List a merge queue's pull requests", - "description": "Lists the pull requests in a queue, **most recently enqueued first**.\n\n**This is not queue order, and a pull request's position in the queue is not yet exposed** — do not infer position from this list's index. `state` accepts a comma-separated list of queue-entry states. `readiness.hasImpactedTargets` is omitted on this list read, because resolving it costs one upstream call per entry; read a single pull request to get it. `sort` and `direction` are not configurable yet: each accepts only the value this list already uses (`createdAt`, `desc`) and rejects anything else with `400`. `concludedAfter` is not served at all and is rejected with `400` whatever the value. Neither is ignored, so a client cannot silently receive an ordering or a window it did not ask for.", + "description": "Lists the pull requests in a queue, **most recently enqueued first**.\n\n**This is not queue order** — do not infer position from this list's index. For the order the engine will consider, use `GET /v2/merge-queues/{id}/order`. `state` accepts a comma-separated list of queue-entry states. `readiness.hasImpactedTargets` is omitted on this list read, because resolving it costs a storage lookup per entry; read a single pull request to get it. `sort` and `direction` are not configurable yet: each accepts only the value this list already uses (`createdAt`, `desc`) and rejects anything else with `400`. Neither is ignored, so a client cannot silently receive an ordering it did not ask for.\n\n`concludedAfter` and `concludedBefore` keep pull requests that reached `merged`, `failed` or `cancelled` inside that window, both bounds inclusive — v1's `since`/`until`. That is a fact about the entry's history, not its current state: an entry that failed in the window and was later resubmitted still matches, so pair them with `state` to filter on where it is now.", "parameters": [ { "schema": { @@ -3922,6 +5007,30 @@ "description": "Only pull requests opened by this author.", "name": "author", "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only entries that concluded at or after this instant (ISO-8601).", + "example": "2026-09-01T00:00:00Z" + }, + "required": false, + "description": "Only entries that concluded at or after this instant (ISO-8601).", + "name": "concludedAfter", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only entries that concluded at or before this instant (ISO-8601).", + "example": "2026-09-08T00:00:00Z" + }, + "required": false, + "description": "Only entries that concluded at or before this instant (ISO-8601).", + "name": "concludedBefore", + "in": "query" } ], "responses": { @@ -4510,6 +5619,16 @@ } } }, + "422": { + "description": "`ALLOWED_BOT_SUBMITTERS_FROZEN`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "429": { "description": "`RATE_LIMIT_EXCEEDED`", "content": { @@ -4548,7 +5667,7 @@ "operationId": "impactedTargets.upload", "tags": ["impactedTargets"], "summary": "Upload a pull request's impacted targets", - "description": "Records which build targets a pull request's head sha impacts, for queues that gate on impacted targets. Address the pull request by exactly one of `number` or `branch`, and declare impact with exactly one of `impactedTargets` or `impactsAllTargets: true`. An empty `impactedTargets` array is a valid upload meaning the sha impacts nothing — it satisfies the queue's has-impacted-targets gate, because that gate asks whether CI reported, not whether it reported a non-zero result. Supported only for github.com repositories named `owner/repo`; anything else returns `IMPACTED_TARGETS_UNSUPPORTED`. Answers 202: the targets are stored, but readiness is re-evaluated asynchronously, so a 202 does not mean readiness has changed yet.", + "description": "Records which build targets a pull request's head sha impacts, for queues that gate on impacted targets. Address the pull request by exactly one of `number` or `branch`, and declare impact with exactly one of `impactedTargets` or `impactsAllTargets: true`. An empty `impactedTargets` array is a valid upload meaning the sha impacts nothing — it satisfies the queue's has-impacted-targets gate, because that gate asks whether CI reported, not whether it reported a non-zero result. Supported only for github.com repositories named `owner/repo`; anything else returns `IMPACTED_TARGETS_UNSUPPORTED`. Answers 202: the targets are stored, but readiness is re-evaluated asynchronously, so a 202 does not mean readiness has changed yet.\n\nA token from a GitHub Actions run may upload only for its own repository. One from a fork pull request's run is bound to that pull request and its head sha: address the pull request by `number`, and send that sha, or the upload is refused with `INSUFFICIENT_PERMISSIONS`.", "requestBody": { "required": true, "content": { @@ -4651,7 +5770,7 @@ "operationId": "mergeQueueVerificationRuns.get", "tags": ["mergeQueueVerificationRuns"], "summary": "Get a verification run", - "description": "Returns one merge-queue verification run, including its normalized CI checks and the pull requests it tested.", + "description": "Returns one merge-queue verification run, including its normalized CI checks, the pull requests it tested, and the impacted targets uploaded for its commits.", "parameters": [ { "schema": { @@ -5009,7 +6128,7 @@ "operationId": "pullRequests.queueEntryHistory", "tags": ["pullRequests"], "summary": "List a pull request's queue-entry state history", - "description": "Returns the state transitions of this pull request's queue entry, **oldest first** — the one list in this API that does, because it is an append-only log a reader replays forward. To find the CURRENT transition you must page to the end; `hasMore: false` is how you know you are there. A pull request that was never enqueued is a 200 with an empty page, not a 404.", + "description": "Returns the state transitions of this pull request's queue entry. Defaults to **oldest first** — the one list in this API that does, because it is an append-only log a reader replays forward — so finding the CURRENT transition means paging to the end, and `hasMore: false` is how you know you are there. Send `direction=newest` for v1's `includeTimeline` order, where the current transition is the first row of the first page. A pull request that was never enqueued is a 200 with an empty page, not a 404.", "parameters": [ { "schema": { @@ -5059,6 +6178,19 @@ "description": "Set true to include `total` in the response. Costs an extra COUNT query, so it is off by default.", "name": "includeTotal", "in": "query" + }, + { + "schema": { + "type": "string", + "enum": ["oldest", "newest"], + "default": "oldest", + "description": "Order of the transitions within a timeline. `oldest` (the default) reads the log forward; `newest` matches v1's `includeTimeline` order, so the current transition is the first row rather than the last.", + "example": "oldest" + }, + "required": false, + "description": "Order of the transitions within a timeline. `oldest` (the default) reads the log forward; `newest` matches v1's `includeTimeline` order, so the current transition is the first row rather than the last.", + "name": "direction", + "in": "query" } ], "responses": { @@ -5130,7 +6262,7 @@ "operationId": "pullRequests.queueEntryHistoryBatch", "tags": ["pullRequests"], "summary": "List queue-entry state history for several pull requests", - "description": "Returns up to 50 pull requests' complete queue-entry timelines in one response — the batch form of `GET /v2/pull-requests/{id}/queue-entry-history`, and the equivalent of v1's `getSubmittedPullRequests` with `includeTimeline`.\n\n`limit` counts **pull requests**, not transitions, and every group carries that pull request's whole timeline oldest-first, so one call answers for the whole set and each group is complete on arrival. A page's byte size therefore follows the data rather than a fixed row count: for a pull request with an exceptionally long timeline, page within it using the single-pull-request endpoint instead.\n\nAn id naming a pull request in another organization, or one that was never enqueued, comes back as an **empty group** rather than an error — this route never 404s, so diff the ids you asked for against the groups you got.", + "description": "Returns up to 50 pull requests' complete queue-entry timelines in one response — the batch form of `GET /v2/pull-requests/{id}/queue-entry-history`, and the equivalent of v1's `getSubmittedPullRequests` with `includeTimeline`.\n\n`limit` counts **pull requests**, not transitions, and every group carries that pull request's whole timeline (oldest-first by default; send `direction=newest` for v1's `includeTimeline` order), so one call answers for the whole set and each group is complete on arrival. A page's byte size therefore follows the data rather than a fixed row count: for a pull request with an exceptionally long timeline, page within it using the single-pull-request endpoint instead.\n\nAn id naming a pull request in another organization, or one that was never enqueued, comes back as an **empty group** rather than an error — this route never 404s, so diff the ids you asked for against the groups you got.", "parameters": [ { "schema": { @@ -5169,6 +6301,19 @@ "name": "includeTotal", "in": "query" }, + { + "schema": { + "type": "string", + "enum": ["oldest", "newest"], + "default": "oldest", + "description": "Order of the transitions within a timeline. `oldest` (the default) reads the log forward; `newest` matches v1's `includeTimeline` order, so the current transition is the first row rather than the last.", + "example": "oldest" + }, + "required": false, + "description": "Order of the transitions within a timeline. `oldest` (the default) reads the log forward; `newest` matches v1's `includeTimeline` order, so the current transition is the first row rather than the last.", + "name": "direction", + "in": "query" + }, { "schema": { "type": "string", @@ -5250,7 +6395,7 @@ "operationId": "pullRequests.resolveByCoordinates", "tags": ["pullRequests"], "summary": "Resolve pull requests by repository coordinates", - "description": "Looks pull requests up by their provider coordinates within one repository. Send exactly one of `number` or `branch`.\n\n`number` takes up to 50 comma-separated numbers. `(repositoryId, number)` is unique, so each matches at most one pull request; numbers Trunk has not indexed are simply absent from the page, so recover them by diffing what you asked for against the `number`s you got back.\n\n`branch` is a head branch, which can carry several pull requests over time, so it resolves to the open one if there is one and otherwise the most recently created. Pair it with `baseBranch` — a merge queue's target branch — to scope the answer to pull requests aimed there.\n\nRows come back ordered by `number` ascending, which is not necessarily the order you asked in. A batch can exceed `limit`, so this page really does paginate — follow `nextCursor` rather than assuming one page. `readiness.hasImpactedTargets` is resolved only for a lookup that ASKED for a single pull request (`branch`, or one `number`); resolving it costs one upstream call each, so a multi-number lookup omits it whatever it matches, as the queue's list read does.", + "description": "Looks pull requests up by their provider coordinates within one repository. Send exactly one of `number` or `branch`.\n\n`number` takes up to 50 comma-separated numbers. `(repositoryId, number)` is unique, so each matches at most one pull request; numbers Trunk has not indexed are simply absent from the page, so recover them by diffing what you asked for against the `number`s you got back.\n\n`branch` is a head branch, which can carry several pull requests over time, so it resolves to the open one if there is one and otherwise the most recently created. Pair it with `baseBranch` — a merge queue's target branch — to scope the answer to pull requests aimed there.\n\nRows come back ordered by `number` ascending, which is not necessarily the order you asked in. A batch can exceed `limit`, so this page really does paginate — follow `nextCursor` rather than assuming one page. `readiness.hasImpactedTargets` is resolved only for a lookup that ASKED for a single pull request (`branch`, or one `number`); resolving it costs a storage lookup each, so a multi-number lookup omits it whatever it matches, as the queue's list read does.", "parameters": [ { "schema": { @@ -5379,16 +6524,6 @@ } } }, - "404": { - "description": "`REPOSITORY_NOT_FOUND`", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "429": { "description": "`RATE_LIMIT_EXCEEDED`", "content": { @@ -5408,16 +6543,6 @@ } } } - }, - "502": { - "description": "`UPSTREAM_ERROR`", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } } } @@ -5427,7 +6552,7 @@ "operationId": "pullRequests.get", "tags": ["pullRequests"], "summary": "Get a pull request", - "description": "Returns one pull request with its queue entry and readiness, if it has ever been submitted to a queue. `readiness.hasImpactedTargets` is omitted for org-token callers, whose credential the merge service cannot use to read it.", + "description": "Returns one pull request with its queue entry and readiness, if it has ever been submitted to a queue.", "parameters": [ { "schema": { @@ -5484,7 +6609,7 @@ } }, "404": { - "description": "`PR_NOT_FOUND` | `REPOSITORY_NOT_FOUND`", + "description": "`PR_NOT_FOUND`", "content": { "application/json": { "schema": { @@ -5512,16 +6637,6 @@ } } } - }, - "502": { - "description": "`UPSTREAM_ERROR`", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } } } @@ -5665,7 +6780,7 @@ "operationId": "pullRequests.updateQueueEntry", "tags": ["pullRequests"], "summary": "Update a queued pull request's entry", - "description": "Patches a live queue entry; an omitted field leaves that attribute unchanged. `priorityValue` and `priorityName` are mutually exclusive. `skipsTheLine` is not settable — it is derived from `priorityValue === 0`, so send that instead. `isForceEnqueued` force-enqueues past unmet readiness (an org API key is trusted; a user token requires repository-admin permission). `allowBatching: false` gives the entry a test run of its own.", + "description": "Patches a live queue entry; an omitted field leaves that attribute unchanged. `priorityValue` and `priorityName` are mutually exclusive. `skipsTheLine` is not settable — it is derived from `priorityValue === 0`, so send that instead. `direct` sends the entry directly to the queue past unmet readiness (it needs the **Merge PRs with direct to queue** permission, and a signed-in user also needs repository-admin permission). `allowBatching: false` gives the entry a test run of its own.", "parameters": [ { "schema": { @@ -6037,7 +7152,7 @@ "operationId": "dynamicCi.generatePlan", "tags": ["dynamicCi"], "summary": "Generate a CI skip plan", - "description": "Returns a per-job verdict for one workflow run: which jobs the change makes it safe to skip, and the evidence behind each decision. Normally called once per run by the [`trunk-io/dynamic-ci`](https://github.com/trunk-io/dynamic-ci) action, which maps the verdicts onto job outputs.\n\n**Fail open.** A job absent from `jobs` must be run — that is the contract, not an error. Treat any non-200 the same way.\n\nThe changed file set is not sent: Trunk fetches the diff between `baseSha` and `commitSha` through your GitHub App installation.", + "description": "Returns a per-job verdict for one workflow run: which jobs the change makes it safe to skip, and the evidence behind each decision. Normally called once per run by the [`trunk-io/dynamic-ci`](https://github.com/trunk-io/dynamic-ci) action, which maps the verdicts onto job outputs.\n\n**Fail open.** A job absent from `jobs` must be run — that is the contract, not an error. Treat any non-200 the same way.\n\nSend the changed files as `changedFiles` when you can compute them. Without it, Trunk fetches the diff between `baseSha` and `commitSha` through your GitHub App installation, which needs both commits on GitHub.", "requestBody": { "required": true, "content": { @@ -6127,7 +7242,7 @@ "operationId": "dynamicCi.generateBuildkitePlan", "tags": ["dynamicCi"], "summary": "Generate a CI skip plan for a Buildkite pipeline", - "description": "As [`dynamicCi.generatePlan`](#tag/dynamicCi/operation/dynamicCi.generatePlan), for a Buildkite pipeline. Identical request and response but for the workflow identity: Buildkite has no workflow file, so the pipeline is named by the organization and pipeline slugs its agent exports, and `jobKeys` are step `key:` values.\n\n**Fail open.** A job absent from `jobs` must be run — that is the contract, not an error. Treat any non-200 the same way.\n\nThe changed file set is not sent: Trunk fetches the diff between `baseSha` and `commitSha` through your GitHub App installation.", + "description": "As [`dynamicCi.generatePlan`](#tag/dynamicCi/operation/dynamicCi.generatePlan), for a Buildkite pipeline. Identical request and response but for the workflow identity: Buildkite has no workflow file, so the pipeline is named by the organization and pipeline slugs its agent exports, and `jobKeys` are step `key:` values.\n\n**Fail open.** A job absent from `jobs` must be run — that is the contract, not an error. Treat any non-200 the same way.\n\nSend the changed files as `changedFiles` when you can compute them. Without it, Trunk fetches the diff between `baseSha` and `commitSha` through your GitHub App installation, which needs both commits on GitHub.", "requestBody": { "required": true, "content": {