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:

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:

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:

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

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 }'
actionWhat it does
set_shareSets the share to percent (1–100). Raising it only adds installs: nobody who has the new version is moved back.
pauseEveryone gets the current version until you resume. Installs that already updated keep the new one.
resumeContinues the rollout at its share.
completeThe 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.