mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): add OSS release workflow and auto-updater support (#461)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Semver version (e.g. 0.4.0)"
|
||||
required: true
|
||||
ref:
|
||||
description: "Branch, tag, or SHA to build"
|
||||
default: main
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
steps:
|
||||
- name: Validate version
|
||||
run: |
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "::error::Invalid version '$VERSION'. Expected semver (e.g. 0.4.0 or 1.0.0-beta.1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: just desktop-install-ci
|
||||
|
||||
- name: Patch version
|
||||
run: |
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "$VERSION"
|
||||
cd src-tauri && cargo generate-lockfile
|
||||
|
||||
- name: Generate release config
|
||||
run: cd desktop && node scripts/build-release-config.mjs
|
||||
env:
|
||||
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
|
||||
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
|
||||
|
||||
- name: Build sidecars
|
||||
run: |
|
||||
cargo build --release -p sprout-acp -p sprout-mcp -p git-credential-nostr
|
||||
./scripts/bundle-sidecars.sh
|
||||
|
||||
- name: Build Tauri app
|
||||
run: cd desktop && pnpm tauri build --config src-tauri/tauri.release.conf.json
|
||||
env:
|
||||
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
|
||||
SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
||||
|
||||
- name: Locate build artifacts
|
||||
id: artifacts
|
||||
run: |
|
||||
BUNDLE_DIR="desktop/src-tauri/target/release/bundle"
|
||||
|
||||
# Find the DMG (Tauri names it Sprout_<version>_<arch>.dmg)
|
||||
DMG=$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -type f | head -1)
|
||||
if [[ -z "$DMG" ]]; then
|
||||
echo "::error::No DMG found in $BUNDLE_DIR/dmg"
|
||||
exit 1
|
||||
fi
|
||||
echo "dmg=$DMG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Find the updater .tar.gz and .sig
|
||||
ARCHIVE=$(find "$BUNDLE_DIR/macos" -name '*.tar.gz' ! -name '*.sig' -type f | head -1)
|
||||
SIG="${ARCHIVE}.sig"
|
||||
if [[ -z "$ARCHIVE" || ! -f "$SIG" ]]; then
|
||||
echo "::error::Updater archive or signature not found in $BUNDLE_DIR/macos"
|
||||
exit 1
|
||||
fi
|
||||
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
|
||||
echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT"
|
||||
echo "sig=$SIG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate latest.json
|
||||
run: |
|
||||
bash desktop/scripts/generate-oss-latest-json.sh \
|
||||
"$VERSION" \
|
||||
"$SIG_PATH" \
|
||||
"https://github.com/block/sprout/releases/download/sprout-desktop-latest/$ARCHIVE_NAME" \
|
||||
> latest.json
|
||||
cat latest.json
|
||||
env:
|
||||
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
|
||||
ARCHIVE_NAME: ${{ steps.artifacts.outputs.archive_name }}
|
||||
|
||||
- name: Create versioned GitHub release
|
||||
run: |
|
||||
RELEASE_SHA=$(git rev-parse HEAD)
|
||||
gh release create "v${VERSION}" \
|
||||
--target "$RELEASE_SHA" \
|
||||
--title "Sprout Desktop v${VERSION}" \
|
||||
--notes "Sprout Desktop v${VERSION}" \
|
||||
"$DMG_PATH"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DMG_PATH: ${{ steps.artifacts.outputs.dmg }}
|
||||
|
||||
- name: Update rolling release for auto-updater
|
||||
run: |
|
||||
gh release create sprout-desktop-latest \
|
||||
--prerelease \
|
||||
--title "Sprout Desktop Auto-Update" \
|
||||
--notes "Rolling release for the Tauri auto-updater. Do not download manually — use the versioned release instead." \
|
||||
2>/dev/null || true
|
||||
gh release upload sprout-desktop-latest \
|
||||
latest.json \
|
||||
"$ARCHIVE_PATH" \
|
||||
"$SIG_PATH" \
|
||||
--clobber
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARCHIVE_PATH: ${{ steps.artifacts.outputs.archive }}
|
||||
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# Releasing Sprout Desktop
|
||||
|
||||
This document describes how to create a new OSS release of the Sprout
|
||||
desktop app.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Write access** to the `block/sprout` GitHub repository. Only
|
||||
collaborators with push permissions can trigger the release workflow.
|
||||
- The following **GitHub Actions secrets** must be configured on the repo
|
||||
(Settings > Secrets and variables > Actions):
|
||||
|
||||
| Secret | Purpose |
|
||||
|--------|---------|
|
||||
| `SPROUT_UPDATER_PUBLIC_KEY` | Tauri updater public key (minisign) |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY` | Tauri updater private key (used to sign the update archive) |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the private key |
|
||||
|
||||
---
|
||||
|
||||
## Creating a Release
|
||||
|
||||
1. Go to **Actions > Release** in the GitHub UI:\
|
||||
`https://github.com/block/sprout/actions/workflows/release.yml`
|
||||
|
||||
2. Click **"Run workflow"**.
|
||||
|
||||
3. Fill in the inputs:
|
||||
- **version** — a semver version string, e.g. `0.4.0` or `1.0.0-beta.1`.
|
||||
Do not include a `v` prefix.
|
||||
- **ref** — the branch, tag, or commit SHA to build from. Defaults to
|
||||
`main`.
|
||||
|
||||
4. Click **"Run workflow"** to start the build.
|
||||
|
||||
The workflow will:
|
||||
|
||||
- Validate the version string
|
||||
- Check out the specified ref
|
||||
- Patch the version into `package.json`, `tauri.conf.json`, and `Cargo.toml`
|
||||
- Build all sidecar binaries (`sprout-acp`, `sprout-mcp`,
|
||||
`git-credential-nostr`)
|
||||
- Build the Tauri desktop app with updater signing enabled
|
||||
- Create a versioned GitHub release (`v0.4.0`) with the `.dmg` installer
|
||||
- Update the rolling `sprout-desktop-latest` release with the signed
|
||||
update archive and `latest.json` manifest for the auto-updater
|
||||
|
||||
---
|
||||
|
||||
## What Gets Published
|
||||
|
||||
Each release produces two GitHub releases:
|
||||
|
||||
1. **`v<version>`** (e.g. `v0.4.0`) — the user-facing release with the
|
||||
`.dmg` installer. This is what users download manually.
|
||||
|
||||
2. **`sprout-desktop-latest`** — a rolling pre-release used by the Tauri
|
||||
auto-updater. Contains `latest.json`, the signed `.tar.gz` archive,
|
||||
and its `.sig` signature. Users should not download from this release
|
||||
directly.
|
||||
|
||||
---
|
||||
|
||||
## Platform Support
|
||||
|
||||
The release workflow currently builds for **macOS ARM64 only**
|
||||
(`darwin-aarch64`). Intel Mac (`darwin-x86_64`) support would require
|
||||
adding a matrix build to the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Updater
|
||||
|
||||
The desktop app checks for updates by fetching `latest.json` from the
|
||||
`sprout-desktop-latest` release:
|
||||
|
||||
```
|
||||
https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
|
||||
```
|
||||
|
||||
When a new version is available, the app downloads the signed archive,
|
||||
verifies the signature against the embedded public key, and applies the
|
||||
update.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build fails at "Validate version"
|
||||
The version string must be valid semver: `MAJOR.MINOR.PATCH` with an
|
||||
optional pre-release suffix (e.g. `1.0.0-beta.1`). Do not include a `v`
|
||||
prefix.
|
||||
|
||||
### Build fails at "Build Tauri app"
|
||||
Check that the signing secrets are configured correctly. The build
|
||||
requires `TAURI_SIGNING_PRIVATE_KEY` and
|
||||
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` to be set.
|
||||
|
||||
### Auto-updater reports "no update available"
|
||||
Verify that the `sprout-desktop-latest` release exists and contains a
|
||||
valid `latest.json`. If the user is on Intel Mac, no update will be
|
||||
found (ARM64 only).
|
||||
@@ -0,0 +1,56 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// Write a tauri.release.conf.json alongside sprout's base tauri.conf.json.
|
||||
//
|
||||
// For OSS release builds this script:
|
||||
// 1. Sets bundle.macOS.minimumSystemVersion = "10.15" for broad compatibility.
|
||||
// 2. Configures plugins.updater with the public key and endpoint from env vars.
|
||||
// Both SPROUT_UPDATER_PUBLIC_KEY and SPROUT_UPDATER_ENDPOINT are required —
|
||||
// the script fails if either is missing (OSS builds always ship with updater).
|
||||
// 3. Sets bundle.createUpdaterArtifacts = true so Tauri automatically produces
|
||||
// the .tar.gz archive and .sig signature during the build.
|
||||
|
||||
const baseConfigPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const outputConfigPath = resolve(
|
||||
process.cwd(),
|
||||
"src-tauri/tauri.release.conf.json",
|
||||
);
|
||||
const baseConfig = JSON.parse(readFileSync(baseConfigPath, "utf-8"));
|
||||
|
||||
const releaseConfig = { ...baseConfig };
|
||||
|
||||
const updaterPubkey = process.env.SPROUT_UPDATER_PUBLIC_KEY;
|
||||
const updaterEndpoint = process.env.SPROUT_UPDATER_ENDPOINT;
|
||||
|
||||
const missing = [];
|
||||
if (!updaterPubkey) missing.push("SPROUT_UPDATER_PUBLIC_KEY");
|
||||
if (!updaterEndpoint) missing.push("SPROUT_UPDATER_ENDPOINT");
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`Error: required environment variable(s) missing: ${missing.join(", ")}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
releaseConfig.bundle = {
|
||||
...(releaseConfig.bundle ?? {}),
|
||||
macOS: {
|
||||
...(releaseConfig.bundle?.macOS ?? {}),
|
||||
minimumSystemVersion: "10.15",
|
||||
},
|
||||
createUpdaterArtifacts: true,
|
||||
};
|
||||
|
||||
releaseConfig.plugins = {
|
||||
...(releaseConfig.plugins ?? {}),
|
||||
updater: {
|
||||
pubkey: updaterPubkey,
|
||||
endpoints: [updaterEndpoint],
|
||||
},
|
||||
};
|
||||
|
||||
console.log(`Updater enabled -> ${updaterEndpoint}`);
|
||||
|
||||
writeFileSync(outputConfigPath, `${JSON.stringify(releaseConfig, null, 2)}\n`);
|
||||
console.log(`Wrote ${outputConfigPath}`);
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 3 ]]; then
|
||||
echo "Usage: generate-oss-latest-json.sh <version> <sig-file> <archive-url>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
SIG_FILE="$2"
|
||||
ARCHIVE_URL="$3"
|
||||
|
||||
# Only darwin-aarch64 is included because the workflow builds on ARM64 runners
|
||||
# only. Supporting Intel Macs (darwin-x86_64) would require a matrix build.
|
||||
jq -n \
|
||||
--arg version "$VERSION" \
|
||||
--arg notes "Sprout v$VERSION" \
|
||||
--arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg signature "$(cat "$SIG_FILE")" \
|
||||
--arg url "$ARCHIVE_URL" \
|
||||
'{ version: $version, notes: $notes, pub_date: $pub_date, platforms: { "darwin-aarch64": { signature: $signature, url: $url } } }'
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error("Usage: node scripts/set-version-from-tag.mjs <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
console.error(
|
||||
`Invalid version "${version}". Expected semver format (e.g. 1.2.3 or 1.2.3-beta.1)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonPath = resolve(process.cwd(), "package.json");
|
||||
const tauriConfigPath = resolve(process.cwd(), "src-tauri/tauri.conf.json");
|
||||
const cargoTomlPath = resolve(process.cwd(), "src-tauri/Cargo.toml");
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.version = version;
|
||||
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
console.log(`Set package.json to ${version}`);
|
||||
|
||||
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, "utf8"));
|
||||
tauriConfig.version = version;
|
||||
writeFileSync(tauriConfigPath, `${JSON.stringify(tauriConfig, null, 2)}\n`);
|
||||
console.log(`Set tauri.conf.json to ${version}`);
|
||||
|
||||
const cargoToml = readFileSync(cargoTomlPath, "utf8");
|
||||
const updatedCargoToml = cargoToml.replace(
|
||||
/^version = ".*"$/m,
|
||||
`version = "${version}"`,
|
||||
);
|
||||
writeFileSync(cargoTomlPath, updatedCargoToml);
|
||||
console.log(`Set Cargo.toml to ${version}`);
|
||||
Reference in New Issue
Block a user