Packages the published app (API + built React SPA, served from one process - see the
root README's "Serve React build as static files" note) plus a "ScriptRunner" Windows
Service registration into a single ScriptRunnerSetup.msi.
This installer was authored in a Linux sandbox with no Windows machine or WiX
toolchain available, then iterated against real wix build output on an actual
Windows machine - several real bugs (missing WixToolset.Heat/WixToolset.UI.wixext
package references, a BindPath needed for harvested files, InstallerPlatform for
64-bit components, and a deferred-custom-action property-resolution bug that silently
skipped service registration entirely) were found and fixed this way. Budget time to
verify the specific machine/OS build you're targeting still installs and starts the
service cleanly - paste back whatever WiX, msiexec, or install-service.log reports if
something doesn't behave as documented here, the same way earlier issues got fixed.
| What | Where |
|---|---|
Application files (published API + wwwroot with the built SPA) |
C:\Program Files\ScriptRunner\ |
| Windows Service | Name ScriptRunner, Automatic startup, auto-restart on crash |
Default data directory + bootstrap pointer (bootstrap.json, install log) |
%ProgramData%\ScriptRunner\ - see "First run" below; the actual database location is chosen from the web setup wizard and can be redirected elsewhere |
Secrets (SCRIPTRUNNER_MASTER_KEY, SCRIPTRUNNER_JWT_SIGNING_KEY, ASPNETCORE_URLS) |
HKLM:\SYSTEM\CurrentControlSet\Services\ScriptRunner\Environment (per-service, not machine-wide - see SECURITY.md) |
| Firewall rule | Inbound TCP allow on the configured port (default 8080) |
The installer itself does not prompt for a data directory or create an admin account
- both of those now happen from the web UI after the service starts (the first-run setup
wizard - see the root README's "First run" section). This is deliberate: a native MSI
dialog can't browse the server's filesystem in any useful way once the app might be
managed remotely, so the data-directory picker is a web page backed by a small server-side
API instead (
SetupController/DirectoryBrowser.cs).
The service listens on http://0.0.0.0:8080 by default. Change the port by editing
$Port in scripts/Install-Service.ps1 before building, or by hand-editing the
registry Environment value's ASPNETCORE_URLS entry after install and restarting the
service (the install script preserves whatever's already there on upgrade/repair, so a
manual change survives an MSI upgrade).
-
.NET 8 SDK and Node.js 20+ (same as running the app itself - see the root README's Prerequisites section).
-
WiX v5 CLI + UI extension:
dotnet tool install --global wix --version 5.0.2 wix extension add WixToolset.UI.wixext/5.0.2
(If you already have an older
wixglobal tool installed, usedotnet tool update --global wix --version 5.0.2instead.)
From the repo root:
.\build-installer.ps1This runs, in order: npm install && npm run build in web/, copies web/dist into
src/ScriptRunner.Api/wwwroot, dotnet publish the API to publish/, then
dotnet build this project. The resulting MSI lands at
installer\bin\Release\ScriptRunnerSetup.msi.
Useful flags:
.\build-installer.ps1 -SelfContained- publishes a self-containedwin-x64build (larger MSI, but the target machine doesn't need the .NET 8 runtime preinstalled)..\build-installer.ps1 -SkipFrontendBuild- reuses whatever's already inweb/distinstead of rebuilding it; handy while iterating on the WiX authoring itself.
If you'd rather run the steps by hand (e.g. to isolate which one fails):
cd web; npm install; npm run build; cd ..
Copy-Item web\dist\* src\ScriptRunner.Api\wwwroot\ -Recurse -Force
dotnet publish src\ScriptRunner.Api -c Release -o publish\
dotnet build installer\ScriptRunner.Installer.wixproj -c ReleaseRun the resulting ScriptRunnerSetup.msi elevated (double-click and accept the UAC
prompt, or msiexec /i ScriptRunnerSetup.msi /l*v install.log from an elevated prompt
for verbose logging - the /l*v log is the first thing to check if the install fails or
the custom action reports an error). The installer asks for an install directory
(defaults to C:\Program Files\ScriptRunner) and shows the license in
License.rtf (replace this placeholder with your own org's terms
before distributing the installer to anyone outside your team).
A framework-dependent build (the default) checks for the ASP.NET Core Runtime (x64)
before copying any files, and stops with a clear message + download link if it's not
found, rather than installing successfully and then failing to start the service later
with a much less obvious error. It checks for the existence of the shared-framework
folder (C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App), not a specific 8.x
version - a first attempt at this checked the registry instead and turned out to false-
block a machine that genuinely had 8.0.30 installed, so it's deliberately kept to the
simplest thing that can't be wrong: that folder either exists or it doesn't. That also
means it's intentionally loose the other way - "some version of the ASP.NET Core shared
runtime is present," not "specifically 8.x" - since a false pass here (only .NET 6
installed, say) isn't a silent failure: the service still fails to start with .NET's own
clear "You must install .NET to run this application" error naming the exact missing
version. If you still get a false block on a machine you're sure has it installed, check
whether that folder exists under a different path on that machine (e.g. a custom .NET
install location) and let me know. A -SelfContained build (see below) skips this check
entirely, since it doesn't need the runtime preinstalled.
On first launch, browse to http://<machine>:8080 - you'll land on the first-run setup
wizard (see the root README's "First run" section) rather than a login page, since there's
no admin account yet. Walk through it to choose a data directory and create the admin
account; you're signed in as that account automatically once it finishes.
Rebuild and run a new ScriptRunnerSetup.msi (the Version in Product.wxs should be
bumped for a real release, but AllowSameVersionUpgrades="yes" means re-running the same
version also works, e.g. while iterating). The MajorUpgrade element handles removing
the old version's files and re-running the service install; existing secrets in the
registry and the SQLite database wherever it currently lives are left alone
(Install-Service.ps1 only generates a key when one doesn't already exist, and setup
"completing" a second time is a no-op - see IDataDirectoryProvider).
Upgrading from a build before the network-reference-scripts / setup-wizard feature
needs one manual step: this app uses EF Core's EnsureCreated() rather than migrations
(see the root README's "What's intentionally out of scope"), so an existing SQLite
database file won't automatically gain the new columns (SourceMode, NetworkPath,
NetworkCredentialId on scripts) or the new bootstrap.json setup-state file - the app
will likely throw a "no such column" error against a stale schema. Back up and delete the
old scriptrunner.db (and any bootstrap.json next to it) before starting the upgraded
service, then re-create your scripts/credentials/users, or restore them via your own
backup/scripting against the new schema.
Uninstall from "Apps & Features" or msiexec /x ScriptRunnerSetup.msi. This stops and
removes the ScriptRunner service and the installed files, but deliberately leaves
%ProgramData%\ScriptRunner (the database) and the service's registry Environment key
(the secrets) in place - see the comment at the top of scripts/Uninstall-Service.ps1.
Delete those by hand if you want a genuinely clean wipe:
Remove-Item -Recurse -Force "$env:ProgramData\ScriptRunner"
Remove-Item -Force "HKLM:\SYSTEM\CurrentControlSet\Services\ScriptRunner" -Recurse -ErrorAction SilentlyContinueEverything the MSI's custom actions do is also just scripts\Install-Service.ps1, which
is safe to run by hand from an elevated PowerShell prompt against a dotnet publish
output you produce yourself - useful if you'd rather not build/use the MSI at all, or if
you're debugging why a custom action didn't behave as documented:
dotnet publish src\ScriptRunner.Api -c Release -o C:\ScriptRunner
.\installer\scripts\Install-Service.ps1 -InstallDir C:\ScriptRunner -DataDir "$env:ProgramData\ScriptRunner"And to remove it: .\installer\scripts\Uninstall-Service.ps1. This is the same path the
root README's older "Running as a Windows Service" section walked through by hand with
raw sc.exe commands - the scripts here just make it idempotent and add the registry
env-var handling and firewall rule.
Product.wxs- the WiX source: directories, the harvested-files feature, the two service-management custom actions, and the install UI.ScriptRunner.Installer.wixproj- SDK-style WiX v5 project; harvests$(ApiPublishDir)(defaults to..\publish\) into the MSI.scripts/Install-Service.ps1/scripts/Uninstall-Service.ps1- the actual service (de)registration logic, invoked by the MSI and runnable standalone.License.rtf- placeholder license text shown in the install UI.