Add an installer and a tag-driven release pipeline - #5
Merged
Conversation
Inno Setup installer, built and published by a Release workflow. Upgrades and removing the old version: the .iss carries a fixed AppId, which is the identity Windows and Inno use to recognise an existing installation. A newer build therefore upgrades the existing one in place - old files are replaced, the Add/Remove Programs entry is updated rather than duplicated, and the previous uninstaller is superseded. AppMutex names the single-instance mutex the app now holds, so Setup detects a running VerseLink and asks the user to close it rather than failing part way through on a locked exe. Per-user install to %LOCALAPPDATA%\Programs\VerseLink: no UAC prompt, and the install directory stays writable, which matters because the app runs AsInvoker. Deliberately not shipped: config.json. Settings live in %APPDATA%\VerseLink and are created with defaults on first run. A config.json next to the exe would be picked up by the legacy-config migration and could overwrite what an upgrading user actually had. The same reasoning applies to the portable zip. Uninstall keeps settings unless the user says otherwise, and removes the log. Release pipeline: - A v* tag builds, self-tests, packages and publishes. workflow_dispatch runs the same pipeline without publishing, so packaging can be exercised without creating a release that then has to be deleted. - The tag must match Version.h, checked before anything is built, so a release cannot advertise a version its binary does not report. build-installer.ps1 enforces the same thing for local runs, and additionally checks the built exe's own VERSIONINFO. - Packaging is gated on --selftest passing; a build that cannot pass its own checks is never packaged. - Publishing uses gh rather than a third-party action, consistent with the Node 20 cleanup. Also: the zip is named by version rather than by date so it pairs with the installer, and both now share packaging/DISTRIBUTION-NOTES.txt instead of the script generating its own copy.
workflow_dispatch only works from the default branch, so a packaging change could not be exercised before merging. A scoped pull_request trigger builds the installer and zip whenever packaging, the release workflow or Version.h change. Publishing stays gated on a tag, so these runs only produce artifacts.
Reworked to match how this should actually behave:
- The install location is chosen by the user. The directory page is always
shown, and elevation can be picked from the first dialog, so C:\VerseLink or
Program Files work as well as the per-user default. The app does not need a
writable install directory - its settings live in %APPDATA%.
- An existing version is no longer removed. Before the new executable is
copied in, the old one is moved to
{app}\previous-versions\VerseLinkWindows-<version>.exe, named by the version
it reports so successive updates do not collide. Rolling back is copying that
file back over VerseLinkWindows.exe.
- Settings are untouched, as before: they live in %APPDATA%\VerseLink, which
the installer neither writes nor removes, and no config.json is shipped.
- A running VerseLink is closed before the update and relaunched afterwards, so
an update does not leave the user without their hotkey. Whether it was running
is recorded before Setup prompts, and the post-install "Launch VerseLink"
checkbox is suppressed in that case so it cannot start a second copy.
For the close to be clean, the app now handles WM_CLOSE, WM_QUERYENDSESSION and
WM_ENDSESSION. WM_CLOSE previously fell through to DefWindowProc, which
destroyed the hidden window but left the message loop and worker thread running
- the process stayed alive with no window and no tray icon, and Restart Manager
had nothing to work with. Shutdown now runs the same path as the tray's Quit,
which also makes sign-out and any external close request behave.
Verified: taskkill without /F (a plain WM_CLOSE) now produces
"Shutdown requested by the system or an installer" followed by a full clean
shutdown, where it previously left the process alive.
AppMutex only blocks. It tells the user to close the app and aborts if they do
not, which under /SILENT fails outright:
Defaulting to Cancel for suppressed message box (OK/Cancel):
Please close all instances of it now, then click OK to continue...
Got EAbort exception.
That is the opposite of the intended behaviour, and it blocked any install
while VerseLink was running anywhere - not just an update of the copy being
replaced.
PrepareToInstall now closes it: WM_CLOSE is posted to the app's hidden window,
which the app handles as a clean shutdown, and Setup waits for the
single-instance mutex to clear so the exe is genuinely unlocked before being
replaced. If it cannot be closed within 15 seconds the user gets a clear
message naming the tray icon, rather than a mid-install failure.
CloseApplications stays on as a Restart Manager backstop for any other locked
file. The window class name is now noted in both the .iss and the source, since
the two have to agree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #4, which GitHub auto-closed when its base branch (
feat/installer-prerequisites, merged in #3) was deleted. Same branch, same commits, retargeted atmaster.Inno Setup installer plus a Release workflow that builds and publishes it.
Update behaviour
The installer updates in place and keeps the old version. It does not uninstall or delete anything.
C:\VerseLinkor Program Files work as well as the per-user default. The app doesn't need a writable install dir — settings live in%APPDATA%.{app}\previous-versions\VerseLinkWindows-<version>.exebefore the new exe is copied in. Rolling back is copying that file back.%APPDATA%\VerseLink, which the installer neither writes nor removes, and noconfig.jsonis shipped.Two fixes this required
AppMutexhad to go. It only blocks: it tells the user to close the app and aborts if they don't. Under/SILENTthat fails outright —Defaulting to Cancel for suppressed message box... Got EAbort exception— and it blocked any install while VerseLink was running anywhere, not just an update of the copy being replaced.PrepareToInstallnow closes the app itself and waits for its mutex to clear, with a clear message if it can't within 15s.WM_CLOSEwas broken in the app. It fell through toDefWindowProc, which destroyed the hidden window but left the message loop and worker thread running — the process stayed alive with no window and no tray icon, and Restart Manager had nothing to work with. It now runs the same shutdown path as the tray's Quit, which also fixes sign-out behaviour.Release pipeline
v*tag builds, self-tests, packages and publishes.workflow_dispatchruns the same pipeline without publishing.Version.h, checked before anything is built, so a release can never advertise a version its binary doesn't report.--selftestpassing.pull_requesttrigger builds packaging on PRs that touch it — that's what caught theAppMutexfailure.Verified end to end
Against the installer CI produced, on a real machine:
Honest limitation
A copy that was never installed (an unzipped folder) is invisible to this installer — nothing recorded it, so it's neither updated nor removed. Settings still migrate on first run, but the old folder and any Startup shortcut have to go manually. Called out in the release notes and
DISTRIBUTION-NOTES.txt.