mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Remove release pipeline from public repo (#360)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
79
.github/workflows/bundle-desktop-macos.yml
vendored
79
.github/workflows/bundle-desktop-macos.yml
vendored
@@ -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
|
||||
154
.github/workflows/release.yml
vendored
154
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
164
.github/workflows/sprout-desktop-release.yml
vendored
164
.github/workflows/sprout-desktop-release.yml
vendored
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
46
desktop/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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}`);
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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}`);
|
||||
80
desktop/src-tauri/Cargo.lock
generated
80
desktop/src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
26
justfile
26
justfile
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user