Skip to content

feat(installer): ship a Windows setup with PostgreSQL 17 - #20

Draft
mrcsin wants to merge 35 commits into
site-loopback-and-restartfrom
windows-installer
Draft

mrcsin wants to merge 35 commits into
site-loopback-and-restartfrom
windows-installer

Conversation

@mrcsin

@mrcsin mrcsin commented Oct 19, 2026

Copy link
Copy Markdown
Member

Commissioning a machine today takes three manual steps: install PostgreSQL with winget, run semibase site, restart the service. A suite installer cannot chain that.

SemiBase-Setup.exe (Inno Setup) does it in one run. It carries the EDB PostgreSQL 17 installer, pinned by SHA256, installs it when the machine has no PostgreSQL, uses a registered 17 when there is one, refuses every other engine state, then runs semibase site --restart-service. Exit code 0 means provisioned. Passwords reach semibase through the environment, never a command line.

Plan: docs/plans/20261019-windows-installer.md. Stacked on #19.

Verification: in progress, task by task. The installer CI job runs the setup silently on windows-latest, one case per state the plan lists.

mrcsin added 30 commits October 19, 2026 17:20
SemiBase-Setup.exe needs a script before it can provision anything.
This one carries semibase.exe into {app}, the EDB installer stored
uncompressed for [Code] to extract, and the C:\DISTR log folder.

PrepareToInstall refuses a missing superuser password, a password that
is not printable ASCII, and a superuser password with " or a trailing
\, so a refusal exits 7 before a file is copied. Setup's parser drops
every " from its command line, measured here, so a password parameter
that does not appear verbatim in GetCmdTail is refused as well.
A windows-latest job builds semibase.exe, fetches the cached EDB
payload, compiles the setup and runs it with no superuser password:
it must exit 7, install nothing and leave postgresql-x64-17 Disabled.
Until now a usable 17 or a clean machine made Setup copy semibase.exe
and exit 0 with nothing provisioned. PrepareToInstall now does the work
before a file is copied, so exit 0 means provisioned.

On a clean machine it runs the EDB installer with an option file under
{tmp}, deleted as soon as EDB returns, and reads the install back from
the registry like any registered 17. It then sets the service to
Automatic, starts it, waits up to 60 s for the port, and runs
semibase site with the passwords in Setup's environment and its output
in C:\DISTR\Logs\SemiBase\site-*.log. A non-zero EDB or site exit, or
a port that stays closed, is a refusal naming the log.

ExtractTemporaryFile matches the DestName of a [Files] entry, measured
with a scratch setup and recorded in installer.md.
Acceptance case 2: Setup with SEMIBASE_SUPER_PASSWORD=root and a writer
token in its environment must exit 0, leave postgresql-x64-17 Automatic
and let semiplot read semiplot_tags with password semiplot.
An interactive Setup had no way to take the superuser and scada_writer
passwords: it refused every run that came without a parameter or a
SEMIBASE_* variable. A password page now holds both, prefilled from the
same resolution, and PrepareToInstall reads its passwords from the page
in every mode, so the checks keep one input. Next on the page refuses an
empty superuser field, an unprintable or EDB-breaking password, and an
empty writer field when no PostgreSQL is registered; PrepareToInstall
still enforces each rule. Under /SILENT Setup simulates Next on the page,
so the page check returns True there and silent runs refuse exactly as
before.
The setup replaces the bare Windows binary as the commissioning path,
and Inno Setup needs a Windows runner while the image needs Linux. The
one release job becomes build, installer and release: the Windows
binary only feeds the setup, and the release page carries the Linux
binary and SemiBase-Setup.exe. "Latest" is still decided once in the
build job, and the image is still built and smoke-run before the
release and pushed after it.
The setup replaced winget, the bare Windows binary and the manual
restart, but every doc still described them. deployment.md now gives
the silent command line with Start-Process -Wait, the parameters, the
SEMIBASE_* fallbacks, exit codes 0 and 7, the setup and site logs, the
three state tables in operator terms, and the recovery section every
engine refusal points to, including the manual semibase.exe run.
README, CLAUDE.md, the architecture index, provisioning.md and
overview.md follow the three-job release and the two release assets;
installer.md gains what the setup does.
Every refusal came before the [Files] copy and the release no longer
attaches the Windows binary, so a machine where Setup never succeeded
had no semibase.exe for the manual recovery, and an offline site could
not get one. Every run now copies the carried binary to
C:\DISTR\Tools\SemiBase before any check.

The EDB password rules (ASCII, ", trailing \) refused a registered 17
whose password reaches semibase byte for byte; they now hold on the
fresh-install row only, which also refuses a leading - and leading or
trailing spaces nobody measured. An unquoted space in a password
parameter no longer passes as its first half. SEMIBASE_WRITER_PASSWORD
is always set, so a cleared field does not let the caller's value reset
scada_writer. The port probe dials localhost like semibase and tells a
failed probe from a closed port; registry values cmd.exe would change
are refused; an exception reaches the setup log; the dead [Dirs] entry
and the one-read Fresh field are gone. The payload has a fixed name, so
the pin lives in the fetch script, and Inno Setup is pinned to the
measured 6.7.3 in one composite action.

CI now runs every reachable engine row, the remaining password rules,
the variable channel on the fresh install with a watch on the option
file, a superuser-only re-run, a writer password change under full
statement logging, and a wrong registered port.
The release installer job compiled SemiBase-Setup.exe and attached it
without ever running it. It now runs the shipped setup once with no
password, requires the refusal, and requires the semibase.exe the
setup copied out to print the tag. The build steps come from the
shared build-setup action, so release and CI compile the same way.
The recovery section points at the copy every run leaves in
C:\DISTR\Tools\SemiBase and gives per-state inputs for the manual run;
the password rules, refusal rows, pens step, port and writer re-run
notes match the setup; CLAUDE.md, installer.md and the plan describe
the new CI cases, and the plan records skipped items as skipped.
The example "/SUPERPASSWORD=два слова" lost its quotes in Start-Process
and was non-ASCII, so Setup refused it. A password with a space now goes
through the variable, or quoted inside the -ArgumentList string, the
form measured to reach Setup intact.
Elevated Setup wrote its logs to C:\DISTR\Logs\SemiBase and the
recovery semibase.exe to C:\DISTR\Tools\SemiBase. C:\ gives every
authenticated user Modify on folders made under it and lets any user
create one, so a user could pre-create either path as a junction and
redirect Setup's log writes and CopyFile, or swap the recovery binary
that the deployment guide runs elevated with the superuser password.
A protected DACL on the leaf folder does not help: measured, a user
who can modify the parent renames it and plants a junction in its
place. Locking C:\DISTR and C:\DISTR\Logs would touch folders other
products own, so both now live in {commonpf}\SemiBase Setup, where
only administrators can create or change anything.

ParameterRefusal compared only a prefix of the raw tail, so
/SUPERPASSWORD=Aa1z" arrived as Aa1z and passed, and EDB would create
the cluster with a password other than the one typed. The value must
now end at the end of the tail, at a space, or at a quote closing one
opened right before /NAME=; "/SUPERPASSWORD=two words" still passes.

Files: Installer/SemiBase.iss, .github/scripts/installer-case.ps1,
.github/workflows/ci.yml, docs/deployment.md,
docs/architecture/installer.md,
docs/plans/20261019-windows-installer.md, CLAUDE.md, README.md
An unquoted /SUPERPASSWORD=abc /def reached Setup as two arguments:
the value abc matched the command tail verbatim and /def passed the
stray-argument check because it starts with /. Inno 6.7.3 ignores an
unknown switch, so EDB and site took abc, Setup exited 0 and the typed
password did not log in; the same held for /WRITERPASSWORD.

Every argument must now be a password parameter or a switch Inno's
InitializeSetup loop reads (Setup.MainFunc.pas, SetupLdr.dpr at
is-6_7_3), so switches after the passwords still work. The refusal
names the argument by number only, since its text may be the rest of a
password. A scratch probe confirmed that Inno ignores unknown switches
and that the documented forms still pass; installer.md records it.

CI case 5 adds both split forms, a /NOCANCEL after the password, and
"/SUPERPASSWORD=two words\", which only the whole value carries to the
trailing-\ rule. The plan's case 9 line and its ci.yml comment now say
what case 6 watches.
The major 17 stood as a literal in the version check, the
--expected-major argument and five messages, and the 60 s in the port
wait refusal copied PortWaitMilliseconds. A change to either left the
other copy wrong. PgMajor now feeds PgInstallation, PgService,
PgDataDir and every message, and the refusal reads the constant.

The password page derived the writer rule a second time, with its own
wording. It now calls FreshInstallRefusal with ProbeMachine False, so
the fresh row's rules and texts live in one function. LogPath creates
the log folder, which LogRefusal and SiteRefusal each did before.
IsSetupSwitch also accepts the two password parameters, so it is now
IsKnownArgument. No refusal text changes.
installer-case.ps1 holds the setup's paths, but the diagnostics step
spelled out the token file and the log folder again and carried its own
redaction loop, and the cases named postgresql-x64-17 per step and ran
psql as postgres three different ways. A path or name change had to be
made in several places.

The helper now holds $PgInstallation, $PgService, Invoke-Super and
Write-Redacted, which Write-Logs uses, and the diagnostics step
dot-sources it. Shellcheck covers .github/scripts/*.sh too, which it
passes with no change, as bash.md asks of every script.
The pin docs said a bump edits the fetch script alone, while
installer.md keeps a measured table of the same values. "The Windows
job" now names the build job, since installer also runs on Windows.
release.yml's image run is the only run of the shipped Linux binary,
not of all shipped bytes, since the setup smoke run runs the Windows
one. The CLAUDE.md paragraphs this branch edited are rewrapped at 100
columns, and the Windows build job paragraph names the helper and the
wider shellcheck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant