- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG - storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S, Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download; the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json - credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI made; precedence env > named profile > project file > default profile; config `account`, "account" in the project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use - app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop, apps folders | add | remove, ws init and ws without a workspace file - desktop: notice when a folder pins another storebound version; storebound-desktop --version - tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust), keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
322 lines
22 KiB
Markdown
322 lines
22 KiB
Markdown
# Releasing storebound desktop
|
|
|
|
How the desktop app (`apps/desktop`) is built, signed, checked and published. For what the app does, see
|
|
[desktop.md](desktop.md).
|
|
|
|
## Distribution
|
|
|
|
- **The desktop app is its own download.** A signed app from storebound.dev/desktop/:
|
|
- macOS: a DMG with a universal app (Apple silicon and Intel), macOS 11 or later;
|
|
- Windows: an NSIS installer that installs for the current user and brings the WebView2 bootstrapper;
|
|
- Linux: an AppImage and a .deb, built on Ubuntu 22.04.
|
|
- **It updates itself** with Tauri's updater: storebound.dev/desktop/latest.json, every bundle signed with the
|
|
updater key. The app checks the signature before it replaces anything and installs only after the owner clicks
|
|
Install in a native dialog (Settings > Updates).
|
|
- **It brings its own CLI.** The app bundles the storebound CLI as a sidecar, so it never depends on an npm install.
|
|
That CLI is the npm package of the same version.
|
|
- **The npm package stays the CLI, the MCP server and the skills.** It contains no Tauri binaries.
|
|
- **Bridge: `storebound desktop`** installs, updates and opens the app of the CLI's own version (docs/desktop.md,
|
|
"One product with the CLI"). It reads `storebound.dev/desktop/dl/<version>/latest.json` (written by the publish
|
|
script next to the version's files) and falls back to the channel's `latest.json`. `--download` only saves the
|
|
checked installer to ~/Downloads.
|
|
|
|
## One version, one release
|
|
|
|
The CLI and the desktop app are one product with one version number: `version` in the root `package.json`. 0.8.0 is
|
|
the first release of both under one number. (The desktop had its own `desktopVersion`, 0.1.0; its release candidate
|
|
in `~/Desktop/storebound-dist/desktop-0.1.0` was never published, so no installed app or link depends on it.)
|
|
|
|
- `bun tools/desktop-version.ts` checks the copies: the agent plugin manifests (`.claude-plugin/plugin.json`,
|
|
`marketplace.json`, `.codex-plugin/plugin.json`, `gemini-extension.json`), `apps/desktop/package.json`,
|
|
`tauri.conf.json`, `Cargo.toml` and the app's entry in `Cargo.lock`. CI runs this check; `src-tauri/tests/d5.rs`
|
|
checks the app's copies from the Rust side.
|
|
- `bun tools/desktop-version.ts --set 0.8.0` writes a new version into package.json and all of them.
|
|
- **Release order** (one commit, one version): build and check the desktop artifacts (`mac`, `add`), `publish` them
|
|
(it refuses a folder whose version is not package.json's), then `npm publish` of the same version (CONTRIBUTING.md).
|
|
The app goes first, so `npx storebound desktop` of the new CLI finds its own version.
|
|
|
|
## Keys and private files (outside every repository)
|
|
|
|
`~/.storebound-keys/` (mode 700) on the release Mac holds:
|
|
|
|
| file | what | made by |
|
|
|---|---|---|
|
|
| `updater.key` | minisign private key of the updater (no password; mode 600) | `bunx tauri signer generate -w ~/.storebound-keys/updater.key -p "" --ci` (done once, D5) |
|
|
| `updater.key.pub` | its public key; the same text is `plugins.updater.pubkey` in tauri.conf.json and `PUBLIC_KEY` in cli/lib/desktop-dist.ts | the same command |
|
|
| `forbidden-terms.txt` | names the privacy scan must never find: one per line (people, accounts, emails, machine names) | the owner |
|
|
| `notary.json` | optional: `{"keyId", "issuerId", "keyPath"}` of the App Store Connect API key for notarytool (or the `ASC_*` / `APPLE_API_*` variables) | the owner |
|
|
|
|
**Back up `updater.key`** (password manager or an offline copy). If it is lost, installed apps can never update
|
|
again: they only accept bundles signed with this key, so everyone would have to reinstall by hand. Never commit
|
|
it, never copy it to a runner. The Forgejo workflow builds unsigned installers, and the Mac signs them.
|
|
|
|
## Privacy gate
|
|
|
|
`bun tools/privacy-scan.ts <files or folders>` runs on every artifact before it can ship.
|
|
|
|
- **Terms it looks for:**
|
|
- the terms file;
|
|
- this machine's account name, home folder, full name, host and computer names, and git's global identity;
|
|
- the private-name rule of tools/scrub.ts;
|
|
- any `/Users/<name>`, `/home/<name>` or `C:\Users\<name>` that is not a generic account.
|
|
- **How it reads files:** as bytes, in UTF-8 and UTF-16LE.
|
|
- **Archives it opens:** DMG (mounted read-only, never shown in Finder), .tar.gz, .deb, .zip, AppImage (unsquashfs
|
|
or 7-Zip) and NSIS (7-Zip).
|
|
- **Debug files:** .dSYM, .pdb and source maps fail the scan.
|
|
- **Exit codes:** 1 when it finds something, 2 when it cannot open an archive. A term is never printed in full.
|
|
|
|
**Path-free builds.** `tools/desktop-release.ts` builds with `--remap-path-prefix` and `-ffile-prefix-map`, which
|
|
turn home into `~`, the checkout into `/src`, the cargo and rustup homes into `/cargo` and `/rustup`, and
|
|
`CARGO_TARGET_DIR` into `/target`. On Windows it also uses `/PDBALTPATH` and `/d1trimfile`. The target folder must
|
|
be outside the home folder. Release profile: `strip = true`, LTO, `panic = "abort"`.
|
|
|
|
## macOS (on the owner's Mac)
|
|
|
|
```sh
|
|
bun tools/desktop-release.ts mac --out ~/.storebound-releases/desktop/0.1.0 # from a clean commit
|
|
```
|
|
|
|
**Runs in the owner's logged-in macOS session.** It refuses SSH and background sessions: over SSH `codesign`
|
|
cannot use the login keychain (errSecInternalComponent).
|
|
|
|
What it does, in order:
|
|
|
|
1. Checks the session, the version, a clean checkout, the updater key and that the rustup targets are installed.
|
|
2. Picks the signing identity:
|
|
- **Developer ID Application found** (`security find-identity -v -p codesigning`): hardened runtime with
|
|
`src-tauri/entitlements.plist` (JIT for the Bun sidecar), notarization and stapling. Tauri notarizes the app,
|
|
the script notarizes the DMG.
|
|
- **None found**: an ad-hoc signed build whose files end in `-unsigned`.
|
|
3. Builds the sidecars for arm64 and x64, merges them into one universal file with `lipo`, then runs
|
|
`tauri build --target universal-apple-darwin --bundles app --no-sign` with the path-free environment. Then the
|
|
**architecture guard** on that unsigned app: every Mach-O file in the bundle (Contents/MacOS, Frameworks,
|
|
Resources: main binary, sidecar, helpers, dylibs) must contain arm64 (`lipo -archs`), nothing may be Intel-only,
|
|
and Info.plist must not set `LSRequiresNativeExecution` false or put x86_64 first in `LSArchitecturePriority`.
|
|
Users must never see macOS's "App Update Required ... will not open in macOS 28" (Rosetta) notice. Only then does
|
|
`tauri bundle` sign, notarize and staple the same binaries. The guard runs again on the signed app, the app in the
|
|
DMG and the one in the updater bundle, and its lines go into evidence.json.
|
|
4. Makes the updater bundle (`.app.tar.gz`, root-owned entries, no macOS metadata) and the DMG. The DMG is made with
|
|
`hdiutil` only (no Finder scripting, nothing comes to the front).
|
|
5. **Checks the exact artifacts:**
|
|
- `codesign --verify --deep --strict`;
|
|
- the Gatekeeper expectation (signed: `spctl` accepts it as Notarized Developer ID and `stapler validate` passes;
|
|
unsigned: ad-hoc, and `spctl` rejects it);
|
|
- `lipo -archs` on every executable in the bundle (each must carry arm64 and x86_64). The x86_64 slice is never
|
|
run on the build Mac: that starts Rosetta, and macOS then warns the owner that the app "includes a component that
|
|
will not open" in the next macOS;
|
|
- the self-test (arm64, native), run from the mounted DMG and from the unpacked updater bundle.
|
|
`storebound-desktop --self-test` starts the bundled sidecar, sends `session.hello`, prints one JSON
|
|
line and exits, before Tauri starts, so no window opens;
|
|
- the privacy scan of the app, the DMG and the updater bundle.
|
|
6. Writes minisign `.sig` files with the updater key and checks them right away against tauri.conf.json's key.
|
|
7. Writes `latest.json`, `SHA256SUMS` and `evidence-macos-<version>.json`. The evidence file (tool versions, checks,
|
|
hashes) stays local.
|
|
|
|
### Signing identity and notarization key
|
|
|
|
1. **Developer ID certificate.** A **Developer ID Application** certificate of your Apple developer team in the login
|
|
keychain (the Account Holder creates it at developer.apple.com > Certificates > +, with a CSR from Keychain
|
|
Access). It is installed on the release Mac since October 2026. A machine without it still builds, but only the
|
|
ad-hoc `-unsigned` preview.
|
|
2. **Notarization key.** An App Store Connect API key, found in this order:
|
|
- `APPLE_API_KEY`, `APPLE_API_ISSUER` and `APPLE_API_KEY_PATH`;
|
|
- storebound's own `ASC_KEY_ID`, `ASC_ISSUER_ID` and `ASC_P8` (a path or the PEM; by default
|
|
`~/.appstoreconnect/private_keys/AuthKey_<ASC_KEY_ID>.p8`, where Xcode keeps it);
|
|
- `~/.storebound-keys/notary.json`.
|
|
|
|
The key ids are never written into the repository.
|
|
3. **Build.** Run the command above in Terminal on the Mac.
|
|
|
|
**Gates on the exact artifacts.** A signed release stops unless all of these pass:
|
|
|
|
- every executable in `Contents/MacOS` (the app and the CLI sidecar, signed inside out by Tauri), and the bundle, is
|
|
signed with Developer ID, with the hardened runtime and a secure timestamp;
|
|
- no executable carries the App Sandbox entitlement (Mac App Store) or `get-task-allow`;
|
|
- `spctl -a -vvv -t exec <app>` says Notarized Developer ID, and `xcrun stapler validate <app>` passes for the
|
|
built app, the app inside the mounted DMG and the app inside the updater bundle;
|
|
- `spctl -a -vvv -t open --context context:primary-signature <dmg>` accepts the DMG, and
|
|
`xcrun stapler validate <dmg>` passes.
|
|
|
|
### Opening an unsigned preview (what the downloads page says)
|
|
|
|
macOS: copy storebound to Applications and open it. When macOS refuses, open System Settings > Privacy & Security
|
|
and choose Open Anyway (once).
|
|
|
|
## Windows and Linux (Forgejo Actions)
|
|
|
|
`.forgejo/workflows/desktop.yml` runs on a `desktop-v*` tag or by hand (workflow_dispatch; tick "windows" for the
|
|
Windows installer too: a tag push builds Linux only).
|
|
|
|
- **Linux** (`runs-on: ubuntu-22.04`, a container): AppImage and .deb built with the path-free environment, then:
|
|
- self-tests of the extracted AppImage and the unpacked .deb;
|
|
- both started for real under Xvfb (no xdg-utils or desktop-file-utils in the container, like a minimal system):
|
|
the job fails unless host.log says `sidecar started` (the window, WebKitGTK and the UI ran) with no error line in
|
|
host.log or webview.log;
|
|
- the privacy scan and SHA256SUMS;
|
|
- an artifact `storebound-desktop-linux`.
|
|
|
|
`WEBKIT_DISABLE_DMABUF_RENDERER=1`: the app sets it itself at start on Linux unless the user set it (some GPU / VM
|
|
setups leave the window blank with DMABUF), and the workflow sets it too.
|
|
- **Windows** (`runs-on: windows`): an NSIS installer (`installMode: currentUser`, WebView2 `embedBootstrapper`,
|
|
unsigned), then:
|
|
- a silent per-user install (`/S`), the self-test of the installed exe, a silent uninstall;
|
|
- the privacy scan;
|
|
- an artifact `storebound-desktop-windows`.
|
|
|
|
Then on the Mac:
|
|
|
|
```sh
|
|
bun tools/desktop-release.ts add --out ~/.storebound-releases/desktop/0.1.0 \
|
|
--commit <the run's full commit hash> --run <the run's URL> \
|
|
storebound_0.1.0_amd64.AppImage storebound_0.1.0_amd64.deb storebound_0.1.0_x64-setup.exe
|
|
```
|
|
|
|
It scans and signs each file, renames the unsigned Windows installer to `-unsigned`, and updates latest.json
|
|
(`windows-x86_64`, `linux-x86_64`) and SHA256SUMS. It writes `evidence-linux-<version>.json` and
|
|
`evidence-windows-<version>.json` (run, commit, sizes, hashes, the scan; they stay local like the macOS one) and
|
|
refuses a `--commit` other than the one in the folder's macOS evidence: every artifact of a version comes from one
|
|
commit. Before `add`, check the downloads against the run: the Linux artifact carries SHA256SUMS; for the Windows
|
|
installer compare with `Get-FileHash` of `C:\storebound-target\release\bundle\nsis\` on the runner. For the scan, install `squashfs` and `sevenzip` from Homebrew.
|
|
|
|
### Runner setup
|
|
|
|
Forgejo runs in Docker on the VDS. `.forgejo/workflows/ci.yml` (`bun run ci`, Rust tests and clippy included) runs on
|
|
every push and pull request to main; `desktop.yml` as above. Neither publishes anything (Jenkins still publishes
|
|
site/). Workflow artifacts of this public repository can be downloaded by anyone for 14 days.
|
|
|
|
1. **Actions in Forgejo (done).** `FORGEJO__actions__ENABLED=true` and
|
|
`FORGEJO__actions__DEFAULT_ACTIONS_URL=https://data.forgejo.org` in the compose file; the Actions unit is on for
|
|
CRT/storebound only.
|
|
2. **Linux runner on the VDS (done).** `/srv/forgejo-runner/`: Forgejo runner 13 (`vds-linux`, registered to
|
|
CRT/storebound only, label `ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04`, capacity 2, job timeout
|
|
2 h) next to its own Docker daemon (`docker:29-dind`, TLS only, 16 CPUs / 24 GB). Jobs run in throwaway containers
|
|
of that daemon: no Docker socket, no host volumes, nothing of Forgejo's data. The runner never sees a signing key.
|
|
`actions/cache` uses the runner's cache server. `prune.sh` (cron, daily) removes stopped containers, dangling
|
|
images and cache entries unused for 14 days, and caps images at 40 GB and the cache at 20 GB.
|
|
- Re-register (a new token from the repository's Actions runner settings):
|
|
`forgejo-runner register --no-interactive --instance https://git.crt.fyi --token <token> --name vds-linux`
|
|
in the runner image with `/srv/forgejo-runner/data` as `/data`, then `docker compose up -d`.
|
|
- Linux quirks the workflow handles: `NO_STRIP=1` (strip removes the bun sidecar's payload) and the sidecar's rpath
|
|
set before bundling (linuxdeploy's own patchelf corrupts the bun executable; with the rpath already set it
|
|
leaves the file alone). The self-tests start the sidecar from the AppImage and the .deb, so a broken sidecar
|
|
fails the job.
|
|
3. **Windows runner on the VDS (done).** Windows Server 2022 Standard (evaluation) in a KVM virtual machine, in
|
|
Docker (`dockurr/windows:6.05`), `/srv/windows-runner/`. Forgejo runner 13.2.0 (`vds-windows`, label
|
|
`windows:host`, capacity 1, job timeout 3 h), registered at the **CRT user level**: every CRT repository can use
|
|
it (storebound, Demo App, ...).
|
|
- **Resources:** 8 vCPUs, 12 GB RAM, a 150 GB disk (`storage/data.img`, sparse). `restart: unless-stopped`.
|
|
- **Network:** nothing public. The web viewer (8006), RDP (3389) and the VM's OpenSSH (22, as host port 2223) are
|
|
bound to 127.0.0.1 on the VDS; use an SSH tunnel (`ssh -L 8006:127.0.0.1:8006 root@<vds>`, then
|
|
http://127.0.0.1:8006). The VM reaches the internet and git.crt.fyi through the container's NAT. No host folders
|
|
but its own storage and `oem/` (copied into the VM once, at install).
|
|
- **Accounts:** `builder` (local admin, auto sign-in; password in `/srv/windows-runner/.env`, mode 600) and
|
|
`runner` (no admin rights, random password known only to Windows) for the service. OpenSSH takes only the key
|
|
in `/srv/windows-runner/ssh/` (`./vmssh`, `./vmscp` on the VDS).
|
|
- **Files:** `compose.yml`; `oem/install.bat` + `oem/setup.ps1` (first boot: OpenSSH, Windows Update policy, time
|
|
zone UTC+8); `provision.ps1` (build tools, idempotent, pinned downloads; log in `provision.log`);
|
|
`runner-setup.ps1` (account, `config.yml`, registration with a token on stdin, WinSW service);
|
|
`build/runner/` (forgejo-runner built from the v13.2.0 tag with `GOOS=windows`: Forgejo ships no Windows
|
|
binary).
|
|
- **Toolchain** (machine-wide, on the PATH the service sees): VS 2022 Build Tools 17.14 (MSVC x64/x86 and ARM64,
|
|
Windows 11 SDK 10.0.26100), rustup in `C:\rust` (stable-x86_64-pc-windows-msvc, writable by `runner`, so jobs
|
|
can `rustup target add`), Bun 1.4.0 (`bun`, `bunx`) in `C:\bun`, Git for Windows 2.56 (Git Bash's `bash` on the
|
|
PATH for `shell: bash`; `core.autocrlf false`, `core.longpaths true`), PowerShell 7.6 (`shell: pwsh`), Node 24
|
|
(JavaScript actions), 7-Zip 26.04, WebView2 runtime. Tauri fetches NSIS itself.
|
|
- **Service:** `forgejo-runner` (WinSW 2.12, delayed automatic start, restarts on failure) runs
|
|
`C:\forgejo-runner\forgejo-runner.exe daemon` as `runner`; jobs work in `C:\actions` (a daily task removes job
|
|
folders older than a day). Defender stays on, with exclusions for `C:\actions`, `C:\storebound-target`,
|
|
`C:\rust` and `C:\bun`.
|
|
- **Windows Update:** installs daily at 03:00 (UTC+8) and never restarts on its own while `builder` is signed
|
|
in (it always is); active hours 07:00 to 23:00. A restart for updates is manual:
|
|
`./vmssh "Restart-Computer -Force"` when no job runs.
|
|
- **Licensing:** the evaluation edition runs 180 days, then shuts down every hour. Extend it with
|
|
`slmgr /rearm` (6 rearms left at install, 180 days each; the VM was installed in October 2026) or rebuild the VM (delete `storage/`, `docker compose up -d`, run `provision.ps1`
|
|
and `runner-setup.ps1` again). The evaluation license is for evaluation and testing; for a permanent build
|
|
machine, buy a Windows Server (or Windows 11 Pro) license and enter the key.
|
|
- **Re-register** (a new token from CRT's user settings > Actions > Runners, or
|
|
`GET /api/v1/user/actions/runners/registration-token`): on the VDS, delete `C:\forgejo-runner\.runner` in the
|
|
VM and pipe the token into `./vmssh "powershell -File C:\provision\runner-setup.ps1"`.
|
|
- **Shared runner:** any workflow of any CRT repository with `runs-on: windows` runs here, as `runner`, with
|
|
internet access. Keep workflows of fork pull requests off it (as ci.yml does for Linux).
|
|
|
|
The workflow's Windows job runs only when "windows" is ticked (from the API, `"inputs": {"windows": "on"}`).
|
|
4. **Windows code signing (later).** Buy a code-signing certificate, then sign the installer before `add` and pass
|
|
`--code-signed`.
|
|
|
|
## Hosting on storebound.dev
|
|
|
|
| URL | served from | how it gets there |
|
|
|---|---|---|
|
|
| /#download (the downloads, a section of the home page; /desktop/ redirects there) | /var/www/storebound.dev/index.html, /var/www/storebound.dev/desktop/index.html | `site/index.html`, `site/desktop/index.html`, with the site (Jenkins + /usr/local/sbin/storebound-publish) |
|
|
| /desktop/latest.json, /desktop/beta/latest.json | /var/www/storebound-desktop/ | storebound-desktop-publish |
|
|
| /desktop/dl/<version>/<files> | /var/www/storebound-desktop/dl/ | storebound-desktop-publish |
|
|
|
|
**Why the binaries live outside the site's document root.** The site's publish script rsyncs `site/` with
|
|
`--delete-after`, so anything not in git would vanish on the next site deploy. Binaries are never committed.
|
|
Apache maps the three paths with `Include /etc/apache2/storebound-desktop.conf` in the :443 vhost
|
|
(`tools/vds/apache-storebound-desktop.conf`):
|
|
|
|
- latest.json is sent `no-cache`;
|
|
- version folders are sent `immutable`;
|
|
- no directory listings.
|
|
|
|
**`/usr/local/sbin/storebound-desktop-publish <version> [stable|beta]`** (`tools/vds/storebound-desktop-publish`,
|
|
root:root 755) takes `/var/lib/storebound-desktop/incoming/<version>/` (root only, mode 700) and checks it:
|
|
|
|
- only plain files with plain names;
|
|
- SHA256SUMS matches;
|
|
- latest.json is that version, and every URL in it names an uploaded file under /desktop/dl/<version>/.
|
|
|
|
Then it publishes:
|
|
|
|
- a new version folder appears at once (temp folder, then rename);
|
|
- a later `add` to the same version only adds files, and a published file is never replaced by other bytes;
|
|
- latest.json goes in last (temp file, then rename), so apps see a version only when all its files are there.
|
|
|
|
It runs nothing from the upload.
|
|
|
|
**No sudoers entry is needed today.** The owner's Mac uploads and runs the script as root over SSH. If a CI job
|
|
should publish one day, give its user exactly this script, as the existing Jenkins entries do:
|
|
`<user> ALL=(root) NOPASSWD: /usr/local/sbin/storebound-desktop-publish`.
|
|
|
|
Both files are installed on the VDS (D5). The Apache vhost was backed up to `/root/storebound.dev.conf.bak-*` first.
|
|
To reinstall after a change:
|
|
|
|
```sh
|
|
scp tools/vds/storebound-desktop-publish root@<server>:/usr/local/sbin/storebound-desktop-publish
|
|
scp tools/vds/apache-storebound-desktop.conf root@<server>:/etc/apache2/storebound-desktop.conf
|
|
ssh root@<server> 'chmod 755 /usr/local/sbin/storebound-desktop-publish && apache2ctl configtest && systemctl reload apache2'
|
|
```
|
|
|
|
## Publish
|
|
|
|
Publishing is the owner's call. Every installed app on the channel offers the update as soon as latest.json is
|
|
live.
|
|
|
|
```sh
|
|
export STOREBOUND_PUBLISH_HOST=root@<server> # the VDS (not the Cloudflare name)
|
|
bun tools/desktop-release.ts verify --out ~/.storebound-releases/desktop/0.1.0
|
|
bun tools/desktop-release.ts publish --out ~/.storebound-releases/desktop/0.1.0 # dry run: the plan
|
|
bun tools/desktop-release.ts publish --out ~/.storebound-releases/desktop/0.1.0 --yes # upload + publish
|
|
```
|
|
|
|
1. **Checks again:** sums, signatures against tauri.conf.json's key, latest.json and the privacy scan.
|
|
2. **Uploads:** rsync into the incoming folder (evidence stays local).
|
|
3. **Publishes:** runs the publish script.
|
|
4. **Confirms:** reads the live latest.json back.
|
|
|
|
`--channel beta` publishes to /desktop/beta/latest.json, which apps on the Beta channel read.
|
|
|
|
**Rollback.** Tauri updates only to a higher version, so a bad release is replaced by a fixed higher version. Its
|
|
files stay where they are, because links and checksums must not change.
|
|
|
|
## Going public: checklist
|
|
|
|
1. A signed, notarized macOS build from the release commit (the Developer ID certificate is on the Mac). The gates
|
|
above must pass.
|
|
2. Forgejo Actions enabled, the Linux runner registered, a `desktop-v0.1.0` tag built, `add` of the AppImage and
|
|
deb.
|
|
3. Windows: a runner, or ship macOS + Linux first. The page and latest.json list only what exists.
|
|
4. `publish --yes` to stable. Then open storebound.dev/#download on each OS and run `storebound desktop --download`.
|
|
5. An updater round trip: publish 0.1.1 to beta, switch a 0.1.0 install to Beta, Check for updates, Install.
|
|
6. Update CHANGELOG (the home page's download section and README already point at storebound.dev/#download).
|