Maintainers
Release Guide
This guide covers release packaging for Windows, macOS, and Linux, plus how GitHub Actions publishes artifacts.
Pre-Flight Checks
Run these before creating a beta or release tag.
Quick check (fast, native-runtime sanity)
npm run preflight:release:quick
Runs everything in the full pre-flight below except the dist build and packaged-artifact checks
(--skip-dist): native-module loading (sharp, better-sqlite3, @napi-rs/canvas, lz4-napi,
node-llama-cpp), the critical-path baseline comparison and commercial compliance. It also needs a
clean git worktree. Use it to catch native-binding breakage and regressions early.
Full pre-flight (recommended before every tag)
npm run preflight:release
It requires a clean git worktree before it begins. On the current platform this runs:
- Native module verification (
verify:native-modules) - Critical-path baseline comparison (
test:critical-path:twice, unless--skip-critical) - Commercial compliance verification (
compliance:commercial:verify, unless--skip-commercial) - Clean
release/build - Platform dist build (e.g.
dist:winon Windows) - Packaged artifact verification (
verify:packaged-artifacts)
What test:critical-path:twice actually does
scripts/stability/compare-baseline.js checks out two temporary git worktrees under
.tmp/stability-worktrees/ — the previous release tag (the baseline) and HEAD (the target) — and:
- Runs the fixed smoke suite (
scripts/stability/run-smoke-suite.js) on the baseline - Runs the same smoke suite on the target
- Runs the threshold check (
scripts/stability/check-release-thresholds.js), catching:- Required-journey failures
- Baseline-pass to target-fail regressions
- ≥50% latency regressions from baseline to target
If system-metrics exports are provided to the threshold script, it also enforces fallback-rate guardrails for search and Uncategorized routing.
Wizard regression checks
npx jest --config test/jest.config.js test/components/WelcomePhase.test.js test/components/ModelSetupWizard.test.js
npx jest --config test/jest.config.js test/components/PhaseRenderer.test.js test/phaseTransitions.test.js
Pass criteria — do not tag until all are true
- Pre-flight command exits with code
0 - Critical-path smoke suite passes on the candidate SHA with no regressions against the baseline tag
- Wizard/phase regression suites pass
- Dist artifacts are generated under
release/build - No unexpected warnings/errors in command output
- Fallback telemetry is reviewed when available (search fallback rate, Uncategorized routing rate)
CI workflows that enforce these checks
.github/workflows/ci.yml(full CI gate).github/workflows/release.yml(Windows packaging/publish).github/workflows/mac-release.yml(macOS + Linux packaging/publish).github/workflows/manual-dist.yml(manual packaging builds)
Release Checklist
- Update release metadata
- Bump
versioninpackage.json - Move release notes from
CHANGELOG.md[Unreleased] to the new version section
- Bump
- Run quality gates
npm run ci
- Smoke-test installers
- Windows: run
npm run dist:win, installStratoSortStack-Setup-*.exe - macOS: on a macOS host, run
npm run dist:mac:arm64, openStratoSortStack-*.dmg - Linux: on a Linux host, run
npm run dist:linux, launch the AppImage or install the.deb - Confirm the installed app actually launches after install, not just that packaging succeeded
- Windows: run
- Verify first-run experience
- AI setup confirms bundled OCR runtime availability
- Base-model download works from the app UI
- Disposable profile (temp demo, Windows) — optional, for demos or screen recordings without
touching your real
$env:APPDATA\StratoSort Stackdata. See Exact walkthrough: temp demo profile (Windows) below. - Confirm release branding inputs
- Either keep
assets/icons/StratoSortStackLogo.svg(or legacyassets/stratosort-logo.png) in the repo so workflows can regenerate icons/installer graphics - Or commit the generated files under
assets/icons/andassets/installer/
- Either keep
- Confirm docs
- Ensure
docs/INSTALL_GUIDE.mdmatches current installer names and OS prompts - Ensure release references are updated for the upcoming tag:
README.mdversion badgeCHANGELOG.mdcompare links ([Unreleased]and new version anchor).github/WORKFLOW_README.mdartifact names/checksum filenames
- Ensure
Older macOS startup workaround
Packaged macOS builds intentionally skip Electron's single-instance lock on macOS 13.x and older
to avoid a launch-time Electron crash seen on older Intel macOS systems.
Tradeoff:
- Affected macOS versions may allow duplicate app launches instead of focusing an existing instance.
- This is preferred over a crash-on-launch failure for supported release artifacts.
Release guidance:
- When smoke-testing macOS releases, confirm the packaged app launches on the oldest macOS version you still support.
- Revisit this workaround after any Electron runtime upgrade (currently
^45.1.0inpackage.json; see.github/dependabot.ymlfor the upgrade policy).
Disposable profile (Windows demo)
Use a throwaway --user-data-dir when you need first-run–style behavior for a demo or recording,
without changing your normal profile under $env:APPDATA\StratoSort Stack (same folder as
%APPDATA%\StratoSort Stack in Explorer).
Exact walkthrough: temp demo profile (Windows)
Use Windows PowerShell (not Command Prompt). The & operator is required to run an executable
whose path contains spaces.
Quit StratoSort completely (including the system tray if it stays open).
Create an empty folder for the disposable profile (adjust the path if you prefer):
New-Item -ItemType Directory -Force -Path C:\temp\stratosort-demo-profileLaunch the app with
--user-data-dirpointing at that folder. Use one of these:A — Packaged app (change the
.exepath if your install location differs):& "C:\Program Files\StratoSort Stack\StratoSort Stack.exe" --user-data-dir=C:\temp\stratosort-demo-profileB — From the repo (after a normal dev build, from the repo root):
cd <path-to-your-clone> npx cross-env NODE_ENV=development electron . --enable-logging --user-data-dir=C:\temp\stratosort-demo-profileConfirm you’re on the temp profile — open
C:\temp\stratosort-demo-profilein Explorer. After the UI loads, you should see new files (for examplesettings.json, logs, and later models if you download models). Your usual profile folder ($env:APPDATA\StratoSort Stack) should stay unchanged for this run.Record your demo — you should get first-run–style behavior. Allow time if large models download into this profile’s
modelsfolder.Quit the app when done.
Optional: delete the temp profile
Remove-Item -Recurse -Force C:\temp\stratosort-demo-profileYour real profile under
$env:APPDATA\StratoSort Stackwas not used for that session.
Tip: If something feels stuck or locked, use a new folder name (for example
stratosort-demo-profile-2) for the next take.
Tag-Triggered Releases (Recommended)
Push a semver tag:
git tag vX.Y.Z
git push origin vX.Y.Z
Replace X.Y.Z with the version from package.json after you bump it (for example v5.0.0).
This triggers:
mac-release.yml(macOS + Linux)release.yml(Windows)
Both also support workflow_dispatch against an existing tag for re-runs (e.g. after adding signing
secrets or fixing a transient build failure).
Release workflows are packaging/publish workflows, not full CI replacements. Run npm run ci before
tagging, then use the release workflows to build and upload artifacts.
Published Artifacts
Windows workflow publishes:
StratoSortStack-Setup-*.exeStratoSortStack-*-win-*.exe(portable)latest.yml— one channel file listing both the x64 and-arm64installers. electron-updater requests exactlylatest.ymlon Windows for every arch, so the x64 and arm64 matrix legs stagelatest-x64.yml/latest-arm64.ymlas artifacts and thepublish-releasejob merges them withscripts/merge-updater-metadata.js(x64 first — the updater serves the first.exeto clients whose arch appears in no filename) beforeverify-updater-metadataand upload. The staging names are never published.*.blockmapchecksums-windows-x64.sha256/checksums-windows-arm64.sha256(per matrix arch; installers and blockmaps only)
macOS workflow publishes:
StratoSortStack-*.dmgStratoSortStack-*.ziplatest-mac.yml— one channel file listing the x64 and arm64 zips/dmgs (signed + notarized builds only; unsigned builds ship no updater metadata). electron-updater requests exactlylatest-mac.ymlon macOS, so the per-archlatest-mac-<arch>.ymlstaging artifacts are merged inpublish-releasethe same way as Windows and are never published.checksums-macos.sha256
Linux workflow publishes:
StratoSortStack-*-linux-x86_64.AppImageandStratoSortStack-*-linux-arm64.AppImageStratoSortStack-*-linux-amd64.debandStratoSortStack-*-linux-arm64.deblatest-linux.yml(x64) andlatest-linux-arm64.yml(when produced)checksums-linux.sha256
Application auto-updates
Shipped installers check the public GitHub Releases feed for updates by default, so the repository
and its releases must stay public (private releases are not visible to electron-updater without
auth). STRATOSORT_ENABLE_AUTO_UPDATES=false disables the updater; optional
STRATOSORT_UPDATE_FEED_URL points at a generic HTTPS directory with latest.yml. See
CONFIG.md and
UPDATE_HOST.md.
Updater live smoke (run before every update-enabled release)
The renderer/main wiring is oracled in unit + e2e suites (UpdateIndicator.test.js,
autoUpdater.test.js), but only a real installed build proves the end-to-end chain. Once per
update-enabled release:
- Build TWO versions: the release candidate v(N) and a throwaway v(N+1) (bump
package.jsonversion, rebuild). Both signed. - Stage v(N+1)'s
latest.yml+ installer +.blockmapin an HTTPS directory with a valid certificate (Cloudflare R2 / any static host).scripts/verify-updater-metadata.jsmust pass on the staged files. - On a clean Windows VM: install v(N), then launch with
STRATOSORT_UPDATE_FEED_URL=<staging URL>,STRATOSORT_UPDATE_FEED_ALLOWED_HOSTS=<staging host>. - Verify the full chain: update event → background download → "Update Ready" indicator → Apply → in-flight-op drain → relaunch as v(N+1).
- Negative checks: the portable build refuses with the portable reason; a Microsoft Store
build (
process.windowsStore) refuses with the Store reason; a wrongALLOWED_HOSTSvalue blocks the feed. - Uninstall/reinstall v(N) to confirm the rollback story.
Post-Install Smoke (Windows NSIS)
Use this after npm run dist:win or an official CI-built StratoSortStack-Setup-*.exe to confirm
the install and first-run path matches INSTALL_GUIDE.md.
Preconditions
- Quit StratoSort completely (tray included).
- Prefer a clean profile: new Windows user, VM, or disposable folder with
--user-data-dir(see the disposable profile section above).
Automated checks (developer machine)
From the repo root after a Windows dist build:
node scripts/verify-updater-metadata.js --platform=win --build-root=release/build
npm test -- test/autoUpdater.test.js --runInBand --no-coverage
Manual steps
- Install — Run
release\build\StratoSortStack-Setup-<version>.exe(or your tagged artifact). - Profile path — Confirm app data lives under
%APPDATA%\StratoSort Stack(not the legacy lowercasestratosort-corepath that older postinstall scripts derived from the npm package name). - First launch — App opens without an updater error notification; the startup update check against GitHub Releases succeeds (or reports "up to date").
- Settings → Application — "Background Update Checks" is on and can be switched off (see CONFIG.md and UPDATE_HOST.md).
- Models — Complete welcome / model setup; optional "Download Base Models" and confirm
.gguffiles appear under%APPDATA%\StratoSort Stack\modelswhen downloads succeed.
Packaged-integrity checks
Run these once per release on a packaged build. Every path below exists only when packaged:
main, preload and the renderer live inside the integrity-validated app.asar, while seven
worker entry bundles are unpacked beside it (asarUnpack in electron-builder.json). A worker that
resolves fine in npm run dev can still fail to load from app.asar.unpacked/dist/ — that is
exactly how the settings-import worker broke in #441 — so development runs prove nothing here.
- Renderer loads from inside the asar.
getRendererIndexPath()prefersapp.asar/dist/index.html; the unpacked renderer is a legacy fallback only. Confirm the window loads the in-asar copy (a packaged run that silently falls back meansasarUnpackwidened again). - OCR on an image — exercises the unpacked
ocrWorkerbundle plustesseract.js-core. - An embedding batch — exercises
embeddingWorkerand theEmbeddingQueueretry path. - An Atlas UMAP projection — exercises
umapProjectionWorker. - Log export (Settings → Diagnostics) — exercises
logExportWorker. - A settings import — exercises
settingsImportParserWorker(the #441 regression surface). - Analyze a document and an image — exercises
textExtractionWorkerandimagePreprocessWorker. - Tamper test. Edit one byte inside
app.asar, then launch: the app must refuse to start. A successful launch means asar integrity is not being enforced. - Fuses on the built binary.
npx @electron/fuses read --app <path to the app>and compare againstelectronFusesinelectron-builder.json. (npm run verify:electron-fuses-configchecks the config in CI, including that noasarUnpackpattern unpacksdist/main.js; it cannot check the shipped bits.)
Two more release-time runtime items pair with this pass, both verifiable only on a packaged build:
- the updater live smoke — § Updater live smoke, including the portable/Store negative checks, and
- the license smoke on a fresh
--user-data-dir: with no key, Settings → License reportsfreeand every feature works; after pasting a valid key it reportslicensedwith the licensee and update window; a key whose update window ended before this build reportsversion_not_covered.
Portable build
StratoSortStack-*-win-x64.exe does not use the same auto-update path as the NSIS installer;
see INSTALL_GUIDE.md for portable vs full-installer behavior.
Signing And Notarization Requirements
Tag-triggered release workflows support both official signed releases and unsigned ad-hoc artifacts. If the required signing/notarization secrets are missing, the workflows continue in unsigned mode. As of v4.0.0, macOS releases are signed and notarized when all five Apple secrets below are set.
Required credentials:
- Windows (Azure Trusted Signing): repository Variables
AZURE_TENANT_ID,AZURE_CLIENT_ID(identifiers, not secret) and repository SecretAZURE_CLIENT_SECRET(the only true credential). - macOS:
MACOS_CSC_LINK,MACOS_CSC_KEY_PASSWORD,APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID
Windows signing uses Azure Trusted Signing (account stratosort-signing, certificate profile
stratosortcore-public-trust-cert, endpoint https://eus.codesigning.azure.net/, publisher
Kennedy Applied Sciences, Inc.). Those four signing-identity values are public and stable, so they
are hardcoded in scripts/configure-win-signing.js. The workflow injects win.azureSignOptions
into the per-arch electron-builder config at build time via that script — the block is intentionally
absent from the committed configs so local and fork builds stay unsigned without Azure credentials.
Credential sources:
AZURE_TENANT_ID/AZURE_CLIENT_ID(repo Variables) +AZURE_CLIENT_SECRET(repo Secret): a Microsoft Entra app registration (service principal) granted the Trusted Signing Certificate Profile Signer role on the signing account. The tenant/client IDs are plain identifiers; only the client secret authenticates. Paste with no trailing newline — the detect step inrelease.ymltrims the variables, but a clean paste avoids surprises.
For the end-to-end Windows runbook that produces the five macOS secrets from a Windows 11 host
without any Mac hardware (OpenSSL CSR + cert request + .p12 assembly + GitHub Secrets paste), see
MAC_SIGNING.md. The runbook is wrapped by npm run setup:mac-signing
(PowerShell helper at scripts/setup-mac-signing.ps1).
Unsigned macOS artifacts will not be notarized and may trigger Gatekeeper warnings on first launch. For one-off rebuilds or draft releases, you can still use the manual dist workflow or local dist commands instead of the tag-triggered workflows.
Recommended operator practice for ad-hoc unsigned builds:
- Ensure
checksums-*.sha256files are uploaded with each build. - Verify install flows manually on clean Windows/macOS test machines.
Manual Dist Workflow
Use .github/workflows/manual-dist.yml (workflow_dispatch) when you need:
- Windows-only or macOS-only rebuilds
- A draft release with combined artifacts
- Ad-hoc release testing without tagging
Local Dist Commands
npm ci
npm run dist:win
# Run on macOS only
npm run dist:mac:arm64
Windows cannot produce macOS DMG/ZIP artifacts locally. Build macOS distributions on a macOS host or
via the mac-release.yml / manual-dist.yml GitHub Actions workflows, which run on macos-latest.
All artifacts are written to release/build/.
Local Checksum Commands
Windows checksum file:
Get-ChildItem release/build -File |
Where-Object { $_.Name -match '^StratoSortStack-.*\.(exe|blockmap)$|^latest\.yml$' } |
ForEach-Object {
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
"$hash *$($_.Name)"
} | Out-File release/build/checksums-windows-x64.sha256 -Encoding ASCII
# (CI generates per-arch files: checksums-windows-x64.sha256 / checksums-windows-arm64.sha256)
macOS checksum file:
cd release/build
shasum -a 256 StratoSortStack-*.dmg StratoSortStack-*.zip > checksums-macos.sha256
Notes on AI Packaging
- The AI stack (node-llama-cpp + Orama) runs in-process.
- AI models are not bundled in installers; users download them in-app.
- OCR runtime support is bundled/fallback-ready via packaged runtime assets.