Every release produces three artifact groups under one version and changelog:
| Artifact | Format | Destination |
|---|---|---|
| VSCode extension | .vsix |
VS Code Marketplace + OpenVSX |
| Standalone (Windows) | .exe (NSIS installer) |
GitHub Release + Tauri updater |
| Standalone (macOS, Apple Silicon) | .tar.gz (contains signed .app) |
GitHub Release + Tauri updater |
| Standalone (Linux) | .AppImage |
GitHub Release + Tauri updater |
Human-driven steps, in order:
- Update dependencies page — run
node website/scripts/generate-deps.jsand review the diff inwebsite/src/data/dependencies.json. Commit if changed. - Finalize changelog — promote the
[Unreleased]section inCHANGELOG.mdto[X.Y.Z]with today's date. Write release notes covering both standalone and VSCode changes. - Bump versions — update
versionin all three places: - Commit and tag —
git commit -m "Release vX.Y.Z"thengit tag vX.Y.Z. - Push —
git push && git push origin vX.Y.Z. This triggers CI (Stage 1). - Wait for CI — monitor the workflow run. VSCode extension publishes automatically.
- Run local signing —
./scripts/sign-and-deploy.sh all X.Y.Z. Plug in the PIV USB key first. The script will:- Download unsigned CI artifacts
- Sign macOS (will prompt for
APPLE_SIGN_PASSif not set) - Sign Windows (will prompt for
EV_SIGN_PINif not set) - Generate Tauri update manifest and copy to
website/public/standalone-latest.json - Create the GitHub Release with all signed assets
- Deploy website — commit the updated
website/public/standalone-latest.jsonand deploy mouseterm.com so the updater endpoint is live. - Verify the release
- Check GitHub Release assets are correct
- On a Mac: extract the
.tar.gz, open the.app, confirm no Gatekeeper warnings - On Windows: run the
.exeinstaller, confirm no SmartScreen warnings - Confirm Tauri auto-updater picks up the new version (test from a previous version)
- Confirm VSCode extension is live on Marketplace and OpenVSX
A single version number (X.Y.Z) applies to all artifacts. The version lives in three places that must stay in sync:
standalone/src-tauri/tauri.conf.json→versionvscode-ext/package.json→versionlib/package.json→version(if applicable)
A release is triggered by pushing a tag: v0.1.0. This is intentionally a single tag (not separate vscode-ext/v* and standalone/v* tags) because we want one changelog entry for both.
Code signing for Windows requires a physical USB hardware key (EV cert via PIV). macOS signing uses a local Developer ID cert. Both must happen locally. So:
Stage 1: CI (GitHub Actions)
→ Build unsigned Tauri apps (win, mac, linux)
→ Build + publish VSCode extension
→ Upload unsigned Tauri artifacts
Stage 2: Local (sign-and-deploy.sh)
→ Download CI artifacts
→ Sign macOS (codesign + notarize)
→ Sign Windows (jsign + PIV hardware key)
→ Generate Tauri update manifest with signatures
→ Upload signed artifacts to GitHub Release
Triggered by tag push v*. Three parallel jobs:
Runs on ubuntu-22.04 (linux), macos-latest (mac), and windows-latest (win). Uses tauri-apps/tauri-action@v0.
strategy:
matrix:
include:
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: macos-latest
target: aarch64-apple-darwin
- platform: windows-latest
target: x86_64-pc-windows-msvcEach matrix leg:
- Checkout, setup Node 22, pnpm 10, Rust stable
- Install workspace dependencies once from the repo root with
pnpm install --frozen-lockfile - Install system deps (Linux: libgtk, libwebkit, etc.)
- Build via
tauri-action— but skip signing (noAPPLE_SIGNING_IDENTITY, noTAURI_SIGNING_PRIVATE_KEY) - Upload artifacts (installers + bundles) via
actions/upload-artifact
Note: We do NOT use tauri-action's built-in GitHub Release creation. We create the release locally after signing.
Runs on ubuntu-latest:
- Checkout, setup Node 22, pnpm 10
pnpm install --frozen-lockfileat the repo rootpnpm --filter mouseterm-lib testpnpm --filter mouseterm build:frontend && pnpm --filter mouseterm buildnpx vsce package --no-dependencies- Upload
.vsixas artifact
Runs after build-vscode succeeds:
- Download
.vsixartifact npx vsce publish --packagePath *.vsix --no-dependenciesnpx ovsx publish --packagePath *.vsix --no-dependencies
This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardware key needed).
Migration note: This replaces the existing .github/workflows/publish-vscode.yml, which was triggered by vscode-ext/v* tags and has never been run. That workflow should be deleted when the unified release workflow is created. Fixes from the old workflow: use ubuntu-latest instead of macos-latest, upgrade to Node 22, and unify under the v* tag convention.
scripts/sign-and-deploy.sh — modeled on the Type The Rhythm script.
The script uses a three-directory layout under release-signed/:
| Directory | Purpose | Mutated? |
|---|---|---|
downloads/ |
Raw CI artifacts, cached per-artifact | Never — read-only after download |
work/ |
Fresh copy of downloads for each signing run | Yes — codesign, jsign, and NSIS path patching all modify files here |
release-assets/ |
Final signed artifacts for GitHub Release upload | Yes — built from signed work copies |
Key invariant: Downloaded artifacts in downloads/ are never modified. All signing and patching operates on copies in work/. This means:
- Re-running a signing step after a failure doesn't require re-downloading
- Modifying the signing scripts (e.g.
patch-nsis-paths.pl) doesn't require re-downloading - Per-artifact caching: each artifact has its own download marker, so a partial download failure only retries the failed artifacts
brew install gh jsign
gh auth login
xcode-select --install
tauri signer generate # one-time: creates update signing keypair| Platform | Tool | Identity |
|---|---|---|
| macOS | codesign + notarytool | Developer ID Application: DiffPlug LLC (LXW8WAGWYX) |
| Windows | jsign | PIV hardware key, alias AUTHENTICATION, TSA http://ts.ssl.com |
There are two independent signing layers. OS signing proves the executable is from DiffPlug; Tauri signing proves the update bundle hasn't been tampered with in transit. Both are required — they protect different things at different points in time.
| Layer | What it signs | Who verifies | What happens without it |
|---|---|---|---|
| OS (codesign / jsign) | The executable (.app / .exe) |
The OS, on launch | Gatekeeper / SmartScreen warnings |
| Tauri updater (ed25519) | The update bundle (.tar.gz / .exe / .AppImage) |
The running app, on update | Updater rejects the download |
Order matters: OS-sign the inner executable first, then package it into the update bundle, then Tauri-sign the bundle. The .sig file is generated from the final bundle that already contains the OS-signed binary.
codesign/jsign the executable
→ package into update bundle (.tar.gz for macOS; installer/AppImage directly on Windows/Linux)
→ Tauri-sign the bundle → produces .sig file
→ upload bundle + .sig to GitHub Release
./scripts/sign-and-deploy.sh all 0.1.0
- Wait for CI — find the workflow run for tag
v0.1.0, poll until complete - Download artifacts —
gh run downloadintorelease-signed/ - Sign macOS (OS layer)
- Fix any framework symlink issues (artifact downloads flatten symlinks)
- Sign nested code explicitly first:
Contents/MacOS/*,*.node,*.dylib, andspawn-helper - Sign the Node sidecar with
standalone/src-tauri/entitlements-macos-node.plist - Sign the outer
.appwithout--deep;--deepwould re-sign Node and drop its entitlements - Verify the signed Node sidecar launches and can load
node-pty - Notarize via
xcrun notarytool submit --wait xcrun stapler staple- Re-package signed
.appinto.dmg(for direct download) and.tar.gz(for updater)
- Sign Windows (OS layer)
- Sign the inner exe:
jsign --storetype PIV --storepass "$PIN" --alias AUTHENTICATION --tsaurl http://ts.ssl.com --tsmode RFC3161 MouseTerm.exe - Rebuild the NSIS installer around the signed exe
- Sign the installer exe:
jsign ... MouseTerm-windows-x64-setup.exe
- Sign the inner exe:
- Sign update bundles (Tauri layer)
- Tauri-sign each update bundle using
TAURI_SIGNING_PRIVATE_KEY - Current Tauri v2 output mode (
createUpdaterArtifacts: true) uses the NSIS installer.exedirectly on Windows and the.AppImagedirectly on Linux - This produces a
.sigfile per bundle - Build the update manifest JSON (see below) with the
.sigcontents inline
- Tauri-sign each update bundle using
- Create GitHub Release
gh release create v0.1.0 --title "v0.1.0" --notes-file CHANGELOG.md- Upload: update bundles (
.tar.gz,.exe,.AppImage) - If a draft release already exists for the tag, publish it after uploading assets
- Verify the tag exists on the remote before creating or publishing the release
- Verify — spot-check signatures, confirm release assets are correct
Windows release builds use the GUI subsystem, so launching mouseterm.exe from a terminal returns immediately and does not stream stdout/stderr. The Tauri backend writes sidecar diagnostics to %LOCALAPPDATA%\MouseTerm\mouseterm.log on Windows, or to $TMPDIR/mouseterm.log on other platforms. Set MOUSETERM_LOG_FILE to override the path.
./scripts/sign-and-deploy.sh resume 0.1.0 # re-download + sign + release
./scripts/sign-and-deploy.sh sign-mac # re-sign macOS only
./scripts/sign-and-deploy.sh sign-win # re-sign Windows only
./scripts/sign-and-deploy.sh sign-updates 0.1.0 # regenerate updater signatures from existing signed work
./scripts/sign-and-deploy.sh release 0.1.0 # re-create GitHub Release onlyAll release assets use stable filenames (no version in the name). This allows hotlinking directly from mouseterm.com via GitHub's /latest/download/ redirect, which always resolves to the most recent release.
| Asset | Filename | Purpose |
|---|---|---|
| Windows | MouseTerm-windows-x64-setup.exe |
Download + Tauri updater |
| macOS | MouseTerm-macos-aarch64.tar.gz |
Download + Tauri updater |
| Linux | MouseTerm-linux-x86_64.AppImage |
Download + Tauri updater |
The mouseterm.com download page can link directly to the latest release with no server-side logic:
https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-windows-x64-setup.exe
https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-macos-aarch64.tar.gz
https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-linux-x86_64.AppImage
These can later be migrated to mouseterm.com/download/... URLs backed by Cloudflare R2 (for analytics) without changing anything in the app — only the website links and the updater endpoint URL in tauri.conf.json would change.
In standalone/src-tauri/tauri.conf.json:
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "<TAURI_SIGNING_PUBLIC_KEY>",
"endpoints": [
"https://mouseterm.com/standalone-latest.json"
]
}
}
}createUpdaterArtifacts: true is the Tauri v2 artifact mode. In this mode Windows updates use the NSIS installer .exe directly, Linux updates use the .AppImage directly, and macOS updates use the .app.tar.gz archive. Do not configure "v1Compatible" unless intentionally producing legacy .nsis.zip and .AppImage.tar.gz updater bundles for old Tauri v1 clients.
And in the Rust app bootstrap (standalone/src-tauri/src/lib.rs), the updater plugin is registered with:
.plugin(tauri_plugin_updater::Builder::new().build())standalone/src-tauri/Cargo.toml must include tauri-plugin-updater = "2" so the configured updater endpoint is actually active at runtime.
Generated by the local script after signing. The script writes it to website/public/standalone-latest.json so it's served from mouseterm.com/standalone-latest.json via Cloudflare Pages. This gives us request analytics on update checks.
{
"version": "0.1.0",
"notes": "Release notes here",
"pub_date": "2026-03-25T12:00:00Z",
"platforms": {
"windows-x86_64": {
"url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-windows-x64-setup.exe",
"signature": "<contents of .sig file>"
},
"darwin-aarch64": {
"url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-macos-aarch64.tar.gz",
"signature": "<contents of .sig file>"
},
"linux-x86_64": {
"url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-linux-x86_64.AppImage",
"signature": "<contents of .sig file>"
}
}
}Note: the update manifest URLs include the version in the path (/v0.1.0/) but the filenames are stable. The manifest itself is served from mouseterm.com/standalone-latest.json — Cloudflare Pages analytics tracks every update check.
A single CHANGELOG.md at the repo root, following Keep a Changelog format. The [Unreleased] section is promoted to [X.Y.Z] at release time. The release notes include both standalone and VSCode changes in one entry.
| Secret | Where | Purpose |
|---|---|---|
VSCE_PAT |
GitHub Actions secret | VS Code Marketplace publish |
OVSX_PAT |
GitHub Actions secret | OpenVSX publish |
GITHUB_TOKEN |
GitHub Actions (automatic) | Artifact upload |
APPLE_SIGNING_IDENTITY |
Local keychain | macOS codesign |
APPLE_ID |
Local env / prompted | Notarization |
APPLE_SIGN_PASS |
Local env / prompted | Notarization password |
APPLE_TEAM_ID |
Local env / hardcoded | Notarization |
EV_SIGN_PIN |
Local env / prompted | Windows PIV signing |
TAURI_SIGNING_PRIVATE_KEY |
Local env | Tauri update signatures |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Local env / prompted | Tauri update key password |