# Releases

> Promoting a build into a channel, cutting a version, and rolling back.

Canonical: https://docs.extension.dev/builds/releases
This page as Markdown: https://docs.extension.dev/builds/releases.md
Section: Console: build and release
Siblings: [Console overview](https://docs.extension.dev/console/overview.md) · [Usage and limits](https://docs.extension.dev/console/usage.md) · [Builds](https://docs.extension.dev/builds/overview.md) · [Channels](https://docs.extension.dev/builds/channels.md) · [Store releases](https://docs.extension.dev/builds/store-releases.md) · [Promote gates](https://docs.extension.dev/builds/promote-gates.md) · [Integrity](https://docs.extension.dev/builds/integrity.md)
Map of every page: https://docs.extension.dev/llms.txt · Whole corpus: https://docs.extension.dev/llms-full.txt

---

# Releases

A release moves one existing build into one channel and records everything
that follows. Reach for it when a build is ready for its audience, when you
want a version cut, or when a channel needs to go back to the build it held
before. Promotion never rebuilds.

## Quick start

```
POST /api/projects/{id}/releases
{ "buildId": "<short-sha>" }
```

The build lands in `stable`, one promote run dispatches per browser, and the
channel's event log gains a promotion event.


## How it works

### Promoting

The body names `buildId`, `channel`, `sourceChannel` and `browsers`.
`buildId` is required. Leave `channel` out and the release lands in `stable`.
Leave `browsers` out and the build's own browsers are promoted, `chrome` when
the build names none. One promote run dispatches per browser, correlated by
a minted dispatch id.

The console's new release form starts on `stable` once it has shipped, else
on the first channel that has, else on `preview`. The promotion is checked
before it runs. Every refusal is in [Promote gates](/builds/promote-gates).

### Cutting a version

```
POST /api/projects/{id}/releases/cut
{ "level": "patch" }
```

`level` is `patch`, `minor` or `major`, or pass `version` to name one. Only
the project owner can cut. The cut bumps the manifest version in your source
repository and opens a release PR.

A cut can be armed. The release PR body carries a checked task line, and an
armed cut promotes the moment the matching build goes green, so merging the
PR ships.

### The event log

Every promotion appends an event to the channel's log: who promoted, which
build entered the channel, and which build it replaced. That log is the
record a release page reads, and the only thing rollback can act on.

In event and rollback URLs, the release id is the channel name. To read
`stable`'s events or roll `stable` back, use `stable` as the id. The events
route also accepts a per-browser id: `edge-preview` reads `preview`, because
it strips a leading `chrome-`, `firefox-`, `edge-` or `safari-` and reads the
rest as the channel. Rollback takes the id whole, so give it the channel name
alone.

### What a release page reports

A promotion writes the channel pointer when it dispatches, so the pointer
says a promotion was asked for. The status on the release page is read from
the promote run instead: `live`, `promoting`, `failed` or `unconfirmed`, one
per browser and folded into one verdict. See [Channels](/builds/channels)
for the fold.

`GET /api/projects/{id}/releases/promotion-runs?ids=...` returns the runs
behind the verdict, keyed by the dispatch ids you pass. Each one is the run
it matched or `found: false`. Pass `since` to bound how far back it looks.

### Rollback

```
POST /api/projects/{id}/releases/{channel}/rollback
```

Rollback reads the channel's newest promotion event and promotes the build
that promotion replaced. It is a promotion like any other, and it is owner
only. The browser list comes from the channel pointers, one dispatch each,
and a channel with no browser pinned rolls `chrome` back. Only the dispatches
GitHub accepts count against the build allowance.

A target resolves from one thing: the newest promotion event, carrying an
earlier build that differs from the one the channel holds now. Three states
give no target: the channel's first promotion, a promotion of the build the
channel already held, and an empty or missing event log. All three answer
`409 NO_ROLLBACK_TARGET`, and all three hide the control for owners.

A history the platform could not read is a fourth state and a different
answer. The API says `503 ROLLBACK_HISTORY_UNREADABLE` rather than reporting
a channel with no earlier build. The release page shows that state instead
of hiding it: a line saying the history could not be read, and a **Try
again** button. While the history loads, the page holds the space.

A member who is not an owner sees the **Roll back** control disabled, with a
note that only workspace owners roll a channel back, whether or not a target
exists.

A rollback that dispatches but cannot re-pin the channel record answers
`200` with `status: "degraded"`. The builds went out, and the pointer needs
another promotion to agree with them.

Rolling back a channel does not roll back a store. A build already published
stays published until a new submission replaces it. Roll the channel back,
then submit again from it.

### GitHub Releases

Tagging a GitHub Release for each promotion is per-project opt-in, off by
default. The toggle **Create a GitHub Release on promote** sits in
**Project settings, Release channels**.

## Reference

### Routes

| Route | What it does | Owner only |
| --- | --- | --- |
| `POST /api/projects/{id}/releases` | Promote a build into a channel | no |
| `POST /api/projects/{id}/releases/cut` | Bump the manifest version and open a release PR | yes |
| `GET /api/projects/{id}/releases/{channel}/events` | Read the channel's event log | no |
| `GET /api/projects/{id}/releases/promotion-runs?ids=...` | Read the promote runs behind a verdict | yes |
| `POST /api/projects/{id}/releases/{channel}/rollback` | Promote the build the newest promotion replaced | yes |

### Promote body

| Field | Required | Default |
| --- | --- | --- |
| `buildId` | yes | |
| `channel` | no | `stable` |
| `sourceChannel` | no | `preview` for a promote to `preview`, else the target channel |
| `browsers` | no | The build's browsers, `chrome` when it names none |

### Cut body

| Field | Values |
| --- | --- |
| `level` | `patch`, `minor` or `major` |
| `version` | One to four dot separated integers from 0 to 65535 |

### Events response

| Field | Meaning |
| --- | --- |
| `items` | The events, newest first |
| `total`, `limit`, `offset`, `hasMore` | Paging |
| `truncated`, `maxItems` | The log is capped at 500 events |

### Rollback answers

| Answer | Meaning |
| --- | --- |
| `200` | Dispatched. `status: "degraded"` when the channel record could not be re-pinned |
| `409 NO_ROLLBACK_TARGET` | No earlier build to return to |
| `503 ROLLBACK_HISTORY_UNREADABLE` | The history could not be read |
| `404 RELEASE_WORKFLOW_NOT_FOUND` | The mirror carries no release workflow |
| `502 ROLLBACK_DISPATCH_FAILED` | Every browser's dispatch failed |

## When it fails

- **No rollback control on the release page.** The channel has no earlier
  build to return to. The control appears once a second promotion gives the
  log something to point back at. Promote the build you want by id with
  `POST /api/projects/{id}/releases` in the meantime.
- **`409 NO_ROLLBACK_TARGET`.** The same state, seen from the API.
- **`503 ROLLBACK_HISTORY_UNREADABLE`.** The promotion history did not load,
  so whether there is an earlier build is unknown. Retry. On the release page
  this is the **Try again** button.
- **`404 RELEASE_WORKFLOW_NOT_FOUND`.** A target resolved, and the mirror
  carries no release workflow. Reconnect the project under **Project
  settings, GitHub integration**.
- **`502 ROLLBACK_DISPATCH_FAILED`.** Every browser's dispatch failed and
  nothing moved. Retry. A run where some browsers dispatched answers `200`
  and names the ones that did not.
- **Rollback or cut refused for a collaborator.** Both are owner only. Ask
  the project owner.
- **An empty event log for a channel that holds a release.** Check the id.
  The events route strips a leading browser name, so a channel whose own
  name starts with `chrome-`, `firefox-`, `edge-` or `safari-` loses that
  prefix: `edge-nightly` reads `nightly` and comes back empty. Any other
  name is read whole.
- **Promotion refused with a code.** See
  [Promote gates](/builds/promote-gates) for every code and its fix.
- **Armed cut never shipped.** It fires only when the matching build goes
  green. Find the build with `GET /api/projects/{id}/builds` and read its
  logs. To watch one run, `GET /api/projects/{id}/builds/live-status?runId=<runId>`
  returns its status and conclusion, and answers `400` without a run id.

## Next

- [Promote gates](/builds/promote-gates)
- [Channels](/builds/channels)
- [Publishing](/publish/overview)
