Skip to content

feat(desktop): move NSIS installs to Velopack on the next update - #2167

Merged
h4yfans merged 6 commits into
mainfrom
feat/nsis-to-velopack-handoff
Sep 11, 2026
Merged

feat(desktop): move NSIS installs to Velopack on the next update#2167
h4yfans merged 6 commits into
mainfrom
feat/nsis-to-velopack-handoff

Conversation

@h4yfans

@h4yfans h4yfans commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

A Windows user on an NSIS install currently stays on the update path that fails on roughly 8.5% of installs per 60 days. This moves them to the Velopack install automatically, on the next update they take, with no prompt and no manual reinstall.

An NSIS install keeps electron-updater for checking and downloading. The install step is what changes. Once electron-updater has the NSIS setup.exe on disk, installer-handoff.ts looks for MemryNote-win-Setup.exe on the same release tag, streams it into userData/installer-handoff/, and verifies it before anything is executed. onDownloaded is deferred until that settles, so "Restart to install" never appears before everything the hand-off needs exists.

Verification rule. Get-AuthenticodeSignature -LiteralPath <file> | ConvertTo-Json -Compress, parsed by judgeAuthenticode. Status must be Valid (serialized as 0 or the enum name) and SignerCertificate.Subject must contain CN=Open Source Developer Kaan Karaca. Anything else deletes the file and declines. An unverified binary is never spawned, on either the updater path or the CLI path.

The hand-off script. Pinned by installer-handoff-script.test.ts, rendered with CRLF, spawned detached through cmd.exe /c:

@echo off
setlocal
rem MemryNote installer hand-off: wait for the app to exit, remove the NSIS install, install the Velopack build.
set "APP_PID=1234"
set "APP_EXE=Memrynote.exe"
set "INSTALL_DIR=C:\Users\kaan\AppData\Local\Programs\MemryNote"
set "UNINSTALLER=C:\Users\kaan\AppData\Local\Programs\MemryNote\Uninstall MemryNote.exe"
set "WORK_DIR=C:\Users\kaan\AppData\Local\Temp\memry-installer-handoff-1234"
set "SETUP=C:\Users\kaan\AppData\Roaming\memrynote\installer-handoff\MemryNote-win-Setup.exe"
set "SETUP_LOG=C:\Users\kaan\AppData\Roaming\memrynote\logs\velopack-setup.log"
set "NSIS_INSTALLER=C:\Users\kaan\AppData\Local\memrynote-updater\pending\MemryNote-2026.911.1-setup.exe"
set "VELOPACK_EXE=%LocalAppData%\MemryNote\current\Memrynote.exe"

:wait_for_exit
tasklist /FI "PID eq %APP_PID%" /FI "IMAGENAME eq %APP_EXE%" /NH 2>nul | find /I "%APP_EXE%" >nul
if not errorlevel 1 (
  ping -n 2 127.0.0.1 >nul
  goto wait_for_exit
)

if not exist "%WORK_DIR%" mkdir "%WORK_DIR%"
copy /y "%UNINSTALLER%" "%WORK_DIR%\Uninstall MemryNote.exe" >nul
start "" /wait "%WORK_DIR%\Uninstall MemryNote.exe" /S _?=%INSTALL_DIR%

start "" /wait "%SETUP%" --silent --verbose --log "%SETUP_LOG%"

if not exist "%VELOPACK_EXE%" (
  if exist "%NSIS_INSTALLER%" start "" /wait "%NSIS_INSTALLER%" /S --force-run
)

del /q "%SETUP%" >nul 2>&1
rmdir /s /q "%WORK_DIR%" >nul 2>&1
(goto) 2>nul & del "%~f0"

Order matters in four places. It waits for our own PID and image name, so a recycled PID cannot match. ping is the sleep because timeout /t aborts with "Input redirection is not supported" under stdio: 'ignore'. The uninstaller runs from a copy with _?= last and unquoted, so it blocks and RMDir /r $INSTDIR can remove the original, and without --updated, so the tolerant full-removal path runs and the Start-menu shortcut at the path Velopack is about to reuse is gone first. Setup.exe runs only after that, and launches the app itself.

Fallbacks. Every refusal lands on the plain NSIS install, which is exactly today's behavior.

What fails What the user sees
The release carries no MemryNote-win-Setup.exe (HEAD 404) Nothing. Info log only, no error event. The NSIS update installs as usual.
HEAD or the download fails Nothing new. INSTALLER_HANDOFF_DOWNLOAD_FAILED through the existing updater error path, then the NSIS update installs as usual.
The signature does not verify Same. INSTALLER_HANDOFF_UNVERIFIED, the downloaded file is deleted, the NSIS update installs as usual.
The script cannot be spawned launch() returns false, so autoInstallOnAppQuit is left on and electron-updater installs the NSIS update from its own quit hook.
Setup.exe runs but leaves no app The script reinstalls from the NSIS installer electron-updater already downloaded. The user is never left without an app.
Windows itself is shutting down No install at all, hand-off included, which is the existing session-end guard. It applies on the next user-initiated quit.

Next launch. The marker now carries installer (electron-updater, velopack, velopack-handoff; older markers lack the field). resolveUpdateInstallOutcome reads it against the running version and the install layout. Same version is the existing UPDATE_INSTALL_DID_NOT_APPLY, now with source: velopack-handoff. A new version from the Velopack layout is app_update_installed with action: migrated. A new version still on the NSIS layout is INSTALLER_HANDOFF_DID_NOT_APPLY, meaning the update arrived but the migration did not. The silent install-on-quit path writes the marker too, which it never did before, so a Windows update that applies on a normal quit is no longer invisible.

Memrynote.exe --cli migrate-installer <path-to-MemryNote-win-Setup.exe> drives the same verify-then-hand-off path against a local file. Windows only, exit 0 only once the script is spawned.

Stacked on #2164.

Release note

none

Test plan

Run from the worktree on Node 24 (.nvmrc), unless noted.

  • pnpm --filter @memry/desktop typecheck:node — pass.
  • pnpm --filter @memry/desktop typecheck:test — pass. No test file was added to the tsconfig.test.*.json exclude backlog.
  • pnpm --filter @memry/desktop exec vitest run --config config/vitest.config.ts --project main src/main/installer-handoff.test.ts src/main/installer-handoff-script.test.ts src/main/updater-nsis-handoff.test.ts src/main/cli/migrate-installer.test.ts src/main/telemetry/update-install-marker.test.ts src/main/cli/headless.test.ts src/main/updater.test.ts src/main/updater-velopack.test.ts — 134 pass.
  • pnpm --filter @memry/desktop test:main — 8263 pass, 3 fail. The three are apps/desktop/scripts/check-cert-hashes-config.test.ts, which passes on Node 24 and only fails when the suite runs on the Node 26 the native modules happen to be built for here. Unrelated to this diff.
  • pnpm lint — 0 errors. The one warning is pre-existing in vault-switcher.tsx.
  • pnpm check:architecture, pnpm check:contracts — pass.
  • pnpm ipc:check — pass on Node 24. No contract changed. On Node 26 it dies on --experimental-transform-types, which that version removed.
  • pnpm docs:impact --base origin/feat/velopack-windows-updater --strict — pass, docs changed on this branch.
  • pnpm docs:build, git diff --check — pass.

What none of that covers: the batch script, the _?= uninstaller behavior and the Get-AuthenticodeSignature | ConvertTo-Json shape only ever run on Windows, so every test here asserts the rendered text and the parse rules rather than the effect. The migrate job in .github/workflows/velopack-smoke.yml is what proves them, and it needs a published release carrying MemryNote-win-Setup.exe before it can run. It installs the published NSIS build, hands off, then asserts the NSIS directory, its uninstall key and its shortcut are gone, the Velopack install is signed and present, the Start menu entry is back and Memrynote.exe is running from the new install. Its run_cli_migration input stays off until a published NSIS build contains migrate-installer. The hand-off is also unverified end to end against a real update until that release exists.

@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request test labels Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 11e48fc.

@h4yfans
h4yfans changed the base branch from feat/velopack-windows-updater to main September 11, 2026 19:20
Windows in-place NSIS updates fail on roughly 8.5% of installs per 60 days, so
an NSIS install has to move to the Velopack install rather than keep retrying
the path that breaks. This is the half that runs before anything is executed.

detectWindowsInstallLayout probes `..\Update.exe` and `Uninstall MemryNote.exe`
to tell a Velopack install from an NSIS one. createInstallerHandoff downloads
`MemryNote-win-Setup.exe` from the same release tag into
`userData/installer-handoff/`, runs Get-AuthenticodeSignature over it, and arms
only when judgeAuthenticode sees status Valid and a signer subject containing
`CN=Open Source Developer Kaan Karaca`. Any other verdict deletes the file, so
an unverified binary is never spawned. launch() runs at most once per armed
plan: the shutdown backstop can race the shutdown sequence into a second
install call, and two hand-off scripts would uninstall and install twice.

renderInstallerHandoffScript stays free of imports and of non-erasable syntax so
Node can strip its types for the smoke workflow. Its command lines are pinned by
a test because they only ever run on Windows.
The install marker could not tell a Velopack hand-off apart from a plain NSIS
install, so the migration would have been invisible in telemetry and a silent
install-on-quit that never came back left no trace at all.

markUpdateInstallStarted now takes the installer and stores it; markers written
by older builds simply lack the field. resolveUpdateInstallOutcome reads it back
against the current version and the install layout: same version is the existing
UPDATE_INSTALL_DID_NOT_APPLY, a new version from the Velopack layout is
app_update_installed with action `migrated`, and a new version still on the NSIS
layout is INSTALLER_HANDOFF_DID_NOT_APPLY, which means the update arrived but the
migration did not. The errorCode of the existing failure is unchanged so Error
Tracking keeps one issue.
An NSIS install stays on electron-updater for checking and downloading, and
migrates at the install step instead. Both install paths now go through the
hand-off, so the migration reaches a user who never presses Restart.

The backend defers onDownloaded until the hand-off settles, so "Restart to
install" only appears once the Velopack installer is on disk and verified, or
the hand-off has declined. Restart-to-install and the silent install-on-quit
both launch the script and then turn autoInstallOnAppQuit off, which is what
stops electron-updater from also running the NSIS installer from its own quit
hook. will-quit writes the marker before the hand-off, because the installer
runs after this process exits and that write is the last evidence left. A
session-end quit still skips the install entirely, marker included.

Every refusal lands on the plain NSIS install: a release without the asset, a
download failure, a signature failure, and a spawn failure.
`Memrynote.exe --cli migrate-installer <path-to-MemryNote-win-Setup.exe>` drives
the same verify-then-hand-off path against a local file. It is how the Windows
smoke workflow exercises the hand-off from a real NSIS install without waiting
for a release to carry the asset, and it gives a stuck user a manual route.

Windows only. It copies the file into the hand-off directory, refuses unless
Get-AuthenticodeSignature agrees, and exits 0 only once the script is spawned.
The hand-off is a batch script, an NSIS `_?=` uninstall and a PowerShell JSON
shape, none of which run anywhere but Windows. A workflow is the only place they
can be proven.

The smoke workflow moves here from the prototype branch with a second `migrate`
job: install the published NSIS build, hand off, then assert the NSIS directory,
its uninstall key and its shortcut are gone, the Velopack install is signed and
present, the Start menu entry is back, and Memrynote.exe is running from the new
install. scripts/installer-handoff-script.mjs renders the script from the same
function the app calls, so the job cannot drift from the shipped one. The CLI
route is gated off until a published NSIS build carries the command.
The install guide told a Windows user on the old installer that nothing would
change. It now says the move happens once, on the next update, that the vault,
settings and index stay put, that the app reopens itself, and that a taskbar pin
may need pinning again.

Observability gains the hand-off's verification rule, its fallbacks and the three
outcomes the next launch can report.
}
const scriptPath = path.win32.join(tmpDir, `memry-installer-handoff-${pid}.cmd`)
try {
fsDeps.writeFileSync(scriptPath, renderInstallerHandoffScript(plan))
@h4yfans
h4yfans force-pushed the feat/nsis-to-velopack-handoff branch from 7e65197 to 11e48fc Compare September 11, 2026 19:23
@h4yfans
h4yfans marked this pull request as ready for review September 11, 2026 19:23
@h4yfans
h4yfans merged commit 808ed9e into main Sep 11, 2026
8 of 9 checks passed
@h4yfans
h4yfans deleted the feat/nsis-to-velopack-handoff branch September 11, 2026 19:23
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants