Remove release pipeline from public repo (#360)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-04-18 07:55:09 -06:00
committed by GitHub
parent 13a14b11ec
commit cca1d51012
17 changed files with 36 additions and 1383 deletions

View File

@@ -1,79 +0,0 @@
# Reusable workflow to bundle the Sprout desktop app for macOS (arm64).
# Called from release.yml — not triggered directly.
on:
workflow_call:
inputs:
signing:
description: 'Whether to perform signing and notarization'
required: false
default: false
type: boolean
ref:
description: 'Git ref to checkout'
required: false
type: string
default: ''
secrets:
OSX_CODESIGN_ROLE:
required: false
CODESIGN_S3_BUCKET:
required: false
name: Reusable workflow to bundle desktop app for macOS
jobs:
bundle-desktop-macos:
runs-on: macos-latest
name: Bundle Desktop App (macOS arm64)
timeout-minutes: 45
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.ref != '' && inputs.ref || '' }}
- name: Activate Hermit
uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: desktop/src-tauri
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Build Tauri app
run: cd desktop && pnpm tauri build
env:
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
- name: Codesign and Notarize
if: ${{ inputs.signing }}
id: codesign
uses: block/apple-codesign-action@679535d1ab7c5a7c18e6f9afcba3464512cc3dde # v1.1.0
with:
osx-codesign-role: ${{ secrets.OSX_CODESIGN_ROLE }}
codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }}
unsigned-artifact-path: desktop/src-tauri/target/release/bundle/macos/Sprout.app
artifact-name: sprout-${{ github.sha }}-${{ github.run_id }}-arm64
- name: Package signed app
if: ${{ inputs.signing }}
env:
SIGNED_PATH: ${{ steps.codesign.outputs.signed-artifact-path }}
run: cp "$SIGNED_PATH" Sprout-darwin-arm64.zip
- name: Package unsigned app
if: ${{ !inputs.signing }}
run: ditto -c -k --keepParent desktop/src-tauri/target/release/bundle/macos/Sprout.app Sprout-darwin-arm64.zip
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: Sprout-darwin-arm64
path: Sprout-darwin-arm64.zip
retention-days: 1

View File

@@ -1,154 +0,0 @@
name: Canary Release
on:
workflow_run:
workflows: ["CI"]
branches: [main]
types: [completed]
workflow_dispatch:
concurrency:
group: release
cancel-in-progress: true
permissions: {}
jobs:
resolve:
if: |
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Resolve commit SHA
id: resolve
env:
WF_SHA: ${{ github.event.workflow_run.head_sha }}
FALLBACK_SHA: ${{ github.sha }}
run: echo "sha=${WF_SHA:-$FALLBACK_SHA}" >> "$GITHUB_OUTPUT"
build:
needs: resolve
permissions:
contents: read
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
target:
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-musl
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.resolve.outputs.sha }}
- name: Activate Hermit
uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: ${{ matrix.target }}
- name: Install cross
uses: taiki-e/install-action@06203676c62f0d3c765be3f2fcfbebbcb02d09f5 # v2
with:
tool: cross@0.2.5
- name: Build binaries
env:
TARGET: ${{ matrix.target }}
run: |
cross build --release --target "$TARGET" \
-p sprout-relay \
-p sprout-acp \
-p sprout-mcp
- name: Package tarballs
env:
TARGET: ${{ matrix.target }}
run: |
BIN="target/${TARGET}/release"
mkdir sprout-relay && cp "${BIN}/sprout-relay" sprout-relay/
tar czf "sprout-relay-canary-${TARGET}.tar.gz" sprout-relay
mkdir sprout-agent && cp "${BIN}/sprout-acp" "${BIN}/sprout-mcp-server" sprout-agent/
tar czf "sprout-agent-canary-${TARGET}.tar.gz" sprout-agent
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: sprout-${{ matrix.target }}
path: sprout-*-canary-${{ matrix.target }}.tar.gz
retention-days: 1
bundle-desktop-macos:
needs: resolve
uses: ./.github/workflows/bundle-desktop-macos.yml
permissions:
id-token: write
contents: read
with:
ref: ${{ needs.resolve.outputs.sha }}
signing: true
secrets:
OSX_CODESIGN_ROLE: ${{ secrets.OSX_CODESIGN_ROLE }}
CODESIGN_S3_BUCKET: ${{ secrets.CODESIGN_S3_BUCKET }}
release:
needs: [resolve, build, bundle-desktop-macos]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.resolve.outputs.sha }}
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: dist/
merge-multiple: true
- name: Get build timestamp
id: timestamp
run: echo "ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Force-move canary tag
env:
COMMIT_SHA: ${{ needs.resolve.outputs.sha }}
run: |
git tag -f canary "$COMMIT_SHA"
git push origin canary --force
- name: Create or update canary release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
with:
tag: canary
name: "Canary (latest main)"
body: |
Pre-alpha canary build. Not for production.
Built from commit `${{ needs.resolve.outputs.sha }}` at ${{ steps.timestamp.outputs.ts }}.
**Relay**: `sprout-relay-canary-<target>.tar.gz`
**Agent tooling** (ACP harness + MCP server): `sprout-agent-canary-<target>.tar.gz`
**Desktop** (macOS arm64): `Sprout-darwin-arm64.zip`
prerelease: true
allowUpdates: true
removeArtifacts: true
artifacts: "dist/*.tar.gz,dist/*.zip"
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,164 +0,0 @@
name: Desktop Release
on:
push:
tags:
- 'desktop/v*'
env:
CARGO_TERM_COLOR: always
jobs:
release:
name: Desktop Release
runs-on: macos-latest
permissions:
id-token: write
contents: write
env:
RELEASE_VERSION: '' # set in the "Extract version from tag" step
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- run: corepack enable pnpm
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: desktop/src-tauri
key: desktop-release-aarch64-apple-darwin
- name: Extract version from tag
run: |
RELEASE_VERSION="${GITHUB_REF#refs/tags/desktop/v}"
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV"
echo "Release version: ${RELEASE_VERSION}"
- name: Set version from tag
working-directory: desktop
run: node scripts/set-version-from-tag.mjs "$RELEASE_VERSION"
- name: Regenerate Cargo lockfile
working-directory: desktop/src-tauri
run: cargo generate-lockfile
- name: Validate release secrets
env:
SPROUT_RELAY_URL: ${{ secrets.SPROUT_RELAY_URL }}
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
OSX_CODESIGN_ROLE: ${{ secrets.OSX_CODESIGN_ROLE }}
CODESIGN_S3_BUCKET: ${{ secrets.CODESIGN_S3_BUCKET }}
run: |
missing=()
for name in \
SPROUT_RELAY_URL \
SPROUT_UPDATER_PUBLIC_KEY \
TAURI_SIGNING_PRIVATE_KEY \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
OSX_CODESIGN_ROLE \
CODESIGN_S3_BUCKET; do
if [ -z "${!name}" ]; then
missing+=("$name")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
echo "::error::Missing required desktop release secrets: ${missing[*]}"
exit 1
fi
- name: Install dependencies
run: |
cd desktop && pnpm install --frozen-lockfile
cd src-tauri && cargo fetch
- name: Build release config
working-directory: desktop
env:
SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }}
SPROUT_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/download/sprout-desktop-latest/latest.json
run: pnpm run tauri:release:config
- name: Build Tauri app
working-directory: desktop
env:
SPROUT_RELAY_URL: ${{ secrets.SPROUT_RELAY_URL }}
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
run: pnpm tauri build --config src-tauri/tauri.release.conf.json
- name: Codesign and Notarize
id: codesign
uses: block/apple-codesign-action@679535d1ab7c5a7c18e6f9afcba3464512cc3dde # v1.1.0
with:
osx-codesign-role: ${{ secrets.OSX_CODESIGN_ROLE }}
codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }}
unsigned-artifact-path: desktop/src-tauri/target/release/bundle/macos/Sprout.app
artifact-name: sprout-${{ github.sha }}-${{ github.run_id }}-arm64
- name: Replace with signed app
env:
SIGNED_PATH: ${{ steps.codesign.outputs.signed-artifact-path }}
run: |
BUNDLE_DIR="desktop/src-tauri/target/release/bundle/macos"
rm -rf "${BUNDLE_DIR}/Sprout.app"
unzip -o "$SIGNED_PATH" -d "${BUNDLE_DIR}"
- name: Create DMG from signed app
run: |
brew install create-dmg
DMG_DIR="desktop/src-tauri/target/release/bundle/dmg"
mkdir -p "${DMG_DIR}"
rm -f "${DMG_DIR}"/*.dmg
set +e
create-dmg \
--volname "Sprout" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 128 \
--icon "Sprout.app" 150 200 \
--app-drop-link 450 200 \
--hide-extension "Sprout.app" \
--no-internet-enable \
"${DMG_DIR}/Sprout_${RELEASE_VERSION}_aarch64.dmg" \
"desktop/src-tauri/target/release/bundle/macos/Sprout.app"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ] && [ $EXIT_CODE -ne 2 ]; then
exit $EXIT_CODE
fi
- name: Create updater archive from signed app
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
BUNDLE_DIR="desktop/src-tauri/target/release/bundle/macos"
rm -f "${BUNDLE_DIR}/Sprout.app.tar.gz" "${BUNDLE_DIR}/Sprout.app.tar.gz.sig"
tar czf "${BUNDLE_DIR}/Sprout.app.tar.gz" -C "${BUNDLE_DIR}" Sprout.app
cd desktop && pnpm tauri signer sign -k "$TAURI_SIGNING_PRIVATE_KEY" -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "src-tauri/target/release/bundle/macos/Sprout.app.tar.gz"
- name: Create versioned release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--title "Sprout Desktop v${RELEASE_VERSION}" \
--notes "See the assets to download and install this version."
- name: Publish updater alias
working-directory: desktop
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
VERSION: ${{ env.RELEASE_VERSION }}
run: pnpm run release:updater:publish
- name: Publish DMG alias
working-directory: desktop
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
VERSION: ${{ env.RELEASE_VERSION }}
run: pnpm run release:dmg:publish

View File

@@ -1,175 +0,0 @@
# Releasing the Sprout Desktop App
This guide covers the end-to-end process for releasing the Sprout desktop app,
including secrets setup, cutting releases, and troubleshooting.
---
## Prerequisites / Secrets Setup
The following GitHub repository secrets must be configured before the first
release:
| Secret | Description |
| ---------------------------------- | ------------------------------------------------------------------ |
| `SPROUT_RELAY_URL` | WebSocket relay URL baked into the release binary (for example `wss://...`) |
| `SPROUT_UPDATER_PUBLIC_KEY` | Tauri updater public key (generate with `pnpm tauri signer generate`) |
| `TAURI_SIGNING_PRIVATE_KEY` | Tauri updater private key |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the private key |
| `OSX_CODESIGN_ROLE` | IAM role ARN for Block's Apple codesigning service |
| `CODESIGN_S3_BUCKET` | S3 bucket for codesigning artifacts |
---
## Generating Tauri Updater Keys
```bash
cd desktop
pnpm tauri signer generate -w ~/.tauri/sprout.key
```
This generates a keypair:
- The **public key** goes in the `SPROUT_UPDATER_PUBLIC_KEY` secret.
- The **private key** goes in the `TAURI_SIGNING_PRIVATE_KEY` secret.
Store the password you chose in `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`.
---
## Cutting a Release
From any branch (typically `main`):
```bash
just desktop-release 0.3.0
```
Or equivalently:
```bash
git tag desktop/v0.3.0
git push origin desktop/v0.3.0
```
That's it. CI extracts the version from the tag and writes it into
`package.json`, `tauri.conf.json`, and `Cargo.toml` at build time. The
versions checked into the repo are not used for releases — the tag is the
source of truth.
### Verify
Check GitHub Releases for:
- The **versioned release** (e.g. `desktop/v0.3.0`)
- The **`sprout-desktop-latest` rolling release** (updated with every release)
---
## What CI Does
The `sprout-desktop-release.yml` workflow:
1. **Extracts the version** from the git tag once into `RELEASE_VERSION`.
2. **Sets the version** into `package.json`, `tauri.conf.json`, and
`Cargo.toml` using `set-version-from-tag.mjs`.
3. **Regenerates `Cargo.lock`** to match the patched `Cargo.toml`.
4. **Validates** all required secrets are present.
5. **Builds** the release config with the updater public key and endpoint.
6. **Builds** the Tauri app (unsigned).
7. **Signs and notarizes** the macOS bundle via `block/apple-codesign-action`.
8. **Re-packages** the signed app into a DMG and updater archive.
9. **Signs** the updater archive with the Tauri updater key.
10. **Publishes** the updater manifest (`latest.json`) to the rolling
`sprout-desktop-latest` release.
11. **Publishes** the DMG to both the versioned and rolling releases.
---
## Local Release Build (Testing)
Local builds will not be codesigned or notarized — that only happens in CI
via `block/apple-codesign-action`. Local builds are useful for testing the
updater runtime config and DMG packaging.
```bash
# Set updater env vars
export SPROUT_UPDATER_PUBLIC_KEY="your-public-key"
export SPROUT_UPDATER_ENDPOINT="https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json"
# Generate release config
cd desktop
pnpm run tauri:release:config
# Build (unsigned) — pass a version to set it before building
just desktop-release-build version=0.3.0
```
You can also set the version separately without building:
```bash
just desktop-set-version 0.3.0
```
---
## Auto-Updates
The app uses `tauri-plugin-updater` to check for updates. The updater endpoint
is:
```
https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json
```
This `latest.json` is updated on every release and contains the download URL
and signature for the latest version.
---
## Relay URL Configuration
The app connects to the relay via the `SPROUT_RELAY_URL` environment variable.
- **Production releases**: The GitHub release workflow builds the app with
`SPROUT_RELAY_URL` sourced from the `SPROUT_RELAY_URL` repository secret,
which is baked into the release binary as its default relay URL.
- **Local release builds**: Export `SPROUT_RELAY_URL` before running
`just desktop-release-build` if you want a non-localhost relay URL compiled
into the app.
- **Development**: If not set, it defaults to `ws://localhost:3000`.
---
## How Versioning Works
The git tag is the single source of truth for the release version. The version
fields in `package.json`, `tauri.conf.json`, and `Cargo.toml` on `main` are
**not** used for releases — CI overwrites them at build time from the tag.
This means the tagged commit will show a different version in its source files
than what the release actually contains. This is an accepted tradeoff — the tag
is the canonical version, the commit is just the code state at release time.
This is standard practice in ecosystems like Docker, Go, and Rust where the
tag drives the version.
---
## Troubleshooting
- **"Missing required desktop release secrets"**: Ensure all secrets listed in
[Prerequisites](#prerequisites--secrets-setup) are configured in GitHub repo
settings.
- **Codesigning failures**: Verify `OSX_CODESIGN_ROLE` and
`CODESIGN_S3_BUCKET` are configured correctly. Check the
`block/apple-codesign-action` step logs for details.
- **Build failures**: If versions are wrong, check that the tag follows the
format `desktop/v<semver>` (e.g. `desktop/v0.3.0`). CI extracts the version
from the tag automatically.
- **"A public key has been found, but no private key"**: The Tauri build should
not require `TAURI_SIGNING_PRIVATE_KEY`. If you see this, the build is trying
to generate updater artifacts before the signed app bundle exists. The updater
archive is supposed to be created and signed later from the notarized app.

View File

@@ -18,10 +18,7 @@
"test:e2e:smoke": "pnpm build && playwright test --project=smoke",
"test:e2e:integration": "pnpm build && playwright test --project=integration",
"test:e2e:report": "playwright show-report",
"tauri:build": "tauri build",
"tauri:release:config": "node scripts/build-release-config.mjs",
"release:dmg:publish": "node scripts/publish-dmg-to-github-release.mjs",
"release:updater:publish": "node scripts/publish-updater-to-github-release.mjs"
"tauri:build": "tauri build"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
@@ -42,7 +39,6 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.0",
"@tiptap/core": "^3.22.3",
"@tiptap/extension-link": "^3.22.3",
"@tiptap/extension-placeholder": "^3.22.3",

46
desktop/pnpm-lock.yaml generated
View File

@@ -62,9 +62,6 @@ importers:
'@tauri-apps/plugin-process':
specifier: ^2.3.1
version: 2.3.1
'@tauri-apps/plugin-updater':
specifier: ^2.10.0
version: 2.10.1
'@tiptap/core':
specifier: ^3.22.3
version: 3.22.3(@tiptap/pm@3.22.3)
@@ -79,7 +76,7 @@ importers:
version: 3.22.3
'@tiptap/react':
specifier: ^3.22.3
version: 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
version: 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tiptap/starter-kit':
specifier: ^3.22.3
version: 3.22.3
@@ -1325,9 +1322,6 @@ packages:
'@tauri-apps/plugin-process@2.3.1':
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
'@tauri-apps/plugin-updater@2.10.1':
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
'@tiptap/core@3.22.3':
resolution: {integrity: sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==}
peerDependencies:
@@ -1343,6 +1337,12 @@ packages:
peerDependencies:
'@tiptap/core': ^3.22.3
'@tiptap/extension-bubble-menu@3.22.3':
resolution: {integrity: sha512-Y6zQjh0ypDg32HWgICEvmPSKjGLr39k3aDxxt/H0uQEZSfw4smT0hxUyyyjVjx68C6t6MTnwdfz0hPI5lL68vQ==}
peerDependencies:
'@tiptap/core': ^3.22.3
'@tiptap/pm': ^3.22.3
'@tiptap/extension-bullet-list@3.22.3':
resolution: {integrity: sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==}
peerDependencies:
@@ -1369,6 +1369,13 @@ packages:
peerDependencies:
'@tiptap/extensions': ^3.22.3
'@tiptap/extension-floating-menu@3.22.3':
resolution: {integrity: sha512-0f8b4KZ3XKai8GXWseIYJGdOfQr3evtFbBo3U08zy2aYzMMXWG0zEF7qe5/oiYp2aZ95edjjITnEceviTsZkIg==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': ^3.22.3
'@tiptap/pm': ^3.22.3
'@tiptap/extension-gapcursor@3.22.3':
resolution: {integrity: sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==}
peerDependencies:
@@ -3639,10 +3646,6 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/plugin-updater@2.10.1':
dependencies:
'@tauri-apps/api': 2.10.1
'@tiptap/core@3.22.3(@tiptap/pm@3.22.3)':
dependencies:
'@tiptap/pm': 3.22.3
@@ -3655,6 +3658,13 @@ snapshots:
dependencies:
'@tiptap/core': 3.22.3(@tiptap/pm@3.22.3)
'@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.22.3(@tiptap/pm@3.22.3)
'@tiptap/pm': 3.22.3
optional: true
'@tiptap/extension-bullet-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))':
dependencies:
'@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)
@@ -3676,6 +3686,13 @@ snapshots:
dependencies:
'@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)
'@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)':
dependencies:
'@floating-ui/dom': 1.7.6
'@tiptap/core': 3.22.3(@tiptap/pm@3.22.3)
'@tiptap/pm': 3.22.3
optional: true
'@tiptap/extension-gapcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))':
dependencies:
'@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)
@@ -3766,7 +3783,7 @@ snapshots:
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
'@tiptap/react@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
'@tiptap/react@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@tiptap/core': 3.22.3(@tiptap/pm@3.22.3)
'@tiptap/pm': 3.22.3
@@ -3777,6 +3794,11 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)
'@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.22.3':
dependencies:

View File

@@ -1,45 +0,0 @@
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const publicKey = process.env.SPROUT_UPDATER_PUBLIC_KEY;
const endpoint = process.env.SPROUT_UPDATER_ENDPOINT;
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 };
releaseConfig.bundle.macOS = {
...(releaseConfig.bundle?.macOS ?? baseConfig.bundle?.macOS ?? {}),
minimumSystemVersion: "10.15",
};
if (publicKey && endpoint) {
// Build-time updater artifacts are created later from the signed app bundle.
releaseConfig.plugins = {
...(baseConfig.plugins ?? {}),
updater: {
pubkey: publicKey,
endpoints: [endpoint],
},
};
console.log(`Updater config enabled (${endpoint})`);
} else {
const missing = [];
if (!publicKey) missing.push("SPROUT_UPDATER_PUBLIC_KEY");
if (!endpoint) missing.push("SPROUT_UPDATER_ENDPOINT");
if (releaseConfig.plugins) {
delete releaseConfig.plugins.updater;
if (Object.keys(releaseConfig.plugins).length === 0) {
delete releaseConfig.plugins;
}
}
console.log(`Updater config skipped (missing: ${missing.join(", ")})`);
}
writeFileSync(outputConfigPath, `${JSON.stringify(releaseConfig, null, 2)}\n`);
console.log(`Wrote ${outputConfigPath}`);

View File

@@ -1,187 +0,0 @@
import { execFileSync } from "node:child_process";
import { join, resolve } from "node:path";
import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
const repo = process.env.GITHUB_REPOSITORY ?? "block/sprout";
const version = requireVersionEnv();
const versionTag = `desktop/v${version}`;
const latestTag = "sprout-desktop-latest";
const dryRun = process.env.DRY_RUN === "true" || process.env.DRY_RUN === "1";
const dmgName = `Sprout_${version}_aarch64.dmg`;
const latestAssetName = "Sprout-latest-aarch64.dmg";
const defaultDmgDirs = [
"src-tauri/target/aarch64-apple-darwin/release/bundle/dmg",
"src-tauri/target/release/bundle/dmg",
];
const dmgDir = resolve(
process.cwd(),
process.env.DMG_BUNDLE_DIR ??
defaultDmgDirs.find((dir) =>
existsSync(resolve(process.cwd(), dir, dmgName)),
) ??
defaultDmgDirs[0],
);
const dmgPath = join(dmgDir, dmgName);
function requireVersionEnv() {
const v = process.env.VERSION;
if (!v || !v.trim()) {
throw new Error(
"VERSION env var is required. CI sets this from the git tag; for local use, run: VERSION=x.y.z pnpm run release:dmg:publish",
);
}
return v.trim();
}
function quote(arg) {
if (/^[a-zA-Z0-9._:/=-]+$/.test(arg)) return arg;
return `'${arg.replace(/'/g, `'\\''`)}'`;
}
function runGh(args, options = {}) {
return execFileSync("gh", args, options);
}
function releaseExists(tag) {
try {
runGh(["release", "view", tag, "--repo", repo], { stdio: "ignore" });
return true;
} catch {
return false;
}
}
function ensureRelease(tag, { title, prerelease }) {
if (releaseExists(tag)) {
return;
}
const args = [
"release",
"create",
tag,
"--repo",
repo,
"--title",
title,
"--notes",
"Automated release placeholder.",
];
if (prerelease) {
args.push("--prerelease");
}
runGh(args, { stdio: "inherit" });
}
function readReleaseAssets(tag) {
const assetsRaw = runGh(
[
"release",
"view",
tag,
"--repo",
repo,
"--json",
"assets",
"--jq",
".assets[].name",
],
{ encoding: "utf-8" },
);
return assetsRaw
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function assetUrl(tag, name) {
return `https://github.com/${repo}/releases/download/${tag}/${encodeURIComponent(name)}`;
}
function main() {
if (!existsSync(dmgPath)) {
throw new Error(`Missing DMG artifact: ${dmgPath}`);
}
const stageDir = mkdtempSync(join(tmpdir(), "sprout-release-dmg-"));
const latestAliasPath = join(stageDir, latestAssetName);
try {
cpSync(dmgPath, latestAliasPath);
const uploadVersionedArgs = [
"release",
"upload",
versionTag,
dmgPath,
"--repo",
repo,
"--clobber",
];
const uploadLatestArgs = [
"release",
"upload",
latestTag,
latestAliasPath,
"--repo",
repo,
"--clobber",
];
console.log(`Preparing DMG upload for ${repo}`);
console.log(`- version tag: ${versionTag}`);
console.log(`- latest tag: ${latestTag}`);
console.log(`- versioned asset: ${dmgName}`);
console.log(`- latest alias: ${latestAssetName}`);
if (dryRun) {
console.log("DRY_RUN enabled. Skipping upload.");
console.log(`gh release view ${quote(versionTag)} --repo ${quote(repo)}`);
console.log(
`gh release view ${quote(latestTag)} --repo ${quote(repo)} || gh release create ${quote(latestTag)} --repo ${quote(repo)} --title ${quote("Sprout Desktop Latest")} --notes ${quote("Automated release placeholder.")}`,
);
console.log(`gh ${uploadVersionedArgs.map(quote).join(" ")}`);
console.log(`gh ${uploadLatestArgs.map(quote).join(" ")}`);
console.log(`Versioned URL: ${assetUrl(versionTag, dmgName)}`);
console.log(`Channel URL: ${assetUrl(latestTag, latestAssetName)}`);
return;
}
ensureRelease(versionTag, {
title: `Sprout v${version}`,
prerelease: false,
});
ensureRelease(latestTag, {
title: "Sprout Desktop Latest",
prerelease: false,
});
runGh(uploadVersionedArgs, { stdio: "inherit" });
runGh(uploadLatestArgs, { stdio: "inherit" });
const versionAssets = readReleaseAssets(versionTag);
const latestAssets = readReleaseAssets(latestTag);
if (!versionAssets.includes(dmgName)) {
throw new Error(
`Release ${versionTag} is missing versioned asset ${dmgName} after upload`,
);
}
if (!latestAssets.includes(latestAssetName)) {
throw new Error(
`Release ${latestTag} is missing latest alias asset ${latestAssetName} after upload`,
);
}
console.log("GitHub DMG assets verified.");
console.log(`Versioned URL: ${assetUrl(versionTag, dmgName)}`);
console.log(`Channel URL: ${assetUrl(latestTag, latestAssetName)}`);
} finally {
rmSync(stageDir, { recursive: true, force: true });
}
}
main();

View File

@@ -1,223 +0,0 @@
import { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
const repo = process.env.GITHUB_REPOSITORY ?? "block/sprout";
const version = requireVersionEnv();
const latestTag = "sprout-desktop-latest";
const tauriTarget = process.env.TAURI_TARGET ?? "aarch64-apple-darwin";
const updaterPlatform = process.env.UPDATER_PLATFORM ?? "darwin-aarch64";
const dryRun = process.env.DRY_RUN === "true" || process.env.DRY_RUN === "1";
const defaultBundleDirs = [
`src-tauri/target/${tauriTarget}/release/bundle/macos`,
"src-tauri/target/release/bundle/macos",
];
const bundleDir = resolve(
process.cwd(),
process.env.UPDATER_BUNDLE_DIR ??
defaultBundleDirs.find((dir) => existsSync(resolve(process.cwd(), dir))) ??
defaultBundleDirs[0],
);
const latestPath = join(bundleDir, "latest.json");
function requireVersionEnv() {
const v = process.env.VERSION;
if (!v || !v.trim()) {
throw new Error(
"VERSION env var is required. CI sets this from the git tag; for local use, run: VERSION=x.y.z pnpm run release:updater:publish",
);
}
return v.trim();
}
function requirePath(path) {
if (!existsSync(path)) {
throw new Error(`Missing required file: ${path}`);
}
}
function runGh(args, options = {}) {
return execFileSync("gh", args, options);
}
function releaseExists(tag) {
try {
runGh(["release", "view", tag, "--repo", repo], { stdio: "ignore" });
return true;
} catch {
return false;
}
}
function ensureRelease(tag, title) {
if (releaseExists(tag)) {
return;
}
const args = [
"release",
"create",
tag,
"--repo",
repo,
"--title",
title,
"--notes",
"Automated release placeholder.",
];
runGh(args, { stdio: "inherit" });
}
function readReleaseAssets(tag) {
const assetsRaw = runGh(
[
"release",
"view",
tag,
"--repo",
repo,
"--json",
"assets",
"--jq",
".assets[].name",
],
{ encoding: "utf-8" },
);
return assetsRaw
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function downloadUrl(name) {
return `https://github.com/${repo}/releases/download/${latestTag}/${encodeURIComponent(name)}`;
}
function resolveArchivePath() {
const canonicalArchiveName = "Sprout.app.tar.gz";
const canonicalPath = join(bundleDir, canonicalArchiveName);
if (existsSync(canonicalPath)) {
return canonicalPath;
}
const candidates = readdirSync(bundleDir).filter((entry) =>
entry.endsWith(".app.tar.gz"),
);
if (candidates.length === 1) {
return join(bundleDir, candidates[0]);
}
if (candidates.length === 0) {
throw new Error(
`Could not find updater archive in ${bundleDir}. Expected ${canonicalArchiveName}.`,
);
}
throw new Error(
`Found multiple updater archives in ${bundleDir}: ${candidates.join(", ")}. Cannot determine which to use.`,
);
}
function buildLatestJson(signaturePath) {
const signature = readFileSync(signaturePath, "utf-8").trim();
return {
version,
notes: `Release v${version}.`,
pub_date: new Date().toISOString(),
platforms: {
[updaterPlatform]: {
signature,
url: "",
},
},
};
}
function main() {
const archivePath = resolveArchivePath();
const signaturePath = `${archivePath}.sig`;
requirePath(archivePath);
requirePath(signaturePath);
const latest = existsSync(latestPath)
? JSON.parse(readFileSync(latestPath, "utf-8"))
: buildLatestJson(signaturePath);
latest.version = version;
latest.pub_date = new Date().toISOString();
const platformRecord = latest?.platforms?.[updaterPlatform];
if (!platformRecord) {
const available = Object.keys(latest?.platforms ?? {});
throw new Error(
`Platform "${updaterPlatform}" missing in latest.json. Available: ${available.join(", ") || "(none)"}`,
);
}
const archiveName = basename(archivePath);
const signatureName = basename(signaturePath);
platformRecord.signature = readFileSync(signaturePath, "utf-8").trim();
platformRecord.url = downloadUrl(archiveName);
const stageDir = mkdtempSync(join(tmpdir(), "sprout-github-updater-"));
const stagedLatestPath = join(stageDir, "latest.json");
try {
cpSync(archivePath, join(stageDir, archiveName));
cpSync(signaturePath, join(stageDir, signatureName));
writeFileSync(stagedLatestPath, `${JSON.stringify(latest, null, 2)}\n`);
const uploadArgs = [
"release",
"upload",
latestTag,
stagedLatestPath,
join(stageDir, archiveName),
join(stageDir, signatureName),
"--repo",
repo,
"--clobber",
];
console.log(`Preparing updater upload for ${repo}`);
console.log(`- latest tag: ${latestTag}`);
console.log(`- updater archive: ${archiveName}`);
console.log(`- updater endpoint: ${downloadUrl("latest.json")}`);
if (dryRun) {
console.log("DRY_RUN enabled. Skipping upload.");
console.log(
`gh release view ${latestTag} --repo ${repo} || gh release create ${latestTag} --repo ${repo} --title "Sprout Desktop Latest" --notes "Automated release placeholder."`,
);
console.log(`gh ${uploadArgs.join(" ")}`);
return;
}
ensureRelease(latestTag, "Sprout Desktop Latest");
runGh(uploadArgs, { stdio: "inherit" });
const latestAssets = readReleaseAssets(latestTag);
for (const expected of ["latest.json", archiveName, signatureName]) {
if (!latestAssets.includes(expected)) {
throw new Error(
`Release ${latestTag} is missing ${expected} after upload`,
);
}
}
console.log("GitHub updater assets verified.");
console.log(`Updater endpoint: ${downloadUrl("latest.json")}`);
} finally {
rmSync(stageDir, { recursive: true, force: true });
}
}
main();

View File

@@ -1,38 +0,0 @@
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}`);

View File

@@ -2872,12 +2872,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -3315,18 +3309,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -3482,20 +3464,6 @@ dependencies = [
"ureq 3.3.0",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -5233,7 +5201,6 @@ dependencies = [
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-updater",
"tauri-plugin-websocket",
"tauri-plugin-window-state",
"tempfile",
@@ -5244,7 +5211,7 @@ dependencies = [
"uuid",
"windows-sys 0.61.2",
"zeroize",
"zip 2.4.2",
"zip",
]
[[package]]
@@ -5887,39 +5854,6 @@ dependencies = [
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.4.2"
@@ -8039,18 +7973,6 @@ dependencies = [
"zstd",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.21"

View File

@@ -31,7 +31,6 @@ tauri-plugin-opener = "2"
tauri-plugin-window-state = "2"
tauri-plugin-websocket = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
infer = "0.19"
hex = "0.4"

View File

@@ -1,9 +1,6 @@
fn main() {
println!("cargo:rerun-if-env-changed=SPROUT_RELAY_URL");
println!("cargo:rerun-if-env-changed=SPROUT_RELAY_HTTP");
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_PUBLIC_KEY");
println!("cargo:rerun-if-env-changed=SPROUT_UPDATER_ENDPOINT");
println!("cargo:rustc-check-cfg=cfg(sprout_updater_enabled)");
if let Ok(relay_url) = std::env::var("SPROUT_RELAY_URL") {
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_URL={relay_url}");
@@ -13,18 +10,5 @@ fn main() {
println!("cargo:rustc-env=SPROUT_DESKTOP_BUILD_RELAY_HTTP={relay_http}");
}
let updater_public_key = std::env::var("SPROUT_UPDATER_PUBLIC_KEY")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let updater_endpoint = std::env::var("SPROUT_UPDATER_ENDPOINT")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if updater_public_key.is_some() && updater_endpoint.is_some() {
println!("cargo:rustc-cfg=sprout_updater_enabled");
}
tauri_build::build()
}

View File

@@ -18,9 +18,6 @@
"websocket:default",
"window-state:default",
"dialog:default",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",

View File

@@ -444,19 +444,6 @@ pub fn run() {
.build()
});
// Only register the updater in release builds that were compiled with a
// real updater configuration. Local unsigned builds omit that config and
// should still launch for debugging.
#[cfg(sprout_updater_enabled)]
let builder = if cfg!(debug_assertions) {
builder
} else {
builder.plugin(tauri_plugin_updater::Builder::new().build())
};
#[cfg(not(sprout_updater_enabled))]
let builder = builder;
let shutdown_started = Arc::new(AtomicBool::new(false));
let restore_shutdown_started = Arc::clone(&shutdown_started);
let app = builder

View File

@@ -1,163 +0,0 @@
import { useState } from "react";
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
type UpdateStatus =
| { state: "idle" }
| { state: "checking" }
| { state: "up-to-date" }
| { state: "available"; version: string }
| { state: "downloading"; progress?: number }
| { state: "installing" }
| { state: "ready" }
| { state: "error"; message: string };
export function UpdateChecker() {
const [status, setStatus] = useState<UpdateStatus>({ state: "idle" });
async function checkForUpdate() {
try {
setStatus({ state: "checking" });
const update = await check();
if (update) {
setStatus({ state: "available", version: update.version });
} else {
setStatus({ state: "up-to-date" });
}
} catch (err) {
setStatus({
state: "error",
message: err instanceof Error ? err.message : String(err),
});
}
}
async function downloadAndInstall() {
try {
setStatus({ state: "downloading" });
const update = await check();
if (!update) {
setStatus({ state: "up-to-date" });
return;
}
await update.downloadAndInstall((event) => {
if (event.event === "Started" && event.data.contentLength) {
setStatus({ state: "downloading", progress: 0 });
} else if (event.event === "Progress") {
// Could track progress here
} else if (event.event === "Finished") {
setStatus({ state: "installing" });
}
});
setStatus({ state: "ready" });
} catch (err) {
setStatus({
state: "error",
message: err instanceof Error ? err.message : String(err),
});
}
}
async function handleRelaunch() {
await relaunch();
}
return (
<div className="rounded-lg bg-zinc-900 p-4">
<h3 className="mb-3 text-sm font-medium text-zinc-200">
Software Updates
</h3>
{status.state === "idle" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-400">
Check if a new version is available.
</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Check for Updates
</button>
</div>
)}
{status.state === "checking" && (
<p className="text-sm text-zinc-400">Checking for updates…</p>
)}
{status.state === "up-to-date" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">
✓ You're on the latest version.
</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700"
>
Check Again
</button>
</div>
)}
{status.state === "available" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">
Version <span className="font-semibold">{status.version}</span> is
available.
</p>
<button
type="button"
onClick={downloadAndInstall}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Download &amp; Install
</button>
</div>
)}
{status.state === "downloading" && (
<p className="text-sm text-zinc-400">Downloading update…</p>
)}
{status.state === "installing" && (
<p className="text-sm text-zinc-400">Installing update…</p>
)}
{status.state === "ready" && (
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-300">
Update installed. Restart to apply.
</p>
<button
type="button"
onClick={handleRelaunch}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Restart Now
</button>
</div>
)}
{status.state === "error" && (
<div className="flex items-center justify-between">
<p className="text-sm text-red-400">
Update failed: {status.message}
</p>
<button
type="button"
onClick={checkForUpdate}
className="rounded-lg bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700"
>
Retry
</button>
</div>
)}
</div>
);
}

View File

@@ -177,32 +177,6 @@ desktop-dev:
desktop-app *ARGS:
just dev {{ARGS}}
# ─── Desktop Release ──────────────────────────────────────────────────────────
# Tag and push to trigger the desktop release workflow (CI handles version bumping)
desktop-release version:
#!/usr/bin/env bash
set -euo pipefail
git tag "desktop/v{{version}}"
git push origin "desktop/v{{version}}"
echo "Pushed tag desktop/v{{version}} — CI will set the version, build, and publish the release."
# Set the desktop app version (patches package.json, tauri.conf.json, Cargo.toml)
desktop-set-version version:
cd {{desktop_dir}} && node scripts/set-version-from-tag.mjs "{{version}}"
cd {{desktop_dir}}/src-tauri && cargo generate-lockfile
# Build a local desktop release (for testing). Optionally set a version first.
desktop-release-build version="" target="aarch64-apple-darwin" *args:
#!/usr/bin/env bash
set -euo pipefail
if [ -n "{{version}}" ]; then
just desktop-set-version "{{version}}"
fi
cd {{desktop_dir}} && pnpm exec tauri build --target {{target}} --config src-tauri/tauri.release.conf.json {{args}}
# ─── Mobile ──────────────────────────────────────────────────────────────────
mobile_dir := "mobile"