368 lines
26 KiB
Markdown
368 lines
26 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): see "The release on git.crt.fyi" below. The app goes first
|
|
(`publish` refuses a folder whose version is not package.json's), then `npm publish` of the same version
|
|
(CONTRIBUTING.md), so `npx storebound desktop` of the new CLI finds its own version.
|
|
|
|
## The release on git.crt.fyi
|
|
|
|
One version tag, one Forgejo release: git.crt.fyi/CRT/storebound/releases keeps only the newest entry (older entries
|
|
are deleted after a successful release; their git tags stay). Notes are short: the CHANGELOG section's summary
|
|
paragraph (at most 4 lines), the install line and links, never the whole section. So the section's first paragraph
|
|
is the release summary: write it for that.
|
|
|
|
1. **Version and changelog** on main: `bun tools/desktop-version.ts --set 0.8.0`, a `## 0.8.0 · <date>` section
|
|
that starts with a summary paragraph, `bun run ci`, push.
|
|
2. **Tag push:** `git tag v0.8.0 && git push origin v0.8.0`. `.forgejo/workflows/release.yml` then:
|
|
- checks that the tag is `v` + package.json's version, every version copy matches (tools/desktop-version.ts) and
|
|
the CHANGELOG has the section;
|
|
- runs the full CI (`bun run ci`, the same steps as ci.yml: `.forgejo/actions/linux-setup` and `ci-run`);
|
|
- builds the AppImage + deb (Linux runner) and the NSIS installer (Windows runner), with the checks described
|
|
under "Windows and Linux" below;
|
|
- scans every artifact with the private terms (secret `FORBIDDEN_TERMS`), writes SHA256SUMS;
|
|
- creates or updates the release "storebound 0.8.0" (`tools/forgejo-release.ts upsert`: found by tag, assets of
|
|
the same name replaced, SHA256SUMS merged, read back), then deletes the older release entries
|
|
(`forgejo-release.ts prune --keep v0.8.0`).
|
|
3. **macOS on the Mac** (the DMG cannot be built or notarized on these runners): `bun tools/desktop-release.ts mac`,
|
|
then `add` of the runners' Linux / Windows files (signs them for the updater, see below), then
|
|
`bun tools/desktop-release.ts upload-forgejo --out ~/.storebound-releases/desktop/0.8.0 --yes`: the signed DMG and
|
|
the updater bundle with their `.sig` join the same release, SHA256SUMS is merged. It refuses an unsigned DMG and
|
|
a release the workflow has not made yet. Token: `FORGEJO_TOKEN`, else the git credential helper of git.crt.fyi.
|
|
4. **storebound.dev:** `bun tools/desktop-release.ts publish --out ... --yes` (below).
|
|
5. **npm:** `npm publish` of the same version: the owner's step (CONTRIBUTING.md).
|
|
Option, not set up: an npm automation token as the Actions secret `NPM_TOKEN` and a manual `npm` job in
|
|
release.yml (`needs: release`, workflow_dispatch only, `npm publish` after the Mac's upload) would let the owner
|
|
publish with one click in Forgejo instead of a terminal. The token can publish any version of the package, so it
|
|
stays off until the owner wants it.
|
|
|
|
**Dry run** (no version, nothing public): Actions > release > Run workflow with `dry` ticked (the default), or
|
|
`POST /api/v1/repos/CRT/storebound/actions/workflows/release.yml/dispatches` with
|
|
`{"ref":"main","inputs":{"dry":"on","windows":"on"}}` (over the API a boolean input is "on"; "true" counts as off).
|
|
The same jobs run on that commit; the release job makes a DRAFT `test-<sha>` with the real files, reads it back,
|
|
prints what `prune` would delete, and deletes the draft at the end (also when a step failed). `dry` off needs a `tag`
|
|
input and remakes that tag's release.
|
|
|
|
**Secrets** (repo Settings > Actions > Secrets): `RELEASE_TOKEN`, a token of CRT with scope `write:repository`
|
|
(name `storebound-release-workflow`; releases, assets, the dry run's tag) and `FORBIDDEN_TERMS`, the contents of
|
|
`forbidden-terms.txt` (never printed: hits show `x***`), and `FORGEJO_ORIGIN`, the server's IP: git.crt.fyi is
|
|
behind Cloudflare, which refuses uploads over 100 MB (the AppImage, the DMG) with a 502, so `forgejo-release.ts`
|
|
uploads assets to the origin directly (`curl --resolve`, TLS still checked for git.crt.fyi). On the Mac the same IP
|
|
is in `~/.storebound-keys/forgejo-origin`. Rotate the token: create a new one under CRT's
|
|
Settings > Applications with that scope, put it into the secret, delete the old one.
|
|
|
|
## 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)
|
|
|
|
The `linux` and `windows` jobs of `.forgejo/workflows/release.yml` (a `v*` tag push, or a dry run by hand; untick
|
|
"windows" in a dry run to build 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; `release.yml` as above makes the Forgejo release (Jenkins still publishes
|
|
site/, and `publish` storebound.dev/desktop). 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 and Windows runners registered, a dry run of release.yml green, then the
|
|
`v` tag, `add` of the AppImage, deb and setup.exe from the release.
|
|
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).
|