add builder and updater

This commit is contained in:
Joey Kimsey
2026-09-12 21:21:28 -04:00
parent 6e42b1012d
commit 1ee2774241
24 changed files with 4115 additions and 10 deletions

78
docs/releases.md Normal file
View File

@ -0,0 +1,78 @@
# Packaging and releases
Capsule uses electron-builder 26.15.3 and electron-updater 6.8.9. The first distribution target is Windows x64 with a per-user NSIS installer. Build on Windows with Node 22.12+ and `npm ci`; the lockfile fixes the resolved dependency versions. No PHP server is needed to build the client.
## 1. Package the application
Run `npm run dist:win`. The installer and blockmap are written to `dist/local/`; `npm run pack:win` produces only `dist/local/win-unpacked/`.
Local builds are unsigned, named **Capsule Local**, and have application ID `com.thetempusproject.capsule.local`. They store sessions in `%APPDATA%/capsule-local`. Updates are deliberately disabled, and these installers must not be distributed as the public product. Windows may display an unknown-publisher warning.
Public builds use the permanent ID `com.thetempusproject.capsule`, executable `Capsule.exe`, and `%APPDATA%/capsule/session.json`, preserving the original app's session location. Both installers run per user and preserve app data on uninstall. Do not change the public ID, package name, install scope, or data path after shipping without an explicit migration. The current installer uses Electron's default icon; add an approved `build/icon.ico` before public branding is finalized.
## 2. Configure the release feed
Set `CAPSULE_UPDATE_URL` in the build environment to a permanent public HTTPS directory ending in `/`. No feed is hardcoded or configured yet. The URL must not contain login credentials, a query, or a fragment. The generated `app-update.yml` is baked into the signed application; site selection, login, and IPC cannot change it.
The host serves these files at that URL:
- `Capsule-<version>-x64-Setup.exe`
- `Capsule-<version>-x64-Setup.exe.blockmap`
- `latest.yml`, generated by electron-builder
Use immutable versioned filenames. Retain old release artifacts so downloads already in progress keep working. Serve the installer and blockmap as binary files, allow range requests for differential downloads, and avoid authentication/HTML challenge pages or response transformations. Metadata should revalidate (`Cache-Control: no-cache`); versioned artifacts can have long immutable cache lifetimes. Configure these response headers on the host; an upload request's headers alone do not configure every server.
Publishing `latest.yml` announces a release. Upload and verify both artifacts first. A running Capsule checks 15 seconds after launch and four hours after each scheduled check finishes. A manual check uses the same updater. No push service or background Windows service is installed. Immediate remote notifications can later request a check, but must never supply executable URLs or commands.
## 3. Update behavior
`src/main/updateController.mjs` owns the state machine. `updates.js` wires it into Electron, and `updateIpc.mjs` permits only the known top-level app window. The preload bridge exposes status, check, install, and a status subscription. The renderer receives no signing credentials, release tokens, site tokens, or updater configuration.
The updater automatically downloads a newer stable version. It validates checksums and the configured Windows publisher signature before reporting readiness. The user chooses **Restart to update** after saving work. Normal close, Windows shutdown, and logoff do not intentionally launch an update installer (`autoInstallOnAppQuit = false`). Reopening checks again and can reuse a valid cached download. No downgrade or prerelease is accepted. If a release is faulty, publish a higher version containing the reverted code; do not overwrite an existing installer or assume automatic rollback.
Network, metadata, signature, and download failures show a retryable error while leaving the installed app usable. The update panel is available on the login screen, so a broken site connection does not prevent a client update. Multiple app instances are prevented to avoid competing installs. Local and development builds never contact an update feed.
## 4. Sign a public release
Set these variables in a secure release environment:
- `CAPSULE_UPDATE_URL`: the public read-only feed directory.
- `CAPSULE_PUBLISHER_NAME`: the exact common name on the signing certificate.
- `CSC_LINK`: a supported certificate location or base64 PFX, with `CSC_KEY_PASSWORD` when required; **or** `CSC_NAME`: the subject name of a signing identity already in the Windows certificate store, including a configured hardware-backed identity.
The certificate provider determines how its private key is accessed; not all certificates can be exported to a PFX. Store credentials in the CI secret store or machine certificate provider. Do not commit keys, `.env` files, or release credentials. This project does not load `.env` automatically.
Run `npm run release:win`. This invalidates any prior ready marker, runs the tests, compiles the client, requires code signing, and builds to `dist/release/`. Missing URL, missing publisher, missing signing identity, or a signing failure prevents a publishable build. Signature verification stays enabled. Signing identity configuration must be consistent across releases; plan certificate/publisher changes before the old certificate expires.
The output includes `release-ready.json`, a local record of the exact artifact hashes and feed. It is not served to clients and is not a substitute for Authenticode. Only files referenced by `latest.yml` plus the matching blockmap are publishable. Check both the installed executable and installer using `Get-AuthenticodeSignature` before the first public release.
## 5. Automate build and publication
`scripts/release.ps1` is the CI-independent Windows entry point. It runs `npm ci`, tests, the signed build, and release verification. Without `-Publish` it performs no upload. Keep build jobs serialized for each feed; concurrent publishers can otherwise move `latest.yml` backward. No CI service is configured because this checkout has no remote and no runner was selected.
`npm run publish:release` verifies the build and prints its upload order without uploading. The included upload adapter supports an authenticated HTTPS PUT/WebDAV endpoint. It needs:
- `CAPSULE_UPLOAD_URL`: HTTPS upload directory, ending in `/`, mapped by the host to the public feed.
- `CAPSULE_UPLOAD_TOKEN`: a bearer token with upload access, available only on the release machine.
The host must support authenticated PUT, conditional creation with `If-None-Match: *`, and atomic replacement of `latest.yml`. This adapter is **not** an S3 signing client, SFTP client, or TTP API route. If hosting uses one of those transports, add the corresponding publisher adapter before enabling publication; client update code does not need to change.
Run `npm run publish:release -- --upload`, or `powershell -File scripts/release.ps1 -Publish` for the complete pipeline. It verifies local checksums, uploads the installer and blockmap, verifies their publicly served bytes without credentials, and only then publishes `latest.yml`. Redirects are refused. Existing versioned objects are reused only if their public content matches. A failure stops the pipeline. If verification fails after `latest.yml` was uploaded, inspect and restore the feed pointer before retrying; the script does not claim a rollback.
Never publish `dist/local/`, `win-unpacked/`, build debug files, certificate files, or `release-ready.json`. Increment `package.json` and the lockfile version before each release (`npm version patch --no-git-tag-version` is one option). Release metadata accepts stable `major.minor.patch` versions only. A separate beta feed/channel can be added later.
## Acceptance checks before public distribution
The automated tests exercise updater state transitions, duplicate checks, retryable failures, IPC boundaries, signing configuration, and artifact integrity. After `npm run build`, `npm run smoke` opens the compiled app hidden with an isolated `dist/smoke-profile` data directory, verifies the real preload/status bridge and a synthetic ready prompt, and captures `dist/local/smoke.png` and `smoke-ready.png`. It does not sign in or install anything. These checks do not substitute for an installed upgrade.
On a disposable Windows account or VM with the real signing identity and a staging HTTPS feed:
1. Build and install signed `0.1.0`; confirm the correct publisher, per-user installation, and launch. Save a site/session and preferences.
2. Build signed `0.1.1` against the same staging feed and publish it. Open `0.1.0`, confirm the download and restart prompt, then restart and verify the running version and preserved session/preferences.
3. Close without accepting the update: it must not install at quit. Reopen and check that the cached update can become ready again.
4. Test offline checks and an interrupted download, restore connectivity, and retry. Try an installer signed by a different publisher and a tampered file on the isolated feed; neither may become installable.
5. Test a second launch, a normal uninstall (data retained), and reinstall. Confirm no unexpected elevation prompt. Verify the local build remains separate from the public product.
These installed/signed checks remain pending until a certificate and staging feed are provided. No live host, signing account, or public release is provisioned by the local build scripts.
References: [electron-builder auto updates](https://www.electron.build/docs/features/auto-update/), [Windows configuration](https://www.electron.build/docs/win/). Consult the installed package APIs when newer online documentation describes features from a later major version.