From 5a6a7b8e73f1b565ecf08a84e4cebcb203d15bfb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 25 Mar 2026 17:09:44 -0700 Subject: [PATCH 01/11] Our deploy "spec". --- docs/specs/deploy.md | 296 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 docs/specs/deploy.md diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md new file mode 100644 index 00000000..414d88ee --- /dev/null +++ b/docs/specs/deploy.md @@ -0,0 +1,296 @@ +# Deploy Spec + +## What we ship + +Every release produces three artifact groups under one version and changelog: + +| Artifact | Format | Destination | +|----------|--------|-------------| +| VSCode extension | `.vsix` | VS Code Marketplace + OpenVSX | +| Standalone (Windows) | NSIS `.exe` installer | GitHub Release + Tauri updater | +| Standalone (macOS) | `.dmg` (install) + `.tar.gz` (update) | GitHub Release + Tauri updater | +| Standalone (Linux) | AppImage + `.deb` | GitHub Release + Tauri updater | + +## Versioning + +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` → `version` +- `vscode-ext/package.json` → `version` +- `lib/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. + +## Two-stage pipeline + +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 +``` + +## Stage 1: CI workflow + +Triggered by tag push `v*`. Three parallel jobs: + +### Job: `build-standalone` (matrix) + +Runs on `ubuntu-latest` (win + linux) and `macos-latest` (mac). Uses `tauri-apps/tauri-action@v0`. + +```yaml +strategy: + matrix: + include: + - platform: ubuntu-22.04 + rust-targets: x86_64-unknown-linux-gnu + - platform: macos-latest + rust-targets: aarch64-apple-darwin + - platform: macos-latest + rust-targets: x86_64-apple-darwin + - platform: windows-latest + rust-targets: x86_64-pc-windows-msvc +``` + +Each matrix leg: +1. Checkout, setup Node 22, pnpm 10, Rust stable +2. Install system deps (Linux: libgtk, libwebkit, etc.) +3. `pnpm install` in `standalone/` +4. Build via `tauri-action` — but **skip signing** (no `APPLE_SIGNING_IDENTITY`, no `TAURI_SIGNING_PRIVATE_KEY`) +5. 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. + +### Job: `build-vscode` + +Runs on `ubuntu-latest`: +1. Checkout, setup Node 22, pnpm 10 +2. `cd lib && pnpm install && pnpm test` +3. `cd vscode-ext && pnpm build:frontend && pnpm install && pnpm build` +4. `npx vsce package --no-dependencies` +5. Upload `.vsix` as artifact + +### Job: `publish-vscode` + +Runs after `build-vscode` succeeds: +1. Download `.vsix` artifact +2. `npx vsce publish --packagePath *.vsix --no-dependencies` +3. `npx 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. + +## Stage 2: Local script + +`scripts/sign-and-deploy.sh` — modeled on the Type The Rhythm script. + +### Prerequisites + +```bash +brew install gh jsign +gh auth login +xcode-select --install +tauri signer generate # one-time: creates update signing keypair +``` + +### Signing identity + +| Platform | Tool | Identity | +|----------|------|----------| +| macOS | codesign + notarytool | Developer ID Application: DiffPlug LLC (LXW8WAGWYX) | +| Windows | jsign | PIV hardware key, alias AUTHENTICATION, TSA http://ts.ssl.com | + +### Two signing layers + +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` / `.nsis.zip`) | 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 / .nsis.zip) + → Tauri-sign the bundle → produces .sig file + → upload bundle + .sig to GitHub Release +``` + +### Flow + +``` +./scripts/sign-and-deploy.sh all 0.1.0 +``` + +1. **Wait for CI** — find the workflow run for tag `v0.1.0`, poll until complete +2. **Download artifacts** — `gh run download` into `release-signed/` +3. **Sign macOS** (OS layer) + - Fix any framework symlink issues (artifact downloads flatten symlinks) + - `codesign --force --deep --sign "$IDENTITY" --entitlements ... --options runtime` + - Notarize via `xcrun notarytool submit --wait` + - `xcrun stapler staple` + - Re-package signed `.app` into `.dmg` (for direct download) and `.tar.gz` (for updater) +4. **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.exe` +5. **Sign update bundles** (Tauri layer) + - Tauri-sign each update bundle (the `.tar.gz` and `.nsis.zip` from steps 3-4) using `TAURI_SIGNING_PRIVATE_KEY` + - This produces a `.sig` file per bundle + - Build the update manifest JSON (see below) with the `.sig` contents inline +6. **Create GitHub Release** + - `gh release create v0.1.0 --title "v0.1.0" --notes-file CHANGELOG.md` + - Upload: signed installers (`.dmg`, `.exe`, `.AppImage`, `.deb`) + update bundles (`.tar.gz`, `.nsis.zip`) + `.sig` files + `latest.json` manifest +7. **Verify** — spot-check signatures, confirm release assets are correct + +### Resuming after failure + +```bash +./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 release 0.1.0 # re-create GitHub Release only +``` + +## Artifact filenames + +All 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 installer | `MouseTerm-windows-x64.exe` | Direct download | +| Windows update bundle | `MouseTerm-windows-x64.nsis.zip` | Tauri updater | +| macOS installer (ARM) | `MouseTerm-macos-aarch64.dmg` | Direct download | +| macOS update bundle (ARM) | `MouseTerm-macos-aarch64.tar.gz` | Tauri updater | +| macOS installer (Intel) | `MouseTerm-macos-x86_64.dmg` | Direct download | +| macOS update bundle (Intel) | `MouseTerm-macos-x86_64.tar.gz` | Tauri updater | +| Linux AppImage | `MouseTerm-linux-x86_64.AppImage` | Direct download | +| Linux update bundle | `MouseTerm-linux-x86_64.AppImage.tar.gz` | Tauri updater | +| Linux deb | `MouseTerm-linux-x86_64.deb` | Direct download | +| Update manifest | `latest.json` | Tauri updater endpoint | + +### Download hotlinks + +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.exe +https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-macos-aarch64.dmg +https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-macos-x86_64.dmg +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. + +## Tauri auto-updater + +### Configuration + +In `standalone/src-tauri/tauri.conf.json`: + +```json +{ + "bundle": { + "createUpdaterArtifacts": true + }, + "plugins": { + "updater": { + "pubkey": "", + "endpoints": [ + "https://github.com/diffplug/mouseterm/releases/latest/download/latest.json" + ] + } + } +} +``` + +### Update manifest (`latest.json`) + +Generated by the local script after signing, uploaded as a GitHub Release asset: + +```json +{ + "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.nsis.zip", + "signature": "" + }, + "darwin-aarch64": { + "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-macos-aarch64.tar.gz", + "signature": "" + }, + "darwin-x86_64": { + "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-macos-x86_64.tar.gz", + "signature": "" + }, + "linux-x86_64": { + "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-linux-x86_64.AppImage.tar.gz", + "signature": "" + } + } +} +``` + +Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) but the *filenames* are stable. The updater endpoint itself (`/latest/download/latest.json`) always resolves to the most recent release's manifest. + +## Release checklist + +Human-driven steps, in order: + +1. **Update dependencies page** — run `node website/scripts/generate-deps.js` and review the diff in `website/src/data/dependencies.json`. Commit if changed. +2. **Finalize changelog** — promote the `[Unreleased]` section in `CHANGELOG.md` to `[X.Y.Z]` with today's date. Write release notes covering both standalone and VSCode changes. +3. **Bump versions** — update `version` in all three places: + - `standalone/src-tauri/tauri.conf.json` + - `vscode-ext/package.json` + - `lib/package.json` +4. **Commit and tag** — `git commit -m "Release vX.Y.Z"` then `git tag vX.Y.Z`. +5. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1). +6. **Wait for CI** — monitor the workflow run. VSCode extension publishes automatically. +7. **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_PASS` if not set) + - Sign Windows (will prompt for `EV_SIGN_PIN` if not set) + - Generate Tauri update manifest + - Create the GitHub Release with all signed assets +8. **Verify the release** + - Check GitHub Release assets are correct + - On a Mac: download the `.dmg`, open it, confirm no Gatekeeper warnings + - On Windows: download the `.exe` installer, 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 +9. **Deploy website** — if the release includes website changes, deploy mouseterm.com. + +## Changelog + +A single `CHANGELOG.md` at the repo root, following [Keep a Changelog](https://keepachangelog.com/) 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. + +## Environment / secrets + +| 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 | + From 9d6f4c9e916856ee3c13e80234cfe615475d9acc Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 25 Mar 2026 17:10:42 -0700 Subject: [PATCH 02/11] First cut at publishing pipeline. --- .github/workflows/publish-vscode.yml | 57 --- .github/workflows/release.yml | 151 +++++++ scripts/sign-and-deploy.sh | 584 +++++++++++++++++++++++++++ standalone/src-tauri/tauri.conf.json | 9 + vscode-ext/CHANGELOG.md | 6 +- 5 files changed, 745 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/publish-vscode.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/sign-and-deploy.sh diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml deleted file mode 100644 index c673c7c1..00000000 --- a/.github/workflows/publish-vscode.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Publish VSCode Extension - -on: - push: - tags: - - 'vscode-ext/v*' - -jobs: - publish: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 10 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - cache-dependency-path: | - lib/pnpm-lock.yaml - vscode-ext/pnpm-lock.yaml - - # Install and build frontend for VSCode - - name: Install frontend dependencies - run: cd lib && pnpm install - - - name: Run tests - run: cd lib && pnpm test - - - name: Build frontend for VSCode - run: cd vscode-ext && pnpm build:frontend - - # Install and build extension - - name: Install extension dependencies - run: cd vscode-ext && pnpm install - - - name: Build extension - run: cd vscode-ext && pnpm build - - # Package - - name: Package extension - run: cd vscode-ext && npx vsce package --no-dependencies - - # Publish to VS Code Marketplace - - name: Publish to VS Code Marketplace - run: cd vscode-ext && npx vsce publish --no-dependencies - env: - VSCE_PAT: ${{ secrets.VSCE_PAT }} - - # Publish to OpenVSX - - name: Publish to OpenVSX - run: cd vscode-ext && npx ovsx publish --no-dependencies - env: - OVSX_PAT: ${{ secrets.OVSX_PAT }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3fdc8da2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,151 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build-standalone: + name: Build Standalone (${{ matrix.target }}) + strategy: + matrix: + include: + - platform: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + artifact-name: standalone-linux-x64 + - platform: macos-latest + target: aarch64-apple-darwin + artifact-name: standalone-mac-aarch64 + - platform: macos-latest + target: x86_64-apple-darwin + artifact-name: standalone-mac-x86_64 + - platform: windows-latest + target: x86_64-pc-windows-msvc + artifact-name: standalone-win-x64 + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: standalone/src-tauri + + - name: Install system dependencies (Linux) + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Install frontend dependencies + run: pnpm install + working-directory: standalone + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + projectPath: standalone + tauriScript: pnpm tauri + args: --target ${{ matrix.target }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact-name }} + path: | + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.msi + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.dmg + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.app + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.app.tar.gz + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.app.tar.gz.sig + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage.tar.gz + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage.tar.gz.sig + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.deb + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.nsis.zip + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.nsis.zip.sig + + build-vscode: + name: Build VSCode Extension + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install and test lib + run: cd lib && pnpm install && pnpm test + + - name: Build frontend for VSCode + run: cd vscode-ext && pnpm build:frontend + + - name: Install extension dependencies + run: cd vscode-ext && pnpm install + + - name: Build extension + run: cd vscode-ext && pnpm build + + - name: Package extension + run: cd vscode-ext && npx vsce package --no-dependencies + + - name: Upload .vsix + uses: actions/upload-artifact@v4 + with: + name: vscode-extension + path: vscode-ext/*.vsix + + publish-vscode: + name: Publish VSCode Extension + needs: build-vscode + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install vsce and ovsx + run: cd vscode-ext && pnpm install + + - name: Download .vsix + uses: actions/download-artifact@v4 + with: + name: vscode-extension + path: vscode-ext + + - name: Publish to VS Code Marketplace + run: cd vscode-ext && npx vsce publish --packagePath *.vsix --no-dependencies + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + + - name: Publish to OpenVSX + run: cd vscode-ext && npx ovsx publish --packagePath *.vsix --no-dependencies + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh new file mode 100755 index 00000000..ff311806 --- /dev/null +++ b/scripts/sign-and-deploy.sh @@ -0,0 +1,584 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# Local Code Signing and GitHub Release Script +# ============================================================================= +# Downloads unsigned CI artifacts, signs macOS and Windows binaries locally, +# generates Tauri update manifest, and creates a GitHub Release. +# +# Usage: ./scripts/sign-and-deploy.sh all +# Example: ./scripts/sign-and-deploy.sh all 0.1.0 +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WORK_DIR="$REPO_ROOT/release-signed" + +# ============================================================================= +# Configuration +# ============================================================================= + +# macOS Signing Identity +MACOS_IDENTITY="Developer ID Application: DiffPlug LLC (LXW8WAGWYX)" +MACOS_TEAM_ID="LXW8WAGWYX" +APPLE_ID="edgar.twigg@gmail.com" + +# Windows Signing (jsign with PIV) +JSIGN_ALIAS="AUTHENTICATION" +TSA_URL="http://ts.ssl.com" + +# GitHub repo +GITHUB_REPO="diffplug/mouseterm" + +# Stable filenames for release assets +FNAME_WIN_EXE="MouseTerm-windows-x64.exe" +FNAME_WIN_UPDATE="MouseTerm-windows-x64.nsis.zip" +FNAME_MAC_ARM_DMG="MouseTerm-macos-aarch64.dmg" +FNAME_MAC_ARM_UPDATE="MouseTerm-macos-aarch64.tar.gz" +FNAME_MAC_INTEL_DMG="MouseTerm-macos-x86_64.dmg" +FNAME_MAC_INTEL_UPDATE="MouseTerm-macos-x86_64.tar.gz" +FNAME_LINUX_APPIMAGE="MouseTerm-linux-x86_64.AppImage" +FNAME_LINUX_UPDATE="MouseTerm-linux-x86_64.AppImage.tar.gz" +FNAME_LINUX_DEB="MouseTerm-linux-x86_64.deb" +FNAME_MANIFEST="latest.json" + +# ============================================================================= +# Helper Functions +# ============================================================================= + +log() { echo "[$(date '+%H:%M:%S')] $*"; } +error() { echo "[ERROR] $*" >&2; exit 1; } +warn() { echo "[WARN] $*" >&2; } + +prompt_secret() { + local varname="$1" + local prompt="$2" + if [[ -z "${!varname:-}" ]]; then + read -rsp "$prompt: " "$varname" + echo + export "$varname" + fi +} + +check_command() { + command -v "$1" &>/dev/null || error "Required command not found: $1. Install with: $2" +} + +check_git_clean() { + log "Checking git status..." + + if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then + error "Local changes detected. Commit or stash changes before deploying." + fi + + if [[ -n "$(git -C "$REPO_ROOT" ls-files --others --exclude-standard)" ]]; then + error "Untracked files detected. Commit or remove them before deploying." + fi + + local branch + branch=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) + local upstream + upstream=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null) || true + + if [[ -n "$upstream" ]]; then + local ahead + ahead=$(git -C "$REPO_ROOT" rev-list --count "$upstream..HEAD") + if [[ "$ahead" -gt 0 ]]; then + error "You have $ahead unpushed commit(s). Push changes before deploying." + fi + else + warn "No upstream branch set. Cannot verify commits are pushed." + fi + + log "Git status clean." +} + +# ============================================================================= +# Download CI Artifacts +# ============================================================================= + +download_artifacts() { + local version="$1" + local tag="v$version" + + log "Finding workflow run for tag $tag..." + + check_command gh "brew install gh && gh auth login" + + local run_id="" + local attempts=0 + local max_attempts=60 # 5 minutes of retries + + while [[ -z "$run_id" ]] && [[ $attempts -lt $max_attempts ]]; do + run_id=$(gh run list \ + --repo "$GITHUB_REPO" \ + --workflow release.yml \ + --limit 10 \ + --json databaseId,headBranch,status \ + --jq ".[] | select(.headBranch == \"$tag\") | .databaseId" \ + | head -1) + + if [[ -z "$run_id" ]]; then + attempts=$((attempts + 1)) + log "Workflow not found yet, waiting... (attempt $attempts/$max_attempts)" + sleep 5 + fi + done + + [[ -z "$run_id" ]] && error "Could not find workflow run for tag $tag" + + log "Found workflow run: $run_id" + log "Waiting for workflow to complete (this may take several minutes)..." + + gh run watch "$run_id" --repo "$GITHUB_REPO" --exit-status \ + || error "Workflow failed. Check: https://github.com/$GITHUB_REPO/actions/runs/$run_id" + + log "Workflow completed successfully!" + + rm -rf "$WORK_DIR" + mkdir -p "$WORK_DIR" + + log "Downloading artifacts..." + gh run download "$run_id" \ + --repo "$GITHUB_REPO" \ + --dir "$WORK_DIR" + + log "Artifacts downloaded to $WORK_DIR" + ls -la "$WORK_DIR" +} + +resume_download() { + local version="$1" + local tag="v$version" + + log "Finding completed workflow run for tag $tag..." + + check_command gh "brew install gh && gh auth login" + + local run_id="" + run_id=$(gh run list \ + --repo "$GITHUB_REPO" \ + --workflow release.yml \ + --limit 10 \ + --json databaseId,headBranch,status \ + --jq ".[] | select(.headBranch == \"$tag\") | .databaseId" \ + | head -1) + + [[ -z "$run_id" ]] && error "Could not find workflow run for tag $tag" + + local conclusion + conclusion=$(gh run view "$run_id" --repo "$GITHUB_REPO" --json conclusion --jq '.conclusion') + if [[ "$conclusion" != "success" ]]; then + error "Workflow run $run_id has conclusion '$conclusion' (expected 'success'). Check: https://github.com/$GITHUB_REPO/actions/runs/$run_id" + fi + + log "Found completed workflow run: $run_id" + + rm -rf "$WORK_DIR" + mkdir -p "$WORK_DIR" + + log "Downloading artifacts..." + gh run download "$run_id" \ + --repo "$GITHUB_REPO" \ + --dir "$WORK_DIR" + + log "Artifacts downloaded to $WORK_DIR" + ls -la "$WORK_DIR" +} + +# ============================================================================= +# Sign macOS App Bundles +# ============================================================================= + +sign_macos_app() { + local app_path="$1" + local arch_label="$2" + + log "Signing macOS app ($arch_label): $app_path" + + [[ -d "$app_path" ]] || error "macOS app not found at $app_path" + + # Verify signing identity is available + security find-identity -v -p codesigning | grep -q "$MACOS_IDENTITY" \ + || error "Signing identity not found: $MACOS_IDENTITY" + + # Sign with hardened runtime + codesign --force --deep --sign "$MACOS_IDENTITY" \ + --options runtime \ + --timestamp \ + "$app_path" + + # Verify + codesign --verify --deep --strict --verbose=2 "$app_path" \ + || error "Signature verification failed for $app_path" + + log "macOS signing complete ($arch_label)" +} + +sign_macos() { + log "Starting macOS code signing..." + + # Find and sign both arch builds + local aarch64_app + aarch64_app=$(find "$WORK_DIR/standalone-mac-aarch64" -name "*.app" -type d | head -1) + local x86_64_app + x86_64_app=$(find "$WORK_DIR/standalone-mac-x86_64" -name "*.app" -type d | head -1) + + [[ -n "$aarch64_app" ]] && sign_macos_app "$aarch64_app" "aarch64" + [[ -n "$x86_64_app" ]] && sign_macos_app "$x86_64_app" "x86_64" + + log "All macOS signing complete" +} + +# ============================================================================= +# Notarize macOS Apps +# ============================================================================= + +notarize_macos_app() { + local app_path="$1" + local arch_label="$2" + + log "Notarizing macOS app ($arch_label)..." + + local zip_path="$WORK_DIR/notarize-${arch_label}.zip" + + ditto -c -k --keepParent "$app_path" "$zip_path" + + xcrun notarytool submit "$zip_path" \ + --apple-id "$APPLE_ID" \ + --team-id "$MACOS_TEAM_ID" \ + --password "$APPLE_SIGN_PASS" \ + --wait \ + --timeout 30m + + rm -f "$zip_path" + + xcrun stapler staple "$app_path" + xcrun stapler validate "$app_path" \ + || warn "Stapler validation warning for $arch_label (may still work)" + + log "Notarization complete ($arch_label)" +} + +notarize_macos() { + log "Starting macOS notarization..." + + check_command xcrun "xcode-select --install" + prompt_secret APPLE_SIGN_PASS "Enter Apple ID password (or app-specific password)" + + local aarch64_app + aarch64_app=$(find "$WORK_DIR/standalone-mac-aarch64" -name "*.app" -type d | head -1) + local x86_64_app + x86_64_app=$(find "$WORK_DIR/standalone-mac-x86_64" -name "*.app" -type d | head -1) + + [[ -n "$aarch64_app" ]] && notarize_macos_app "$aarch64_app" "aarch64" + [[ -n "$x86_64_app" ]] && notarize_macos_app "$x86_64_app" "x86_64" + + # Re-package signed+notarized apps into .dmg and .tar.gz + for arch in aarch64 x86_64; do + local app + app=$(find "$WORK_DIR/standalone-mac-${arch}" -name "*.app" -type d | head -1) + [[ -z "$app" ]] && continue + + local app_name + app_name=$(basename "$app") + + if [[ "$arch" == "aarch64" ]]; then + local dmg_name="$FNAME_MAC_ARM_DMG" + local tar_name="$FNAME_MAC_ARM_UPDATE" + else + local dmg_name="$FNAME_MAC_INTEL_DMG" + local tar_name="$FNAME_MAC_INTEL_UPDATE" + fi + + log "Creating $dmg_name..." + hdiutil create -volname "MouseTerm" -srcfolder "$app" \ + -ov -format UDZO "$WORK_DIR/$dmg_name" + + log "Creating $tar_name..." + tar -czf "$WORK_DIR/$tar_name" -C "$(dirname "$app")" "$app_name" + done + + log "All macOS notarization and packaging complete" +} + +# ============================================================================= +# Sign Windows Executable +# ============================================================================= + +sign_windows() { + log "Starting Windows code signing..." + + check_command jsign "brew install jsign" + prompt_secret EV_SIGN_PIN "Enter PIV PIN for Windows signing" + + # Find the inner exe + local exe_path + exe_path=$(find "$WORK_DIR/standalone-win-x64" -name "MouseTerm.exe" -not -name "*setup*" -not -name "*install*" | head -1) + [[ -n "$exe_path" ]] || error "Windows executable not found" + + log "Signing inner executable: $exe_path" + jsign \ + --storetype PIV \ + --storepass "$EV_SIGN_PIN" \ + --alias "$JSIGN_ALIAS" \ + --tsaurl "$TSA_URL" \ + --tsmode RFC3161 \ + "$exe_path" + + # Find the NSIS installer + local installer_path + installer_path=$(find "$WORK_DIR/standalone-win-x64" -name "*setup*.exe" -o -name "*install*.exe" | head -1) + + if [[ -n "$installer_path" ]]; then + # TODO: Rebuild NSIS installer with signed exe, then sign the installer + log "Signing installer: $installer_path" + jsign \ + --storetype PIV \ + --storepass "$EV_SIGN_PIN" \ + --alias "$JSIGN_ALIAS" \ + --tsaurl "$TSA_URL" \ + --tsmode RFC3161 \ + "$installer_path" + + # Copy with stable filename + cp "$installer_path" "$WORK_DIR/$FNAME_WIN_EXE" + fi + + log "Windows signing complete" +} + +# ============================================================================= +# Sign Update Bundles (Tauri Layer) +# ============================================================================= + +sign_updates() { + local version="$1" + + log "Signing update bundles with Tauri key..." + + prompt_secret TAURI_SIGNING_PRIVATE_KEY "Enter Tauri signing private key" + + local release_dir="$WORK_DIR/release-assets" + mkdir -p "$release_dir" + + # Collect and rename update bundles with stable filenames + # macOS .tar.gz (already created by notarize step) + [[ -f "$WORK_DIR/$FNAME_MAC_ARM_UPDATE" ]] && cp "$WORK_DIR/$FNAME_MAC_ARM_UPDATE" "$release_dir/" + [[ -f "$WORK_DIR/$FNAME_MAC_INTEL_UPDATE" ]] && cp "$WORK_DIR/$FNAME_MAC_INTEL_UPDATE" "$release_dir/" + [[ -f "$WORK_DIR/$FNAME_MAC_ARM_DMG" ]] && cp "$WORK_DIR/$FNAME_MAC_ARM_DMG" "$release_dir/" + [[ -f "$WORK_DIR/$FNAME_MAC_INTEL_DMG" ]] && cp "$WORK_DIR/$FNAME_MAC_INTEL_DMG" "$release_dir/" + + # Windows NSIS zip + local win_nsis + win_nsis=$(find "$WORK_DIR/standalone-win-x64" -name "*.nsis.zip" | head -1) + [[ -n "$win_nsis" ]] && cp "$win_nsis" "$release_dir/$FNAME_WIN_UPDATE" + + # Windows installer + [[ -f "$WORK_DIR/$FNAME_WIN_EXE" ]] && cp "$WORK_DIR/$FNAME_WIN_EXE" "$release_dir/" + + # Linux AppImage + local linux_appimage + linux_appimage=$(find "$WORK_DIR/standalone-linux-x64" -name "*.AppImage" -not -name "*.tar.gz" | head -1) + [[ -n "$linux_appimage" ]] && cp "$linux_appimage" "$release_dir/$FNAME_LINUX_APPIMAGE" + + local linux_update + linux_update=$(find "$WORK_DIR/standalone-linux-x64" -name "*.AppImage.tar.gz" | head -1) + [[ -n "$linux_update" ]] && cp "$linux_update" "$release_dir/$FNAME_LINUX_UPDATE" + + local linux_deb + linux_deb=$(find "$WORK_DIR/standalone-linux-x64" -name "*.deb" | head -1) + [[ -n "$linux_deb" ]] && cp "$linux_deb" "$release_dir/$FNAME_LINUX_DEB" + + # Generate .sig files for update bundles using Tauri CLI + for bundle in "$release_dir/$FNAME_MAC_ARM_UPDATE" \ + "$release_dir/$FNAME_MAC_INTEL_UPDATE" \ + "$release_dir/$FNAME_WIN_UPDATE" \ + "$release_dir/$FNAME_LINUX_UPDATE"; do + if [[ -f "$bundle" ]]; then + log "Tauri-signing: $(basename "$bundle")" + # Use tauri signer to sign the bundle + TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" \ + TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \ + npx --prefix "$REPO_ROOT/standalone" tauri signer sign \ + --private-key "$TAURI_SIGNING_PRIVATE_KEY" \ + "$bundle" + fi + done + + # Build latest.json manifest + local base_url="https://github.com/$GITHUB_REPO/releases/download/v$version" + local pub_date + pub_date=$(date -u '+%Y-%m-%dT%H:%M:%SZ') + + # Read .sig file contents + local sig_mac_arm="" sig_mac_intel="" sig_win="" sig_linux="" + [[ -f "$release_dir/$FNAME_MAC_ARM_UPDATE.sig" ]] && sig_mac_arm=$(cat "$release_dir/$FNAME_MAC_ARM_UPDATE.sig") + [[ -f "$release_dir/$FNAME_MAC_INTEL_UPDATE.sig" ]] && sig_mac_intel=$(cat "$release_dir/$FNAME_MAC_INTEL_UPDATE.sig") + [[ -f "$release_dir/$FNAME_WIN_UPDATE.sig" ]] && sig_win=$(cat "$release_dir/$FNAME_WIN_UPDATE.sig") + [[ -f "$release_dir/$FNAME_LINUX_UPDATE.sig" ]] && sig_linux=$(cat "$release_dir/$FNAME_LINUX_UPDATE.sig") + + cat > "$release_dir/$FNAME_MANIFEST" < "$notes_file" + fi + + if [[ ! -s "$notes_file" ]]; then + echo "Release $tag" > "$notes_file" + fi + + # Create the release + gh release create "$tag" \ + --repo "$GITHUB_REPO" \ + --title "$tag" \ + --notes-file "$notes_file" \ + "$release_dir"/* + + rm -f "$notes_file" + + log "GitHub Release created: https://github.com/$GITHUB_REPO/releases/tag/$tag" +} + +# ============================================================================= +# Main Entry Point +# ============================================================================= + +usage() { + cat <" + + check_git_clean + download_artifacts "$version" + sign_macos + notarize_macos + sign_windows + sign_updates "$version" + create_release "$version" + ;; + resume) + local version="${2:-}" + [[ -z "$version" ]] && error "Usage: $(basename "$0") resume " + + resume_download "$version" + sign_macos + notarize_macos + sign_windows + sign_updates "$version" + create_release "$version" + ;; + sign-mac) + sign_macos + ;; + notarize) + notarize_macos + ;; + sign-win) + sign_windows + ;; + sign-updates) + local version="${2:-}" + [[ -z "$version" ]] && error "Usage: $(basename "$0") sign-updates " + sign_updates "$version" + ;; + release) + local version="${2:-}" + [[ -z "$version" ]] && error "Usage: $(basename "$0") release " + create_release "$version" + ;; + *) + error "Unknown command: $cmd. Use --help for usage." + ;; + esac + + log "Done!" +} + +main "$@" diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index 1fb403f7..32a066ba 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -27,6 +27,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "externalBin": [ "binaries/node" ], @@ -40,5 +41,13 @@ "resources": [ "../sidecar/**/*" ] + }, + "plugins": { + "updater": { + "pubkey": "", + "endpoints": [ + "https://github.com/diffplug/mouseterm/releases/latest/download/latest.json" + ] + } } } diff --git a/vscode-ext/CHANGELOG.md b/vscode-ext/CHANGELOG.md index ca7d4777..7ed368f8 100644 --- a/vscode-ext/CHANGELOG.md +++ b/vscode-ext/CHANGELOG.md @@ -2,8 +2,4 @@ ## 0.1.0 -- Initial release -- Multiple terminal panes with dockview tiling layout -- Completion detection (submerged/rising/floating states) -- Keyboard-driven navigation and terminal management -- Spatial navigation with back-navigation support +- Initial release to test publishing. \ No newline at end of file From ff798a6dde21aac5b9517220ef5ebd0de6fae6ff Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 25 Mar 2026 17:42:02 -0700 Subject: [PATCH 03/11] Put the update json on our website. --- docs/specs/deploy.md | 14 +++++++------- scripts/sign-and-deploy.sh | 6 ++++++ standalone/src-tauri/tauri.conf.json | 2 +- website/public/standalone-latest.json | 6 ++++++ 4 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 website/public/standalone-latest.json diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 414d88ee..4b8f5c24 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -209,16 +209,16 @@ In `standalone/src-tauri/tauri.conf.json`: "updater": { "pubkey": "", "endpoints": [ - "https://github.com/diffplug/mouseterm/releases/latest/download/latest.json" + "https://mouseterm.com/standalone-latest.json" ] } } } ``` -### Update manifest (`latest.json`) +### Update manifest (`standalone-latest.json`) -Generated by the local script after signing, uploaded as a GitHub Release asset: +Generated by the local script after signing. The script copies 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. The manifest is also uploaded to the GitHub Release as a backup. ```json { @@ -246,7 +246,7 @@ Generated by the local script after signing, uploaded as a GitHub Release asset: } ``` -Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) but the *filenames* are stable. The updater endpoint itself (`/latest/download/latest.json`) always resolves to the most recent release's manifest. +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. ## Release checklist @@ -265,15 +265,15 @@ Human-driven steps, in order: - Download unsigned CI artifacts - Sign macOS (will prompt for `APPLE_SIGN_PASS` if not set) - Sign Windows (will prompt for `EV_SIGN_PIN` if not set) - - Generate Tauri update manifest + - Generate Tauri update manifest and copy to `website/public/standalone-latest.json` - Create the GitHub Release with all signed assets -8. **Verify the release** +8. **Deploy website** — commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live. +9. **Verify the release** - Check GitHub Release assets are correct - On a Mac: download the `.dmg`, open it, confirm no Gatekeeper warnings - On Windows: download the `.exe` installer, 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 -9. **Deploy website** — if the release includes website changes, deploy mouseterm.com. ## Changelog diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index ff311806..2c396add 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -446,6 +446,12 @@ sign_updates() { EOF log "Update manifest written to $release_dir/$FNAME_MANIFEST" + + # Copy manifest to website for serving via mouseterm.com + local website_manifest="$REPO_ROOT/website/public/standalone-latest.json" + cp "$release_dir/$FNAME_MANIFEST" "$website_manifest" + log "Manifest copied to $website_manifest — commit and deploy website to make it live" + log "Update bundle signing complete" } diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index 32a066ba..6e1e1365 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -46,7 +46,7 @@ "updater": { "pubkey": "", "endpoints": [ - "https://github.com/diffplug/mouseterm/releases/latest/download/latest.json" + "https://mouseterm.com/standalone-latest.json" ] } } diff --git a/website/public/standalone-latest.json b/website/public/standalone-latest.json new file mode 100644 index 00000000..984cf0f4 --- /dev/null +++ b/website/public/standalone-latest.json @@ -0,0 +1,6 @@ +{ + "version": "0.0.0", + "notes": "Placeholder — replaced by sign-and-deploy.sh on each release", + "pub_date": "2026-01-01T00:00:00Z", + "platforms": {} +} From acf2b9e73f2578dd1fe2e8d70270b4feafe9f1a4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 26 Mar 2026 16:00:33 -0700 Subject: [PATCH 04/11] Put the public key in. --- standalone/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index 6e1e1365..ca5c1549 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -44,7 +44,7 @@ }, "plugins": { "updater": { - "pubkey": "", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNUIwMDJFRUU0RkZENwpSV1RYLytUdUFyQTFEbkhRa0FnR3lQc3p1VHgvdEhYZWhzNzRhWUN0Z3FWMEZjaE43c3ZidVQ2Ngo=", "endpoints": [ "https://mouseterm.com/standalone-latest.json" ] From 336c300eacf78bf15b3de7479413c2d14ad7e183 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 11:33:01 -0700 Subject: [PATCH 05/11] Add unified CHANGELOG.md and update Tauri signing key. - Create root CHANGELOG.md as single source of truth - Symlink vscode-ext/CHANGELOG.md to root - Regenerate Tauri updater signing keypair Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 9 +++++++++ standalone/src-tauri/tauri.conf.json | 2 +- vscode-ext/CHANGELOG.md | 6 +----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md mode change 100644 => 120000 vscode-ext/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..fbc9986d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/). + +## [0.1.0] - 2026-04-09 + +- Initial release to test publishing. diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index ca5c1549..58d92937 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -44,7 +44,7 @@ }, "plugins": { "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNUIwMDJFRUU0RkZENwpSV1RYLytUdUFyQTFEbkhRa0FnR3lQc3p1VHgvdEhYZWhzNzRhWUN0Z3FWMEZjaE43c3ZidVQ2Ngo=", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFDNUE3RThENTQxQTY0REIKUldUYlpCcFVqWDVhckxRQjBFbGw4anhJMUZ5L2VEU0pGNTluS1hPR0F1OGc1T3BUYTVjbHd0WG0K", "endpoints": [ "https://mouseterm.com/standalone-latest.json" ] diff --git a/vscode-ext/CHANGELOG.md b/vscode-ext/CHANGELOG.md deleted file mode 100644 index 7ed368f8..00000000 --- a/vscode-ext/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Changelog - -## 0.1.0 - -- Initial release to test publishing. \ No newline at end of file diff --git a/vscode-ext/CHANGELOG.md b/vscode-ext/CHANGELOG.md new file mode 120000 index 00000000..04c99a55 --- /dev/null +++ b/vscode-ext/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file From 7ad2fa22b6859f292b68abb20955fb0e62dd23a0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 18:47:18 +0000 Subject: [PATCH 06/11] Claude Code review R4: fix deploy spec platform description to match actual CI matrix --- docs/specs/deploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 4b8f5c24..978aef7d 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -45,7 +45,7 @@ Triggered by tag push `v*`. Three parallel jobs: ### Job: `build-standalone` (matrix) -Runs on `ubuntu-latest` (win + linux) and `macos-latest` (mac). Uses `tauri-apps/tauri-action@v0`. +Runs on `ubuntu-22.04` (linux), `macos-latest` (mac), and `windows-latest` (win). Uses `tauri-apps/tauri-action@v0`. ```yaml strategy: From 8aa009023b638bb6a4a6d8748d4689ac19ba2bce Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 18:54:13 +0000 Subject: [PATCH 07/11] Codex review R4: fix sign-and-deploy.sh issues --- scripts/sign-and-deploy.sh | 67 +++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index 2c396add..03a93c5e 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -114,10 +114,10 @@ download_artifacts() { run_id=$(gh run list \ --repo "$GITHUB_REPO" \ --workflow release.yml \ - --limit 10 \ - --json databaseId,headBranch,status \ - --jq ".[] | select(.headBranch == \"$tag\") | .databaseId" \ - | head -1) + --branch "$tag" \ + --limit 5 \ + --json databaseId \ + --jq ".[0].databaseId // empty") if [[ -z "$run_id" ]]; then attempts=$((attempts + 1)) @@ -160,10 +160,10 @@ resume_download() { run_id=$(gh run list \ --repo "$GITHUB_REPO" \ --workflow release.yml \ - --limit 10 \ - --json databaseId,headBranch,status \ - --jq ".[] | select(.headBranch == \"$tag\") | .databaseId" \ - | head -1) + --branch "$tag" \ + --limit 5 \ + --json databaseId \ + --jq ".[0].databaseId // empty") [[ -z "$run_id" ]] && error "Could not find workflow run for tag $tag" @@ -370,10 +370,33 @@ sign_updates() { [[ -f "$WORK_DIR/$FNAME_MAC_ARM_DMG" ]] && cp "$WORK_DIR/$FNAME_MAC_ARM_DMG" "$release_dir/" [[ -f "$WORK_DIR/$FNAME_MAC_INTEL_DMG" ]] && cp "$WORK_DIR/$FNAME_MAC_INTEL_DMG" "$release_dir/" - # Windows NSIS zip + # Windows NSIS zip — rebuild with signed exe so Tauri auto-update gets the signed binary local win_nsis win_nsis=$(find "$WORK_DIR/standalone-win-x64" -name "*.nsis.zip" | head -1) - [[ -n "$win_nsis" ]] && cp "$win_nsis" "$release_dir/$FNAME_WIN_UPDATE" + if [[ -n "$win_nsis" ]]; then + local signed_exe + signed_exe=$(find "$WORK_DIR/standalone-win-x64" -name "MouseTerm.exe" -not -name "*setup*" -not -name "*install*" | head -1) + if [[ -n "$signed_exe" ]]; then + log "Rebuilding NSIS zip with signed executable..." + local nsis_tmp="$WORK_DIR/nsis-repack" + mkdir -p "$nsis_tmp" + unzip -o "$win_nsis" -d "$nsis_tmp" + # Replace the unsigned exe inside the extracted zip with the signed one + local inner_exe + inner_exe=$(find "$nsis_tmp" -name "MouseTerm.exe" -not -name "*setup*" -not -name "*install*" | head -1) + if [[ -n "$inner_exe" ]]; then + cp "$signed_exe" "$inner_exe" + # Rebuild the zip + (cd "$nsis_tmp" && zip -r "$release_dir/$FNAME_WIN_UPDATE" .) + else + warn "Could not find exe inside NSIS zip; copying original" + cp "$win_nsis" "$release_dir/$FNAME_WIN_UPDATE" + fi + rm -rf "$nsis_tmp" + else + cp "$win_nsis" "$release_dir/$FNAME_WIN_UPDATE" + fi + fi # Windows installer [[ -f "$WORK_DIR/$FNAME_WIN_EXE" ]] && cp "$WORK_DIR/$FNAME_WIN_EXE" "$release_dir/" @@ -481,12 +504,24 @@ create_release() { echo "Release $tag" > "$notes_file" fi - # Create the release - gh release create "$tag" \ - --repo "$GITHUB_REPO" \ - --title "$tag" \ - --notes-file "$notes_file" \ - "$release_dir"/* + # Create or update the release + if gh release view "$tag" --repo "$GITHUB_REPO" &>/dev/null; then + log "Release $tag already exists — updating assets..." + gh release upload "$tag" \ + --repo "$GITHUB_REPO" \ + --clobber \ + "$release_dir"/* + gh release edit "$tag" \ + --repo "$GITHUB_REPO" \ + --title "$tag" \ + --notes-file "$notes_file" + else + gh release create "$tag" \ + --repo "$GITHUB_REPO" \ + --title "$tag" \ + --notes-file "$notes_file" \ + "$release_dir"/* + fi rm -f "$notes_file" From 23d17b9567111dd5c1e5502aae6039e506688ab7 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 18:58:52 +0000 Subject: [PATCH 08/11] Claude Code review R5: fix BSD head compatibility, remove dead variable, fix spec matrix key --- docs/specs/deploy.md | 8 ++++---- scripts/sign-and-deploy.sh | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 978aef7d..3361dafc 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -52,13 +52,13 @@ strategy: matrix: include: - platform: ubuntu-22.04 - rust-targets: x86_64-unknown-linux-gnu + target: x86_64-unknown-linux-gnu - platform: macos-latest - rust-targets: aarch64-apple-darwin + target: aarch64-apple-darwin - platform: macos-latest - rust-targets: x86_64-apple-darwin + target: x86_64-apple-darwin - platform: windows-latest - rust-targets: x86_64-pc-windows-msvc + target: x86_64-pc-windows-msvc ``` Each matrix leg: diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index 03a93c5e..325ff66f 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -76,8 +76,6 @@ check_git_clean() { error "Untracked files detected. Commit or remove them before deploying." fi - local branch - branch=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) local upstream upstream=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null) || true @@ -497,7 +495,8 @@ create_release() { local notes_file="$WORK_DIR/release-notes.md" if [[ -f "$REPO_ROOT/CHANGELOG.md" ]]; then # Extract section between [X.Y.Z] and the next ## heading - sed -n "/^## \[$version\]/,/^## \[/p" "$REPO_ROOT/CHANGELOG.md" | head -n -1 > "$notes_file" + # Use sed to drop the trailing heading line (macOS BSD head lacks -n -1) + sed -n "/^## \[$version\]/,/^## \[/p" "$REPO_ROOT/CHANGELOG.md" | sed '$d' > "$notes_file" fi if [[ ! -s "$notes_file" ]]; then From 61365057729bcbde65bf9a3b7a60586965fbd51f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 19:32:09 +0000 Subject: [PATCH 09/11] Codex review R5: fix release CI setup and wire the Tauri updater plugin --- .github/workflows/release.yml | 25 ++++++++--------- docs/specs/deploy.md | 22 ++++++++++----- scripts/sign-and-deploy.sh | 50 ++++++++++++++++++++++----------- standalone/src-tauri/Cargo.toml | 1 + standalone/src-tauri/src/lib.rs | 1 + 5 files changed, 63 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fdc8da2..ef5a0476 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,9 @@ jobs: with: version: 10 + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} @@ -50,10 +53,6 @@ jobs: sudo apt-get update -qq sudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - name: Install frontend dependencies - run: pnpm install - working-directory: standalone - - name: Build Tauri app uses: tauri-apps/tauri-action@v0 env: @@ -95,17 +94,17 @@ jobs: with: version: 10 - - name: Install and test lib - run: cd lib && pnpm install && pnpm test + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile - - name: Build frontend for VSCode - run: cd vscode-ext && pnpm build:frontend + - name: Test lib + run: pnpm --filter mouseterm-lib test - - name: Install extension dependencies - run: cd vscode-ext && pnpm install + - name: Build frontend for VSCode + run: pnpm --filter mouseterm build:frontend - name: Build extension - run: cd vscode-ext && pnpm build + run: pnpm --filter mouseterm build - name: Package extension run: cd vscode-ext && npx vsce package --no-dependencies @@ -131,8 +130,8 @@ jobs: with: version: 10 - - name: Install vsce and ovsx - run: cd vscode-ext && pnpm install + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile - name: Download .vsix uses: actions/download-artifact@v4 diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 3361dafc..d6bc7068 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -63,8 +63,8 @@ strategy: Each matrix leg: 1. Checkout, setup Node 22, pnpm 10, Rust stable -2. Install system deps (Linux: libgtk, libwebkit, etc.) -3. `pnpm install` in `standalone/` +2. Install workspace dependencies once from the repo root with `pnpm install --frozen-lockfile` +3. Install system deps (Linux: libgtk, libwebkit, etc.) 4. Build via `tauri-action` — but **skip signing** (no `APPLE_SIGNING_IDENTITY`, no `TAURI_SIGNING_PRIVATE_KEY`) 5. Upload artifacts (installers + bundles) via `actions/upload-artifact` @@ -74,10 +74,11 @@ Each matrix leg: Runs on `ubuntu-latest`: 1. Checkout, setup Node 22, pnpm 10 -2. `cd lib && pnpm install && pnpm test` -3. `cd vscode-ext && pnpm build:frontend && pnpm install && pnpm build` -4. `npx vsce package --no-dependencies` -5. Upload `.vsix` as artifact +2. `pnpm install --frozen-lockfile` at the repo root +3. `pnpm --filter mouseterm-lib test` +4. `pnpm --filter mouseterm build:frontend && pnpm --filter mouseterm build` +5. `npx vsce package --no-dependencies` +6. Upload `.vsix` as artifact ### Job: `publish-vscode` @@ -216,6 +217,14 @@ In `standalone/src-tauri/tauri.conf.json`: } ``` +And in the Rust app bootstrap (`standalone/src-tauri/src/lib.rs`), the updater plugin is registered with: + +```rust +.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. + ### Update manifest (`standalone-latest.json`) Generated by the local script after signing. The script copies 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. The manifest is also uploaded to the GitHub Release as a backup. @@ -293,4 +302,3 @@ A single `CHANGELOG.md` at the repo root, following [Keep a Changelog](https://k | `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 | - diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index 325ff66f..cbeab373 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -92,6 +92,32 @@ check_git_clean() { log "Git status clean." } +resolve_tag_sha() { + local tag="$1" + local tag_sha + + tag_sha=$(git -C "$REPO_ROOT" rev-list -n 1 "$tag^{commit}" 2>/dev/null) \ + || error "Tag $tag not found locally. Fetch tags or create it first." + + [[ -n "$tag_sha" ]] || error "Could not resolve commit for tag $tag" + printf '%s\n' "$tag_sha" +} + +find_release_run_id() { + local tag="$1" + local tag_sha="$2" + + gh run list \ + --repo "$GITHUB_REPO" \ + --workflow release.yml \ + --event push \ + --commit "$tag_sha" \ + --limit 5 \ + --json databaseId,displayTitle,headSha \ + --jq ".[] | select(.displayTitle == \"$tag\" or .headSha == \"$tag_sha\") | .databaseId" \ + | head -1 +} + # ============================================================================= # Download CI Artifacts # ============================================================================= @@ -99,8 +125,10 @@ check_git_clean() { download_artifacts() { local version="$1" local tag="v$version" + local tag_sha + tag_sha=$(resolve_tag_sha "$tag") - log "Finding workflow run for tag $tag..." + log "Finding workflow run for tag $tag ($tag_sha)..." check_command gh "brew install gh && gh auth login" @@ -109,13 +137,7 @@ download_artifacts() { local max_attempts=60 # 5 minutes of retries while [[ -z "$run_id" ]] && [[ $attempts -lt $max_attempts ]]; do - run_id=$(gh run list \ - --repo "$GITHUB_REPO" \ - --workflow release.yml \ - --branch "$tag" \ - --limit 5 \ - --json databaseId \ - --jq ".[0].databaseId // empty") + run_id=$(find_release_run_id "$tag" "$tag_sha") if [[ -z "$run_id" ]]; then attempts=$((attempts + 1)) @@ -149,19 +171,15 @@ download_artifacts() { resume_download() { local version="$1" local tag="v$version" + local tag_sha + tag_sha=$(resolve_tag_sha "$tag") - log "Finding completed workflow run for tag $tag..." + log "Finding completed workflow run for tag $tag ($tag_sha)..." check_command gh "brew install gh && gh auth login" local run_id="" - run_id=$(gh run list \ - --repo "$GITHUB_REPO" \ - --workflow release.yml \ - --branch "$tag" \ - --limit 5 \ - --json databaseId \ - --jq ".[0].databaseId // empty") + run_id=$(find_release_run_id "$tag" "$tag_sha") [[ -z "$run_id" ]] && error "Could not find workflow run for tag $tag" diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index a47023d9..287d236f 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-shell = "2" +tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 80929599..b127c63f 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -255,6 +255,7 @@ fn start_sidecar(app: &AppHandle) -> SidecarState { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) .setup(|app| { let sidecar_state = start_sidecar(app.handle()); app.manage(sidecar_state); From 528e183377f5f12b65c39515c4ea8840fa4afb98 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 9 Apr 2026 19:56:03 +0000 Subject: [PATCH 10/11] Codex review R5b: gate vscode publish on standalone, rebuild NSIS installer with signed exe, multiline key prompt, clean work dir before git check --- .github/workflows/release.yml | 5 ++- scripts/sign-and-deploy.sh | 75 ++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef5a0476..047805e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,7 @@ jobs: standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.deb standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.nsis.zip standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.nsis.zip.sig + standalone/src-tauri/target/${{ matrix.target }}/release/bundle/nsis/** build-vscode: name: Build VSCode Extension @@ -117,7 +118,9 @@ jobs: publish-vscode: name: Publish VSCode Extension - needs: build-vscode + needs: + - build-standalone + - build-vscode runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index cbeab373..22df0701 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -61,6 +61,34 @@ prompt_secret() { fi } +prompt_secret_multiline() { + local varname="$1" + local prompt="$2" + local sentinel="__EOF_${varname}__" + if [[ -z "${!varname:-}" ]]; then + cat >&2 </dev/null || error "Required command not found: $1. Install with: $2" } @@ -68,6 +96,8 @@ check_command() { check_git_clean() { log "Checking git status..." + rm -rf "$WORK_DIR" + if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then error "Local changes detected. Commit or stash changes before deploying." fi @@ -92,6 +122,47 @@ check_git_clean() { log "Git status clean." } +find_nsis_script() { + find "$WORK_DIR/standalone-win-x64" \ + \( -name "*.nsi" -o -name "*.nsh" \) \ + -print \ + | head -1 +} + +rebuild_windows_installer() { + local signed_exe="$1" + local installer_path="$2" + + check_command makensis "Install NSIS (makensis) and re-download artifacts" + + local script_path + script_path=$(find_nsis_script) + [[ -n "$script_path" ]] || error "NSIS script not found in downloaded artifacts; include bundle/nsis staging files before rebuilding the installer." + + local script_dir + script_dir="$(cd "$(dirname "$script_path")" && pwd)" + local bundle_root + bundle_root="$(cd "$script_dir/.." && pwd)" + + local staged_exe + staged_exe=$(find "$bundle_root" -name "MouseTerm.exe" -not -path "$signed_exe" | head -1) + [[ -n "$staged_exe" ]] || error "Could not find staged MouseTerm.exe for NSIS rebuild" + + cp "$signed_exe" "$staged_exe" + + local installer_name + installer_name="$(basename "$installer_path")" + + rm -f "$installer_path" + log "Rebuilding NSIS installer: $installer_name" + ( + cd "$script_dir" + makensis -NOCD -X"OutFile $installer_name" "$(basename "$script_path")" + ) + + [[ -f "$installer_path" ]] || error "NSIS rebuild did not produce $installer_path" +} + resolve_tag_sha() { local tag="$1" local tag_sha @@ -348,7 +419,7 @@ sign_windows() { installer_path=$(find "$WORK_DIR/standalone-win-x64" -name "*setup*.exe" -o -name "*install*.exe" | head -1) if [[ -n "$installer_path" ]]; then - # TODO: Rebuild NSIS installer with signed exe, then sign the installer + rebuild_windows_installer "$exe_path" "$installer_path" log "Signing installer: $installer_path" jsign \ --storetype PIV \ @@ -374,7 +445,7 @@ sign_updates() { log "Signing update bundles with Tauri key..." - prompt_secret TAURI_SIGNING_PRIVATE_KEY "Enter Tauri signing private key" + prompt_secret_multiline TAURI_SIGNING_PRIVATE_KEY "Enter Tauri signing private key" local release_dir="$WORK_DIR/release-assets" mkdir -p "$release_dir" From 7f42d4da5f74d4c9a6e6cd79c272d38d49cda583 Mon Sep 17 00:00:00 2001 From: nedtwigg Date: Thu, 9 Apr 2026 21:28:06 +0000 Subject: [PATCH 11/11] Claude Code review R6: add release-signed/ to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fd6dd3b9..90183994 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ vscode-ext/media/ vscode-ext/node_modules/ vscode-ext/*.vsix +# Release signing work directory (created by scripts/sign-and-deploy.sh) +release-signed/ + # Tauri / Standalone standalone/src-tauri/target/ standalone/src-tauri/binaries/