Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz
# app launch while keeping the current identity and relay data.
# VITE_BUZZ_FORCE_FRESH_ONBOARDING=true

# ── Browser desktop-download channel ────────────────────────────────────────
# Vite bakes this public GitHub owner/repository into the web bundle. Leave it
# unset to use the upstream release channel. Fork deployments should pass it at
# Docker build time instead, e.g. --build-arg
# VITE_BUZZ_RELEASES_REPOSITORY=Cvv9/buzz.
# VITE_BUZZ_RELEASES_REPOSITORY=block/buzz

# ── Subscription & filtering ─────────────────────────────────────────────────
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
# BUZZ_ACP_SUBSCRIBE=mentions
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ env:
# letter, so use its canonical registry spelling unless a variable overrides it.
IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/cvv9/buzz' }}
PUSH_GATEWAY_IMAGE: ${{ vars.GHCR_PUSH_GATEWAY_IMAGE != '' && vars.GHCR_PUSH_GATEWAY_IMAGE || 'ghcr.io/cvv9/buzz-push-gateway' }}
# Public GitHub release channel embedded in the bundled web app. Forks
# default to their own repository; set the variable only to deliberately
# publish downloads from another trusted release repository.
DESKTOP_RELEASES_REPOSITORY: ${{ vars.BUZZ_DESKTOP_RELEASES_REPOSITORY != '' && vars.BUZZ_DESKTOP_RELEASES_REPOSITORY || github.repository }}

jobs:
build:
Expand Down Expand Up @@ -172,6 +176,8 @@ jobs:
target: runtime
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_BUZZ_RELEASES_REPOSITORY=${{ env.DESKTOP_RELEASES_REPOSITORY }}
# Push by digest, not by tag — the merge job assembles the tags
# into one multi-arch manifest. This is what makes the native-arm
# matrix possible.
Expand All @@ -190,6 +196,8 @@ jobs:
target: runtime-debug
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_BUZZ_RELEASES_REPOSITORY=${{ env.DESKTOP_RELEASES_REPOSITORY }}
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
cache-from: |
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
Expand Down
196 changes: 175 additions & 21 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ ARG EXTRA_CA_CERTS=
# npmjs, so public CI builds are unaffected. Consumed by the web-builder stage.
ARG NPM_REGISTRY=

# Public GitHub owner/repository from which the browser invite page resolves
# desktop packages. This is intentionally a build argument: Vite embeds public
# VITE_* settings in the static bundle and runtime container environment cannot
# change them. Empty keeps source builds on the upstream block/buzz channel.
# Example fork deployment:
# docker build --build-arg VITE_BUZZ_RELEASES_REPOSITORY=Cvv9/buzz ...
ARG VITE_BUZZ_RELEASES_REPOSITORY=

# ─── Stage 1: cargo-chef base ───────────────────────────────────────────────
FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS chef
# Trust an optional corporate-proxy CA before any network fetch (no-op if unset).
Expand Down Expand Up @@ -106,7 +114,10 @@ ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# corepack reads COREPACK_NPM_REGISTRY to fetch the pinned pnpm; pnpm/npm read
# the .npmrc registry for dependency installs.
ARG NPM_REGISTRY
ARG VITE_BUZZ_RELEASES_REPOSITORY
ENV COREPACK_NPM_REGISTRY=${NPM_REGISTRY}
# The browser bundle only reads VITE_* settings at build time.
ENV VITE_BUZZ_RELEASES_REPOSITORY=${VITE_BUZZ_RELEASES_REPOSITORY}
# When using a mirror, disable corepack's npmjs signature check: the mirror
# republishes tarballs without the public registry's provenance signatures, so
# strict verification fails ("No compatible signature found"). Only relaxed on
Expand Down
27 changes: 27 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,33 @@ host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
| `TAURI_SIGNING_PRIVATE_KEY` | Secret | Tauri updater private key |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Secret | Password for the private key |

### Fork-owned desktop releases

`release.yml` derives both the updater manifest endpoint and all versioned
artifact URLs from `github.repository`. For example, a release running in
`Cvv9/buzz` embeds and publishes through `https://github.com/Cvv9/buzz`, while
the `block/buzz` release URLs remain unchanged. The workflow creates the rolling
`buzz-desktop-latest` release on its first stable desktop release.

Non-`block/buzz` repositories use an Apple Developer ID and App Store Connect
notarization path rather than Block's signing service. Configure these repository
settings before pushing a `desktop-v*` tag; the setup job validates them before
starting any platform build:

| Name | Purpose |
|------|---------|
| `APPLE_CERTIFICATE_BASE64` | Base64-encoded Developer ID Application `.p12` certificate |
| `APPLE_CERTIFICATE_PASSWORD` | Password for that `.p12` certificate |
| `APPLE_SIGNING_IDENTITY` | Variable: Developer ID Application signing identity name |
| `APPLE_TEAM_ID` | Variable: Apple Developer Team ID expected in the signed bundle |
| `APPLE_API_KEY_BASE64` | Base64-encoded App Store Connect API-key `.p8` file |
| `APPLE_API_KEY_ID` | Variable: App Store Connect API key ID |
| `APPLE_API_ISSUER_ID` | Variable: App Store Connect API key issuer ID |

The fork still needs `BUZZ_UPDATER_PUBLIC_KEY` (or
`SPROUT_UPDATER_PUBLIC_KEY`) and `TAURI_SIGNING_PRIVATE_KEY`; they must be the
same Tauri updater key pair used to sign every platform's update artifact.

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
Expand Down
118 changes: 118 additions & 0 deletions desktop/scripts/sign-and-notarize-macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ "$#" -ne 3 ]]; then
echo "usage: $0 <app-bundle> <dmg> <entitlements-plist>" >&2
exit 2
fi

app_bundle="$1"
dmg_path="$2"
entitlements_path="$3"

for path in "$app_bundle" "$dmg_path" "$entitlements_path"; do
if [[ ! -e "$path" ]]; then
echo "::error::Required signing input does not exist: $path" >&2
exit 1
fi
done

required=(
APPLE_CERTIFICATE_BASE64
APPLE_CERTIFICATE_PASSWORD
APPLE_SIGNING_IDENTITY
APPLE_TEAM_ID
APPLE_API_KEY_BASE64
APPLE_API_KEY_ID
APPLE_API_ISSUER_ID
)
missing=()
for name in "${required[@]}"; do
if [[ -z "${!name:-}" ]]; then
missing+=("$name")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "::error::Missing Apple signing credentials: ${missing[*]}" >&2
exit 1
fi

temp_root="${RUNNER_TEMP:-/tmp}/buzz-apple-signing-${RANDOM}-${RANDOM}"
keychain_path="${temp_root}.keychain-db"
keychain_password="$(uuidgen)"
certificate_path="${temp_root}.p12"
api_key_path="${temp_root}.p8"
rw_dmg_base="${temp_root}-rw"
rw_dmg_path="${rw_dmg_base}.dmg"
signed_dmg_base="${temp_root}-signed"
signed_dmg_path="${signed_dmg_base}.dmg"
app_notary_zip="${temp_root}-app.zip"
mount_path="$(mktemp -d "${RUNNER_TEMP:-/tmp}/buzz-dmg-mount.XXXXXX")"
mounted=false

cleanup() {
set +e
if [[ "$mounted" == true ]]; then
hdiutil detach "$mount_path" -quiet
fi
rm -rf "$mount_path" "$keychain_path" "$certificate_path" "$api_key_path" \
"$rw_dmg_path" "$signed_dmg_path" "$app_notary_zip"
}
trap cleanup EXIT

submit_for_notarization() {
xcrun notarytool submit "$1" --key "$api_key_path" \
--key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER_ID" --wait
}

# macOS uses BSD base64, whose decode flag is -D. Keep decoded credential files
# in the runner's temporary directory and remove them via the trap above.
printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 -D > "$certificate_path"
printf '%s' "$APPLE_API_KEY_BASE64" | base64 -D > "$api_key_path"
chmod 600 "$certificate_path" "$api_key_path"

security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$APPLE_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
-k "$keychain_password" "$keychain_path"

# Sign the final app bundle with hardened runtime. The app-level entitlements
# must be applied only to the outer bundle; --deep signs nested frameworks and
# sidecars without copying those entitlements into their individual signatures.
codesign --force --deep --options runtime --timestamp --keychain "$keychain_path" \
--sign "$APPLE_SIGNING_IDENTITY" --entitlements "$entitlements_path" "$app_bundle"
codesign --verify --deep --strict --verbose=2 "$app_bundle"

actual_team_id="$(codesign -dvvv "$app_bundle" 2>&1 | sed -n 's/^TeamIdentifier=//p' | head -1)"
if [[ "$actual_team_id" != "$APPLE_TEAM_ID" ]]; then
echo "::error::Signing certificate team '$actual_team_id' does not match APPLE_TEAM_ID" >&2
exit 1
fi

# The updater delivers the tarball, not the DMG. Notarize and staple the app
# itself before it is packaged so an offline Gatekeeper assessment succeeds
# after an in-app update as well as after an initial DMG installation.
ditto -c -k --keepParent "$app_bundle" "$app_notary_zip"
submit_for_notarization "$app_notary_zip"
xcrun stapler staple "$app_bundle"
xcrun stapler validate "$app_bundle"

# Preserve the Finder layout/background generated by Tauri. A compressed DMG is
# read-only, so replace the app in a temporary writable image, then recompress.
hdiutil convert "$dmg_path" -format UDRW -o "$rw_dmg_base"
hdiutil attach "$rw_dmg_path" -readwrite -noverify -noautoopen \
-mountpoint "$mount_path" >/dev/null
mounted=true
rm -rf "$mount_path/Buzz.app"
ditto "$app_bundle" "$mount_path/Buzz.app"
hdiutil detach "$mount_path" -quiet
mounted=false
hdiutil convert "$rw_dmg_path" -format UDZO -o "$signed_dmg_base"
mv "$signed_dmg_path" "$dmg_path"

submit_for_notarization "$dmg_path"
xcrun stapler staple "$dmg_path"
xcrun stapler validate "$dmg_path"
6 changes: 5 additions & 1 deletion desktop/src/features/settings/hooks/use-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ const BACKGROUND_BLOCKED_STATES = new Set<UpdateStatus["state"]>([
"manual-required",
]);

const GITHUB_RELEASES_URL = "https://github.com/block/buzz/releases/latest";
// Release builds inject the repository that owns their signed updater
// artifacts. Local OSS builds retain the canonical manual-download page.
const GITHUB_RELEASES_URL =
import.meta.env.VITE_BUZZ_RELEASES_URL ??
"https://github.com/block/buzz/releases/latest";

function toErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
Expand Down
148 changes: 148 additions & 0 deletions docs/varvik-desktop-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# VarVik Desktop Release Channel

This document describes the independent native-app channel for `Cvv9/buzz`.
It must be configured before distributing the first VarVik Windows or macOS
installer.

The web application and the desktop application are separate artifacts:

- A web/relay deployment changes server-backed data immediately: messages,
channels, agent records, and avatars.
- A native UI or Tauri change needs a desktop release. An updater-enabled
desktop install checks at launch and every six hours, downloads a verified
update, then asks the user to install and relaunch it.

Treat every user-facing Buzz product release as coordinated: deploy the web
build and publish a higher `desktop-v<VERSION>` containing the corresponding
desktop implementation. Server-only data/configuration deployments do not need
an empty native rebuild.

Do not point VarVik builds at the upstream `block/buzz` updater. It can replace
a customized desktop client with an upstream build.

## Release channel

The release workflow derives the endpoint and asset URLs from the repository
that publishes the release. For this fork those URLs are:

```text
https://github.com/Cvv9/buzz/releases/download/buzz-desktop-latest/latest.json
https://github.com/Cvv9/buzz/releases/download/desktop-v<VERSION>/<asset>
```

The website download links, desktop manual-update link, updater endpoint, and
generated `latest.json` asset URLs must always use that same release channel.
The workflow creates the rolling `buzz-desktop-latest` release automatically
when publishing the first stable desktop release.
The local release-candidate script derives its changelog links from `origin`,
so the configured `origin` remote must remain `https://github.com/Cvv9/buzz.git`
when publishing from this fork.

## Identity and signing

Complete this setup before the first installer is distributed:

1. Use a new, permanent Tauri updater key pair for VarVik. Every future update
must be signed by its private key and verified against its embedded public
key. Back up the private key in the organization password vault; rotating it
later does not update already-installed clients.
2. Keep the current bundle identifier for the first VarVik release so the
existing pilot installation and its local data can migrate cleanly. Changing
the identifier later requires an explicit data/keychain migration and a
one-time reinstall.
3. Non-upstream releases use VarVik's Apple Developer ID certificate and App
Store Connect API key. The workflow signs, notarizes, and staples the `.app`
before creating the updater archive, then notarizes and staples the DMG.
4. Authenticode-sign the Windows NSIS installer when distributing beyond the
pilot. The existing workflow labels
it `_alpha-unsigned`; Tauri updater signatures protect update integrity, but
do not prevent Windows SmartScreen / "Unknown publisher" warnings.

## GitHub configuration

Configure these names in the `Cvv9/buzz` repository before using the current
workflow. Never put the actual values in this repository.

| GitHub setting | Name | Purpose |
| --- | --- | --- |
| Secret | `BUZZ_UPDATER_PUBLIC_KEY` | Public half of the permanent Tauri updater key, embedded into release builds. |
| Secret | `TAURI_SIGNING_PRIVATE_KEY` | Private half used to sign updater archives. |
| Secret | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the updater private key. |
| Secret | `APPLE_CERTIFICATE_BASE64` | Base64 Developer ID Application `.p12` certificate. |
| Secret | `APPLE_CERTIFICATE_PASSWORD` | Password for the `.p12` certificate. |
| Variable | `APPLE_SIGNING_IDENTITY` | Full Developer ID Application signing identity. |
| Variable | `APPLE_TEAM_ID` | Apple Developer team ID expected in the signed app. |
| Secret | `APPLE_API_KEY_BASE64` | Base64 App Store Connect API-key `.p8` file. |
| Variable | `APPLE_API_KEY_ID` | App Store Connect API key ID. |
| Variable | `APPLE_API_ISSUER_ID` | App Store Connect issuer ID. |

The optional release-candidate automation additionally uses
`BUZZ_RELEASE_TAGGER_CLIENT_ID` (variable) and
`BUZZ_RELEASE_TAGGER_PRIVATE_KEY` (secret). They are not needed when an
authorized maintainer creates the immutable `desktop-v*` tag manually.

Generate the updater key outside the repository, then store the two halves in
the listed GitHub secrets:

```sh
cd desktop
pnpm tauri signer generate --write-keys /secure/location/varvik-buzz-updater.key
```

The command prints the public key and writes the private key. Do not commit the
written key or share it through chat.

## First release checklist

1. Add the required signing secrets and variables above. Configure the optional
GitHub App if automatic tagging is retained, and protect `desktop-v*` tags
against modification/deletion.
2. Validate the repository configuration without exposing secret values:

```sh
bash scripts/verify-varvik-desktop-release-readiness.sh
```

From PowerShell on Windows, invoke the Git-for-Windows shell explicitly
rather than the WSL `bash` shim:

```powershell
& 'C:\Program Files\Git\bin\bash.exe' scripts/verify-varvik-desktop-release-readiness.sh
```

3. Cut `desktop-v0.5.4` or a higher stable version. Publish the versioned
release first, then upload `latest.json` last. Confirm it contains
`darwin-aarch64`, `darwin-x86_64`, and `windows-x86_64` entries that point
to assets in the same VarVik release channel.
4. Install this first updater-enabled installer manually on Windows and each
Mac architecture. Later coordinated desktop releases update in place;
server-only deployments continue to synchronize through the relay.

## Verification after publishing

On a clean device, install the matching package:

- Apple Silicon Mac: `Buzz_<version>_aarch64.dmg`
- Intel Mac: `Buzz_<version>_x64.dmg`
- Windows: the Authenticode-signed x64 NSIS installer

Sign in to the same Buzz identity and production community. The client should
show the same server-backed agents, channels, messages, and avatars as the web
app. Native notification permission, Inbox/read-state UI, and local settings
remain per-device.

For the next release, use the in-app update check. It should discover the
update from the VarVik `latest.json`, download it, and offer **Install and
relaunch** without requiring a new installer.

## Signing hardening still recommended

The current Windows job deliberately publishes an `_alpha-unsigned` installer.
That does not break Tauri's updater verification, but it does cause an unknown
publisher / SmartScreen warning. Before distributing beyond the pilot, add an
Authenticode signing step backed by a VarVik certificate (or a hardware-backed
signing provider) and treat its credentials as release secrets.

The macOS signing path is already wired for a Developer ID Application
certificate and App Store Connect API key. The release workflow stops during
setup with the exact missing names rather than producing an unsigned Mac build.
Loading
Loading