# Channels and staged rollouts

A channel (`stable`, or one you add such as `beta`) serves one release per platform to every install that checks it. A **staged rollout** gives a newer release to a share of installs first, so a bad build reaches a few people instead of everyone. The rest keep the release the channel serves until you complete the rollout.

## 1. Send an install id with update checks

Mocco decides who is in a rollout from an id the app sends with each update check, in the `X-Mocco-Install-Id` header. Make it a random UUID, created once and stored, so the same install always lands on the same side. An install that sends no id (or one that isn't a UUID) always gets the release everyone gets, so apps that skip this step keep working and simply never take part in rollouts.

With the JavaScript API of `tauri-plugin-updater`, pass it through the `headers` option of `check`:

```ts
import { check } from '@tauri-apps/plugin-updater';

function installId(): string {
  const stored = localStorage.getItem('mocco-install-id');
  if (stored !== null) {
    return stored;
  }
  const created = crypto.randomUUID();
  localStorage.setItem('mocco-install-id', created);
  return created;
}

const update = await check({ headers: { 'X-Mocco-Install-Id': installId() } });
if (update !== null) {
  await update.downloadAndInstall();
}
```

From Rust, add the header on the updater builder:

```rust
use tauri_plugin_updater::UpdaterExt;

let update = app
    .updater_builder()
    .header("X-Mocco-Install-Id", install_id)?
    .build()?
    .check()
    .await?;
```

Keep the id somewhere that survives updates (the app's data folder or store). It identifies an install, not a person: don't derive it from an account or a device serial.

## 2. Start a rollout

Publish with a share below 100. With the publish step of your workflow, or with curl:

```sh
curl -fsS -X POST "$MOCCO_API/desktop/releases/$RELEASE_ID/publish" \
  -H "Authorization: Bearer $MOCCO_KEY" -H 'Content-Type: application/json' \
  -d '{ "channel": "stable", "rolloutPercent": 10 }'
```

10% of installs that send an id now get the new version; everyone else keeps the current one. The version must be newer than what the channel serves, and newer than a rollout already running there. Publishing without `rolloutPercent` (or at 100) gives the release to everyone at once, as before.

The static `latest.json`, the `/latest` download links and the releases page keep showing the current version until the rollout is complete.

## 3. Raise, pause, resume or complete it

Change the rollout of a channel with one call (a secret key with `desktop:write`, like the publish):

```sh
curl -fsS -X POST "$MOCCO_API/desktop/apps/$APP_ID/channels/stable/rollout" \
  -H "Authorization: Bearer $MOCCO_KEY" -H 'Content-Type: application/json' \
  -d '{ "action": "set_share", "percent": 50 }'
```

| `action` | What it does |
| --- | --- |
| `set_share` | Sets the share to `percent` (1–100). Raising it only adds installs: nobody who has the new version is moved back. |
| `pause` | Everyone gets the current version until you resume. Installs that already updated keep the new one. |
| `resume` | Continues the rollout at its share. |
| `complete` | The new version becomes what the channel serves to everyone, and shows on `latest.json`, `/latest` and the releases page. |

The call applies to every platform rolling out. Add `"platform": "darwin-aarch64"` to change one platform only; you need it when platforms roll out different versions. Add `"reason"` to say why: it shows in the channel's history and the audit log.

If the new version turns out bad, withdraw it (`POST /v1/desktop/releases/{id}/yank`). That ends its rollout everywhere, and the current version stays.

## On a protected channel

On a channel that needs approval, what reaches more installs waits for someone to approve it: starting a rollout, raising the share, resuming and completing answer `202` with a `requestId`, and nothing changes until the request is approved in Mocco. Pausing and lowering the share make things safer, so they apply at once and the team reviews them afterwards.

## In the console

### The Channels tab

Open your project, then **Desktop releases → Channels**. Each channel shows:

- its name, which opens the channel's page;
- **Listed** or **Unlisted**: listed channels show on the public releases page and in the public release list;
- what it serves now, platform by platform.

Owners and admins also see **List** / **Unlist** per channel and the **Add channel** form; other members read the same page.

#### Add a channel

1. Type the name: lowercase letters, digits and hyphens, starting with a letter (for example `beta`).
2. Tick **List on the releases page** if the public page should show it.
3. Select **Add channel**.

Point a build at it by setting `?channel=beta` in its updater endpoint, and publish to it from the release page, the Action (`channel`), the CLI (`--channel`) or `/v1`.

#### Unlisted is not private

Unlisting only stops advertising a channel. The updater, `latest.json` and the `/latest/<platform>` downloads still serve an unlisted channel to anyone who knows its name. Don't use an unlisted channel for builds that must stay secret.

### A channel's page

Select a channel's name to open its page:

- **Serving now**: per platform, the version the channel serves and since when, or **Serves nothing**. A version opens its release. When a staged rollout is running, the platform also shows the new version, its share and whether it's paused.
- **Rollout**: while a rollout runs, **Set share**, **Pause** or **Resume**, and **Complete**, the same actions as the `rollout` call above. On a protected channel the page says so; raising the share, resuming and completing then send a request for approval (**Request approval**), and pausing or lowering the share applies at once.
- **History**: every change to what the channel serves (publishes, rollout changes and withdrawals), newest first, with who made it and the reason given. Choose **7 days**, **30 days** or **All**; the choice is part of the page's address, so you can share the exact view.
- **List** / **Unlist** for owners and admins, with the same note about unlisted channels.
