mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Brings `buzz-media`, `buzz-relay`, all first-party kind-24242 minters, and desktop token minting into compliance with [NIP-FI §kind-24242](https://github.com/block/buzz/blob/main/docs/nips/NIP-FI.md). ## What changed **`buzz-media/src/auth.rs`** - Add `BlossomStrictness` enum (`Strict` | `Permissive`). `Strict` applies full NIP-FI rules (active modes); `Permissive` preserves pre-NIP-FI Off-mode behavior byte-identical [FI-INV-15]. - Rewrite `verify_blossom_auth_event_for_verb` with count-based cardinality: exactly one `t`/`expiration`/`server` in `Strict`; duplicate `x` also rejected. `Permissive` keeps main's boolean semantics exactly: any valued `t` other than the verb (including `""`) rejects, valueless `t` is ignored. - `Strict`: mandatory `server` tag on all proofs (upload + read); absent or mismatched → `evidence_rejected` (`ServerMismatch`). - `Strict`: 60-second proof window (`now - created_at <= 60s`, `expiration <= created_at + 60s`). `Permissive`: 3600s / optional server. - **[F1]** Permissive server validation uses any-match semantics: a proof with multiple `server` tags is accepted if any tag matches the bound host. This restores origin/main base behavior. Strict duplicate-rejection unchanged. - **[F1 compat]** Permissive server gate uses `server_values.is_empty()` (valued tags only) rather than `server_count > 0` (includes valueless tags). A lone valueless `["server"]` tag is treated as absent in Permissive/Off mode, matching pre-NIP-FI base behavior [FI-INV-15]. - **[F4]** Valueless `x` tags in Strict get reads are tracked before `filter_map` so they cannot become absent scope (host-wide authorization). Any `x` tag present in Strict must contain the exact requested sha256. - **[F3 unit]** Cases A–D in `auth.rs` are helper-level unit tests: they call `verify_blossom_upload_auth` and `verify_upload_hash_only` directly. They do not enter the upload pipeline (`process_upload`/`process_video_upload`). Retained as helper evidence. **`buzz-media/src/error.rs`** - Add `DuplicateTag(&'static str)` variant. - Add `blossom_denial_kind()` method: classifies each auth error as `MissingEvidence`, `EvidenceRejected`, or `AuthorizationDenied`. Non-auth errors return `None`. - `IntoResponse` retains the legacy JSON 401 shape [FI-INV-15]. **`buzz-relay/src/api/media.rs`** - Add `MediaDenial(MediaError, BlossomStrictness)` wrapper mapping to byte-exact NIP-FI fixed responses in `Strict`; falls through to legacy JSON in `Permissive`. - `blossom_strictness_from_state()` selects `Strict` when `state.config.nip_fi.is_enforce()`, otherwise `Permissive`. Strictness is threaded through the `admit_nip_fi_http_on_state` NIP-98 closures on upload and read, and through the post-admission X-SHA-256, hash-binding, and membership denials. - `extract_blossom_auth` keeps first-value `Authorization` extraction; Enforce rejects repeated fields at the `admit_nip_fi_http` cardinality gate, and Off keeps legacy behavior [FI-INV-15]. - Both membership-denial call sites use `media_denial(RelayMembershipRequired, strictness)` so Strict mode produces the correct fixed-text response. - `upload_blob` serving-write fence conversion (`:406`) uses `map_err(|e| media_denial(e, strictness))` — a community fence committed between auth admission and lease acquisition no longer resets Strict to Permissive. - Mode-driven regressions in `mod postgres_tests` (selected by the nextest `postgres-ci` profile via `test(/postgres_tests::/)`, `#[ignore = "requires Postgres"]`), run through the full relay router with a real Enforce verifier and assertion: Enforce rejects Permissive-only proofs (300s lifetime, missing `server` tag) on both upload routes and reads with `evidence rejected`; Off accepts the same proofs with pre-NIP-FI responses; DenyProtected answers 503 before any proof check; membership denials are `authorization denied` text/plain in Enforce and exact legacy JSON 403 in Off, with a member positive control. GET and HEAD are each discriminated (HEAD with a suppressed body). Both upload routes pin the handler's `X-SHA-256` missing/malformed/unsigned and post-body hash-mismatch denials in both modes. Off rejects an empty-valued `t` beside a valid one with legacy 401 JSON in either order, and ignores a valueless one. Exact status, Content-Type, challenge, and body bytes are pinned. **`buzz-media/src/upload.rs`** (`mod minio_tests`) - **[F3 pipeline]** Four tests exercising the actual post-body call sites (`process_upload` `:85`, `process_video_upload` `:413`) with real MinIO I/O: buffered/streaming × expired-proof-accept + mismatched-hash-reject. The positive cases use `fresh_strict_upload_auth` (`created_at = now-58`, `expiration = now+2`, lifetime = 60s) — Strict-admissible at sign time — followed by a real 3s sleep so the proof is expired when the upload completes. `nostr::Timestamp::now()` reads the OS wall clock directly; paused Tokio time does not advance it. The negative cases use a pre-expired proof with a mismatched `x` tag. All four are `#[ignore = "requires MinIO"]`; no CI lane runs them automatically. **`buzz-media/src/validation.rs`** - `minimal_valid_mp4()` promoted to `#[cfg(test)] pub(crate)` — a 618-byte pre-computed H.264/avc1 fast-start MP4 available to all `buzz-media` test modules; smoke-tested by `test_minimal_valid_mp4_passes_validate_video_file`. **`desktop/src-tauri/src/commands/media.rs`** - `sign_blossom_upload_auth`: `server` tag mandatory; upload expiry 60s (was 3600s/300s). - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). **`desktop/src-tauri/src/media_proxy.rs`** - Single re-mint+retry on 401/403 for range requests; non-auth failures do not retry. **`crates/buzz-cli/src/client.rs`** - Read expiry: 60s (was 600s). Upload expiry: 60s. `server` tag mandatory on upload. **`crates/buzz-dev-mcp/src/view_image.rs`** - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). Test updated. **`mobile/lib/shared/relay/media_auth.dart`** - `_mediaGetAuthLifetimeSeconds`: 60 (was 600). Refresh margin updated. **`mobile/lib/shared/relay/media_upload.dart`** - `_uploadAuthLifetimeSeconds`: 60 (was 300). `server` tag mandatory. **`mobile/lib/features/channels/media_viewer_page/video_viewer.dart`** - **[F2]** Remove Android-specific `VideoPlayerController.networkUrl` streaming path. `video_player_android` 2.9.5 freezes headers into static `DefaultHttpDataSource` request properties — a 60s proof minted at controller creation time becomes stale on seeks. All platforms use the authenticated local-file download path. - **[F2 transport]** `unawaited(request.sink.close())` before `client.send()`. `AbortableStreamedRequest` carries no request body; without closing the sink, `IOClient.send()` awaits `stream.pipe(ioRequest)` which blocks until the sink ends — every video download hung in loading indefinitely on every platform. - **[F2 pending-ctrl]** `pendingController` ref set before the first `await (initialize())`, cleared in all exit paths. Effect cleanup disposes `pendingController.value` so a close-during-init releases the native player even if the initialized event never arrives. - **[F2r(a)]** On native init failure (post-create, pre-initialized-event), the inner catch starts disposal detached via `unawaited(localController.dispose().catchError(...))` — the `.catchError` handler absorbs any disposal exception so it does not reach the test error zone — then rethrows the original load error. `video_player` 2.11.1 completes the init future with an error without disposing the native player. - **[F2r(b)]** Non-2xx error body cancelled via `_cancelVideoResponse()` (subscribe+cancel) instead of `drain()`. `drain()` waits for the upstream to close; a stalled server body blocked `initializeVideo` indefinitely. - **[F2r(d)]** `unawaited(localController.dispose().catchError(...))` in the catch — not `await`. Two failure mechanisms exist: (1) when `createWithOptions()` throws, `_creatingCompleter` is never completed and `await dispose()` deadlocks at `:682-683`, blocking the outer catch from setting error state. (2) when `createWithOptions()` succeeds but initialization fails, `dispose()` runs but its thrown exception becomes an unhandled async error reaching the test zone. Both are fixed: `unawaited` lets the outer catch run immediately and set `error.value`; `.catchError(...)` on the detached disposal future absorbs any disposal exception. **`mobile/test/features/channels/media_viewer_page/video_viewer_test.dart`** - `_FinalizingFakeClient`: calls `request.finalize().drain()` — a test without the sink fix times out because the drain never completes, validating the transport contract. - F2r(a): `forceInitError=true` fake emits `PlatformException` on subscribe; `disposeCallCount >= 1` verifies disposal even on init failure. - F2r(b): `stalledBody` `StreamController` with `onCancel` Completer (bounded completion signal, not a sleep). Asserts: (1) `bodyStreamCancelled=true` while body stream is still open (impossible with `drain()`); (2) error UI visible while body remains open. Restoring `drain()` breaks both. - F2r(c): `neverInitialize=true` fake never sends initialized event; unmount asserts `disposeCallCount >= 1` via `pendingController`. Distinct from the event-error case. - F2r(d): `forceCreateError=true` fake throws `PlatformException` from `createWithOptions()` itself (before any player ID is returned). Asserts error UI visible within a bounded deadline. With the old `await dispose()` the outer catch is blocked — `error.value` is never set and the error UI assertion fails at the deadline; with `unawaited(dispose())` the error text appears immediately. **`mobile/test/features/channels/media_viewer_page/video_viewer_transport_test.dart`** (separate file) - Real-IO loopback probes using `flutter_test` (not `package:test`) to avoid `depend_on_referenced_packages` lint. No `TestWidgetsFlutterBinding` — no HttpClient override interfering with `dart:io`. - Probe 1: `HttpServer.bind(loopbackIPv4, 0)` reads the full request body before replying. Without `sink.close()`, `req.drain()` hangs and the test times out. - Probe 2: server signals request arrival via a Completer (not a sleep); abort fires after confirmed arrival; `client.send()` must throw `RequestAbortedException` (typed assertion) within a bounded 5s deadline. **`scripts/test-video-upload.sh`** - Proof minted with `+60` expiry and mandatory `server` tag. **`buzz-test-client/tests/e2e_media*.rs`** + **`buzz-relay/src/api/media.rs` test fixtures** - All fixtures updated to 55s expiry + `server` tag. **`RELEASING.md`** - New "NIP-FI upload proof freshness (relay floor)" section documenting that 60s proofs require the first relay release carrying this PR's post-body hash-only check (the first tag > `v0.2.1` — update once cut) for reliable large-file uploads. Operators on older self-hosted relays (≤ `v0.2.1`) must upgrade before receiving these clients. ## Finding 3: 60s upload proofs vs old-relay compatibility Desktop/CLI/mobile upload proofs are now 60s. Older relays re-run full expiry verification after the body is transferred. A valid upload whose body transfer takes >60s will fail on an old relay. **Resolution (relay floor prerequisite):** `RELEASING.md` requires that verified supported deployments (relay releases confirmed to carry the post-body hash-only check, commit `75e9bef748`) be in place before distributing 60s-proof client builds. Unknown or unverified self-hosted relay deployments are excluded from distribution until they upgrade. This makes the verified-deployment status a prerequisite for release distribution, not a post-distribution advisory. The concrete tag (`> v0.2.1`) is to be recorded once cut. ## Sequencing note Builds on #7264 (merged): Blossom strictness is derived from its `config.nip_fi` mode in this PR, so enabling `BUZZ_NIP_FI_MODE=enforce` activates the strict verifier with no further wiring. ## GIF compatibility exception An unauthenticated Off-mode request on a configured tenant with no GIF provider changes from 404 to 401 (base `gifs.rs:269-275` checked an unconfigured provider first; the current code authenticates first). All other Off-mode Blossom responses are byte-identical to pre-NIP-FI behavior. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
378 lines
18 KiB
Markdown
378 lines
18 KiB
Markdown
# Releasing Buzz
|
|
|
|
Buzz has three independent release lanes. Desktop and relay use release PRs.
|
|
Mobile uses immutable release-candidate tags cut directly from remote `main`:
|
|
|
|
| Lane | Entry point | Artifact |
|
|
|------|-------------|----------|
|
|
| Desktop | `just release-desktop <version>` | Packaged desktop app (signed/notarized macOS, unsigned Windows, and Linux) |
|
|
| Relay | `just release-relay` | `ghcr.io/block/buzz` container image |
|
|
| Mobile | `scripts/mobile-release.sh candidate X.Y.Z` | Exact `mobile-vX.Y.Z-rc.N` source identity |
|
|
|
|
The lanes version independently. Desktop reads its manifests, relay reads its
|
|
crate manifest, and mobile derives both source and marketing version from the
|
|
exact candidate tag. The mobile handoff to the private `buzz-releases` pipeline
|
|
remains manual because OSS CI cannot trigger private CI.
|
|
|
|
## Quick Start
|
|
|
|
Prepare desktop releases locally from an up-to-date, clean `main` checkout:
|
|
|
|
```sh
|
|
just release-desktop 0.5.3
|
|
```
|
|
|
|
The recipe generates the immutable candidate and opens or updates its pull
|
|
request. Candidate branch creation uses the operator's GitHub permissions; the
|
|
release App is intentionally limited to creating protected release tags.
|
|
|
|
```sh
|
|
# Relay release
|
|
just release-relay
|
|
just release-relay 0.4.0
|
|
|
|
# Publish the next mobile candidate from the exact current remote main commit
|
|
scripts/mobile-release.sh candidate 0.5.0
|
|
```
|
|
|
|
Desktop uses an immutable generated candidate PR; relay continues using its
|
|
metadata PR. Mobile does not. Each `mobile-vX.Y.Z-rc.N` tag is an immutable
|
|
candidate and the artifact of record.
|
|
There is no mobile release branch, stable mobile tag alias, finalization step,
|
|
or mobile GitHub Release.
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
### Desktop
|
|
|
|
1. Run `just release-desktop <version>` from a clean, up-to-date `main` checkout.
|
|
The script creates one deterministic candidate commit and records both its
|
|
frozen base and the verified prior release ledger in candidate metadata.
|
|
2. Review the exact candidate SHA, complete changelog, and CI. Regenerating or
|
|
pushing the branch creates a new candidate and requires checks to run again.
|
|
3. **Squash merge** the PR after all protected-branch checks pass. The merge is
|
|
the human authorization event; an authorized owner/admin bypass is treated
|
|
the same way. Unrelated changes reaching `main` do not invalidate the
|
|
reviewed candidate.
|
|
4. `auto-tag-on-release-pr-merge` verifies the closed event against GitHub's PR
|
|
identity, validates candidate content, and proves every required check came
|
|
from its trusted producer and was successful when the PR merged. It creates
|
|
`desktop-v<version>` at the exact reviewed PR head—not the squash commit.
|
|
Retries accept that tag only at the same SHA and never move it. GitHub does
|
|
not expose when an individual check rerun was created, so an ordinary rerun
|
|
after merge deliberately makes tag verification fail closed; inspect that
|
|
run and create a new candidate version rather than retrying the blocked tag.
|
|
5. The tag triggers `release.yml`. It builds and stages all platform artifacts,
|
|
publishes the versioned release only after the complete set succeeds, then
|
|
updates the rolling updater manifest last for stable versions.
|
|
|
|
Because squash merging leaves immutable candidate tags on side history, the next
|
|
release uses validated prior candidate metadata as its ledger boundary. It
|
|
includes unrelated commits after the prior frozen base and excludes exactly the
|
|
prior release's recorded squash commit; tag ancestry is deliberately irrelevant.
|
|
|
|
### Relay
|
|
|
|
1. **`just release-relay`** runs locally on `main`, creates or updates a
|
|
`relay-release/<version>` PR, bumps `crates/buzz-relay/Cargo.toml`,
|
|
regenerates `Cargo.lock`, and updates the relay changelog.
|
|
2. **Merge the PR.** `auto-tag-on-release-pr-merge` pushes
|
|
`relay-v<version>`.
|
|
3. **The tag triggers `docker.yml`.** Stable releases update the version
|
|
aliases and `latest`; prereleases do not. Each release also publishes an
|
|
optimized, symbol-bearing image under matching `debug-` tags (for example,
|
|
`debug-0.3.0` and `debug-latest`) for native profiling. The ordinary tags
|
|
remain stripped and are the default for deployments that do not need it.
|
|
|
|
Every push to `main` continues to publish the rolling relay `:main` and
|
|
`:sha-<7>` tags, plus matching `:debug-main` and `:debug-sha-<7>` variants.
|
|
|
|
#### NIP-FI upload proof freshness (relay floor)
|
|
|
|
Desktop, CLI, and mobile clients mint Blossom upload authorization proofs with
|
|
a **60-second expiry** (`expiration = now + 60`). **Older** relays (≤ `v0.2.1`)
|
|
re-run the full auth verifier — including the expiry check — after receiving the
|
|
full upload body, so a 60-second proof that was valid when admitted can expire
|
|
during a large or slow transfer, causing a spurious rejection.
|
|
|
|
The repair (commit `75e9bef748d2149ce459b14da842e706a51a5f78`, landed in this
|
|
PR) replaces the post-body full-verifier call with a hash-only check
|
|
(`verify_upload_hash_only`): the relay verifies the freshness and all other
|
|
proof fields before reading the body; the only check after transfer is that the
|
|
body's SHA-256 matches what was declared in the signed proof.
|
|
|
|
Clients cannot lengthen proofs beyond 60 seconds (NIP-FI §Strict limits the
|
|
max proof window to 60 s). This is a relay-side concern:
|
|
|
|
- **The relay release that includes this PR's changes** (the first relay
|
|
tag > `v0.2.1`, containing commit `75e9bef748d2149ce459b14da842e706a51a5f78`)
|
|
performs a hash-only post-body check, eliminating the expiry window during
|
|
transfer. **Update this line with the concrete relay tag once it is cut.**
|
|
- **Older self-hosted relays** (≤ `v0.2.1`) re-verify the full proof expiry
|
|
after transfer and may reject large uploads over slow connections.
|
|
Upgrading the relay is the fix; no client workaround exists within Strict
|
|
constraints.
|
|
|
|
**Prerequisite before distributing desktop, CLI, or mobile builds that mint
|
|
60-second upload proofs:** verify that all supported relay environments have
|
|
been upgraded to a relay release containing commit
|
|
`75e9bef748d2149ce459b14da842e706a51a5f78`. An unknown or unverified deployment
|
|
does not satisfy this prerequisite. Deployments running ≤ `v0.2.1` must be
|
|
upgraded before receiving these clients.
|
|
|
|
When releasing a new **relay** version that carries the post-body hash-only
|
|
check, record the concrete tag here so release operators can verify the
|
|
prerequisite. When releasing **desktop, CLI, or mobile** for environments where
|
|
self-hosted relays may be running pre-floor versions, document the floor in
|
|
release notes and require operators to upgrade before distributing these clients.
|
|
|
|
### Mobile
|
|
|
|
1. **Publish a candidate.** From a clean checkout whose `origin` is the
|
|
canonical `block/buzz` repository, run
|
|
`scripts/mobile-release.sh candidate X.Y.Z`. The script resolves and fetches
|
|
the exact current `origin/main` commit, derives the next number from exact
|
|
remote tags for that marketing version, and publishes an annotated
|
|
`mobile-vX.Y.Z-rc.N` tag there through the dedicated `buzz-release-bot`
|
|
GitHub App. It never uses the operator's checked-out commit and never moves
|
|
an existing candidate.
|
|
2. **Build the exact tag.** Enter the candidate tag as `mobile_ref` in the
|
|
private Buzz mobile Buildkite pipeline. OSS CI deliberately cannot trigger
|
|
that private pipeline. The tag supplies both source commit and release
|
|
version. Flutter receives clean marketing version `X.Y.Z`; Buildkite's
|
|
monotonically increasing build number supplies the platform build number.
|
|
3. **Promote tested artifacts.** Promote the already-built signed artifact for
|
|
each platform through its store workflow. Record the exact tag with the
|
|
build or rollout record. No source ref is changed and no final build is cut.
|
|
|
|
The iOS and Android artifacts for one marketing version may come from different
|
|
RC tags. For example, iOS can ship `mobile-v0.5.0-rc.2` while Android ships
|
|
`mobile-v0.5.0-rc.3`. Each platform's exact candidate tag is its source record.
|
|
There is intentionally no single selected or final candidate for the marketing
|
|
version.
|
|
|
|
The simplification trades away a separate stabilization line. Unrelated commits
|
|
that reach `main` become part of every later candidate, and there is no retained
|
|
hotfix branch or branch-ancestry history. Add a dedicated hotfix flow later if a
|
|
release actually needs isolation from `main`.
|
|
|
|
`mobile/pubspec.yaml` keeps `0.0.0+1` only as a valid, visibly non-release
|
|
fallback for local development and validation builds. Release jobs always
|
|
inject both version fields. `mobile/CHANGELOG.md` is retained as historical
|
|
release data. It is not a release ledger for this flow.
|
|
|
|
---
|
|
|
|
## Version Sources
|
|
|
|
| Lane | Release version authority |
|
|
|------|---------------------------|
|
|
| Desktop | `desktop/package.json` and synchronized desktop manifests |
|
|
| Relay | `crates/buzz-relay/Cargo.toml` |
|
|
| Mobile | Exact `mobile-vX.Y.Z-rc.N` remote tag |
|
|
|
|
`just bump-desktop-version <version>` updates the desktop manifests and
|
|
regenerates their lockfiles. `just bump-relay-version <version>` updates the
|
|
relay crate and regenerates `Cargo.lock`. Mobile has no bump recipe or
|
|
release-metadata PR.
|
|
|
|
---
|
|
|
|
## Signed macOS Canary
|
|
|
|
Use the manual **Signed macOS Canary** workflow when you need an Apple Silicon
|
|
build of current `main` for explicit testing without publishing a release:
|
|
|
|
```sh
|
|
gh workflow run signed-macos-canary.yml --repo block/buzz --ref main
|
|
```
|
|
|
|
The workflow derives a `-test.<run-number>` version, signs and notarizes the
|
|
DMG, verifies it with Gatekeeper, and uploads it as a short-lived Actions
|
|
artifact with seven-day retention. Because this is a public repository, any
|
|
signed-in GitHub user can download that artifact while it exists; it is
|
|
unpublished, not private. The workflow has no release permissions, does not
|
|
create or move tags, and cannot update `buzz-desktop-latest` or `latest.json`.
|
|
|
|
Download the artifact from the completed run:
|
|
|
|
```sh
|
|
gh run download <run-id> --repo block/buzz --name <artifact-name>
|
|
```
|
|
|
|
The workflow intentionally accepts only `main`. Use the normal release process
|
|
for distributable builds or builds from an immutable release tag.
|
|
|
|
---
|
|
|
|
## Release Retry
|
|
|
|
`release.yml` has no manual dispatch and cannot build from `main` or another
|
|
caller-selected ref. If a run for an existing immutable
|
|
`desktop-v<version>` tag fails, rerun that failed workflow from GitHub Actions
|
|
(or use `gh run rerun <run-id> --failed --repo block/buzz`). A rerun
|
|
repairs the versioned draft if publication did not complete. It does not
|
|
promote that version to the auto-updater; promotion is a separate manual
|
|
action. Do not recreate, move, or push the immutable tag again.
|
|
|
|
Mobile intentionally has no branch or arbitrary-ref fallback. The private
|
|
Buildkite pipeline accepts only an exact candidate tag.
|
|
|
|
---
|
|
|
|
## Internal Releases
|
|
|
|
For mobile, trigger the private
|
|
[Release Mobile pipeline](https://buildkite.com/runway/buzz-mobile-releases) with
|
|
an exact RC tag for the platform build being cut. For desktop, start
|
|
[Release Desktop](https://buildkite.com/runway/sprout-releases) and enter the
|
|
exact public source tag as `desktop_ref=desktop-v<version>`; a generic
|
|
`v<version>` tag is intentionally rejected. See the
|
|
[buzz-releases README](https://github.com/squareup/buzz-releases#cutting-a-release)
|
|
for the rest of the private pipeline contract.
|
|
|
|
---
|
|
|
|
## What Gets Published
|
|
|
|
Desktop publishes two GitHub releases:
|
|
|
|
1. **`desktop-v<version>`**: the user-facing release with installers and the
|
|
exact `updater-manifest.json` promotion candidate. Publishing this release
|
|
does not expose it through in-app auto-update.
|
|
2. **`buzz-desktop-latest`**: the rolling auto-updater release. Its
|
|
`latest.json` changes only through the manual promotion workflow.
|
|
|
|
### Promote an OSS desktop release to auto-update
|
|
|
|
After installing and testing the published `desktop-v<version>` artifacts, run
|
|
**Promote OSS Desktop Auto-Update** from the `main` branch and enter the exact
|
|
stable `X.Y.Z` version. The workflow validates the immutable tag and release,
|
|
the retained manifest and every referenced updater asset, and requires the
|
|
version to be newer than the currently promoted version before replacing
|
|
`buzz-desktop-latest/latest.json`. Same-version retries succeed only when the
|
|
manifest is identical; downgrades are rejected.
|
|
|
|
Withholding promotion leaves existing clients on the previous version. If a
|
|
promoted release is bad, ship and promote a higher patch version; changing the
|
|
manifest to an older version does not downgrade clients that already updated.
|
|
|
|
Mobile publishes only annotated `mobile-vX.Y.Z-rc.N` git tags. Store artifacts
|
|
and rollout records retain the exact tag they used. Mobile does not publish a
|
|
GitHub Release or a stable `mobile-vX.Y.Z` alias.
|
|
|
|
---
|
|
|
|
## Platform Support
|
|
|
|
The release workflow builds **two separate macOS DMGs**: Apple
|
|
Silicon (`darwin-aarch64`, the `release` job) and Intel
|
|
(`darwin-x86_64`, the `release-macos-x64` job), an unsigned Windows x64
|
|
NSIS installer (its filename includes `_alpha-unsigned`), and Linux `.deb` and
|
|
`.AppImage` packages. Both macOS DMGs are codesigned, notarized, and attached
|
|
to the same `desktop-v<version>` release. Intel users
|
|
download the `_x64.dmg`.
|
|
|
|
The Linux AppImage is post-processed by `desktop/scripts/fix-appimage.sh`,
|
|
which strips infra libraries over-bundled by linuxdeploy (they crash on
|
|
Mesa 25+ / GLib 2.88 distros; see
|
|
[tauri-apps/tauri#15665](https://github.com/tauri-apps/tauri/issues/15665))
|
|
and re-signs the artifact. As a result the AppImage relies on the
|
|
host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
|
|
(Ubuntu 22.04 or newer). The `release-linux` job builds inside a
|
|
`ubuntu:22.04` container for broad GLIBC compatibility.
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
- **Write access** to the `block/buzz` GitHub repository
|
|
- An `origin` remote whose configured URL is the canonical `block/buzz`
|
|
repository
|
|
- `gh` CLI authenticated with permission to push the candidate branch and open
|
|
its pull request
|
|
- The Default `main` ruleset configured for squash-only merging, strict required
|
|
checks, stale-review dismissal, and the **Desktop Release Candidate** check
|
|
- Release tag ruleset [`14378754`](https://github.com/block/buzz/rules/14378754)
|
|
active for `desktop-v*` and `mobile-v*`, with creation, update, deletion, and
|
|
non-fast-forward protections and `buzz-release-bot` as its sole always-bypass
|
|
actor
|
|
- The `buzz-release-bot` App credentials configured for GitHub Actions
|
|
- The following **GitHub Actions variables and secrets** configured for the
|
|
desktop release lane:
|
|
|
|
| Name | Kind | Purpose |
|
|
|------|------|---------|
|
|
| `BUZZ_RELEASE_TAGGER_CLIENT_ID` | Variable | GitHub App client ID used to create protected release tags |
|
|
| `BUZZ_RELEASE_TAGGER_PRIVATE_KEY` | Secret | GitHub App private key |
|
|
| `OSX_CODESIGN_ROLE` | Secret | macOS signing role used by `block/apple-codesign-action` |
|
|
| `CODESIGN_S3_BUCKET` | Secret | macOS signing exchange bucket |
|
|
| `BUZZ_UPDATER_PUBLIC_KEY` or `SPROUT_UPDATER_PUBLIC_KEY` | Secret | Tauri updater public key |
|
|
| `TAURI_SIGNING_PRIVATE_KEY` | Secret | Tauri updater private key |
|
|
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Secret | Password for the private key |
|
|
|
|
Mobile candidate publication requires workflow-dispatch access and the existing
|
|
release App because strict tag protection denies direct human creation. The App
|
|
must be installed on `block/buzz`, have Contents write and Metadata read, and
|
|
retain an `always` bypass on the immutable `mobile-v*` tag rules. It does not
|
|
require GitHub Releases permissions, repository Administration permission, or a
|
|
mobile release-branch ruleset. The publisher validates the App token's effective
|
|
`current_user_can_bypass` value rather than reading the ruleset's hidden bypass
|
|
actor list.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### The desktop candidate is stale or cannot be squash merged
|
|
|
|
Do not update the branch manually and do not weaken the ruleset. Run
|
|
`just release-desktop <version>` again from current `main`; this regenerates the
|
|
candidate, reruns CI, and requires a fresh trusted approval on the new exact
|
|
head. The post-merge verifier refuses to tag a squash whose parent differs from
|
|
the recorded candidate base or whose tree differs from the validated PR head.
|
|
|
|
### Local `just release-desktop` fails with "must be on main branch"
|
|
Switch to `main` and pull latest before running the release recipe.
|
|
|
|
### Local `just release-desktop` fails with "working tree is dirty"
|
|
Commit or stash your changes before running the release recipe.
|
|
|
|
### New commits land after publishing a mobile candidate
|
|
|
|
Run `scripts/mobile-release.sh candidate <version>` again after the intended
|
|
fix reaches remote `main`. It publishes a new immutable RC tag at the new exact
|
|
remote commit. Continue referring to each tested or shipped platform artifact by
|
|
its own exact tag.
|
|
|
|
### `scripts/mobile-release.sh candidate` fails because `main` moved during publication
|
|
|
|
The App-backed workflow may already have published the requested immutable RC
|
|
at the prior `main` tip before the operator command detects the race. Do not
|
|
move or delete that tag, and do not treat it as the candidate for current
|
|
`main`. Inspect the run URL from the command output, then rerun
|
|
`scripts/mobile-release.sh candidate <version>` to publish the next RC from the
|
|
new current `main` tip.
|
|
|
|
### A mobile candidate command selects the wrong RC number
|
|
|
|
Do not retry by moving or deleting a tag. Inspect the exact remote `mobile-v*`
|
|
tags and resolve the unexpected state. Candidate numbers are monotonically
|
|
increasing remote identities.
|
|
|
|
### A mobile candidate publication is rejected by repository rules
|
|
|
|
Confirm `buzz-release-bot` remains the sole always-bypass actor for the active
|
|
`mobile-v*` ruleset and that its Actions credentials are available. Do not grant
|
|
direct human creation or weaken update or deletion protection. Existing
|
|
candidate tags must remain immutable.
|
|
|
|
### Auto-updater reports "no update available"
|
|
Verify that the `buzz-desktop-latest` release exists and contains a
|
|
valid `latest.json`. The manifest covers all four platform keys
|
|
(`darwin-aarch64`, `darwin-x86_64`, `linux-x86_64`,
|
|
`windows-x86_64`); a missing entry usually means that platform's
|
|
release job failed. Check the workflow run.
|