Skip to content

Installer smoke test #13

Installer smoke test

Installer smoke test #13

name: Installer smoke test
# Installs the built installer on a fresh, throwaway GitHub-hosted Windows runner
# and checks that it actually produces a working service - the one thing the
# release process could not verify locally, because running the installer on the
# maintainer's build machine hijacks the development service and COM registration
# (see the release notes in RELEASE.md and the maintainer's environment memory).
#
# A GitHub-hosted runner is the right place for this precisely because it is
# discarded after the job: a real silent install, a real service registration and
# a real uninstall happen on a machine nobody else depends on.
#
# The installer is NOT built here - the server needs VS 2026 / v145 and the
# prebuilt native dependencies, which only the self-hosted runner has. Instead this
# takes an already-built installer: either an asset attached to a release, or an
# artifact uploaded by a prior run. Point it at one with the inputs below.
on:
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag whose installer asset to smoke-test (e.g. v6.2.18). Leave blank to use an uploaded artifact named "installer".'
type: string
required: false
run_id:
description: 'Run ID to download the "installer" artifact from (when not using a release tag).'
type: string
required: false
permissions:
contents: read
actions: read
jobs:
smoke:
name: Install on a throwaway runner and verify
runs-on: windows-latest
steps:
- name: Fetch the installer from the release
if: ${{ inputs.release_tag != '' }}
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force installer | Out-Null
gh release download "${{ inputs.release_tag }}" `
--repo "${{ github.repository }}" `
--pattern "hMailServer-*-x64.exe" `
--dir installer
if (-not $LASTEXITCODE -eq 0) { throw "Could not download the installer asset from ${{ inputs.release_tag }}." }
- name: Fetch the installer from an uploaded artifact
if: ${{ inputs.release_tag == '' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: installer
path: installer
run-id: ${{ inputs.run_id }}
github-token: ${{ github.token }}
- name: Locate the installer
id: find
shell: pwsh
run: |
$exe = Get-ChildItem installer -Recurse -Filter 'hMailServer-*-x64.exe' | Select-Object -First 1
if (-not $exe) { throw "No hMailServer-*-x64.exe found to test." }
Write-Host "Installer: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)"
"exe=$($exe.FullName)" >> $env:GITHUB_OUTPUT
- name: Silent install
shell: pwsh
run: |
$exe = '${{ steps.find.outputs.exe }}'
# /VERYSILENT drives the embedded SQL CE backend, which needs no external
# database - that is what makes the installer testable on a bare runner.
# /SUPPRESSMSGBOXES matters: the installer shows a MsgBox on a DB-setup
# error, and in silent mode that would otherwise pick the default button
# and mask the failure. The admin password comes from a wizard page that
# silent mode skips, so it is not set here and the checks below do not
# assume one.
#
# /LOG and the timeout below both exist because of what happened on
# 15 August 2026: the 6.2.19 installer hung in this step and the job sat
# for 90 minutes and then had to be cancelled by hand, having produced no
# evidence whatsoever about WHERE it stopped. The same test against 6.2.18
# had finished in 33 seconds. A hang that reports nothing is barely better
# than no test, so this step now bounds itself and keeps the log either
# way - Inno's own log names the last [Run] entry or [Code] call it
# reached, which is the one thing needed to tell an installer that is slow
# from one that is stuck.
$log = Join-Path $env:RUNNER_TEMP 'hmailserver-install.log'
"install_log=$log" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$p = Start-Process -FilePath $exe `
-ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/TYPE=full',"/LOG=$log" `
-PassThru
# Ten minutes is far above the 33 seconds a healthy install of this
# product takes, including the bundled .NET runtime, and far below the
# six-hour job ceiling that made the last hang so expensive.
if (-not $p.WaitForExit(10 * 60 * 1000)) {
Write-Host "::error::The installer did not finish within 10 minutes - treating it as hung."
try { $p.Kill() } catch { }
if (Test-Path $log) {
Write-Host "----- last 80 lines of the install log -----"
Get-Content $log -Tail 80 | ForEach-Object { Write-Host $_ }
} else {
Write-Host "No install log was produced at $log."
}
throw "Installer hung."
}
Write-Host "Installer exit code: $($p.ExitCode)"
if ($p.ExitCode -ne 0) {
if (Test-Path $log) {
Write-Host "----- last 80 lines of the install log -----"
Get-Content $log -Tail 80 | ForEach-Object { Write-Host $_ }
}
throw "Installer returned a non-zero exit code."
}
id: install
# Runs only when the install step failed, and it is the diagnostic that
# matters: the installer's message box says merely "exit code 1", while
# DBSetupQuick writes the real reason - the SQL error, the missing script,
# the permission - to stderr, where nobody was reading it. The files are
# already on disk by the time the box appears (Inno logs "Installation
# process succeeded" first), so the tool can simply be run again here.
- name: Why did the database setup fail
if: ${{ failure() }}
shell: pwsh
run: |
$tool = Get-ChildItem 'C:\Program Files*\hMailServer\Bin\DBSetupQuick.exe' -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $tool) {
Write-Host 'DBSetupQuick.exe is not installed - the failure was earlier than the database step.'
exit 0
}
$out = Join-Path $env:RUNNER_TEMP 'dbsetup-stdout.txt'
$err = Join-Path $env:RUNNER_TEMP 'dbsetup-stderr.txt'
$p = Start-Process -FilePath $tool.FullName -ArgumentList '/silent' -PassThru `
-RedirectStandardOutput $out -RedirectStandardError $err
if (-not $p.WaitForExit(3 * 60 * 1000)) {
try { $p.Kill() } catch { }
Write-Host 'DBSetupQuick did not finish within three minutes.'
} else {
Write-Host "DBSetupQuick exit code: $($p.ExitCode)"
}
foreach ($f in @($out, $err)) {
if ((Test-Path $f) -and (Get-Item $f).Length -gt 0) {
Write-Host "----- $(Split-Path $f -Leaf) -----"
Get-Content $f | ForEach-Object { Write-Host $_ }
}
}
$ini = 'C:\Program Files\hMailServer\Bin\hMailServer.INI'
if (Test-Path $ini) {
Write-Host '----- [Database] section as the installer left it -----'
Get-Content $ini | Select-String -Pattern '^\[Database\]' -Context 0,12 | ForEach-Object { Write-Host $_ }
}
- name: The service is registered and points at the installed binary
shell: pwsh
run: |
$svc = Get-CimInstance Win32_Service -Filter "Name='hMailServer'" -ErrorAction SilentlyContinue
if (-not $svc) { throw "The installer did not register the hMailServer service." }
Write-Host "Service image path: $($svc.PathName)"
if ($svc.PathName -notmatch 'hMailServer\.exe') { throw "Service image path does not point at hMailServer.exe." }
- name: The service starts and listens on 25 / 110 / 143
shell: pwsh
run: |
Start-Service hMailServer
# The embedded database initialises on first start; give it a moment.
Start-Sleep -Seconds 10
$svc = Get-Service hMailServer
if ($svc.Status -ne 'Running') { throw "Service did not reach Running (state: $($svc.Status))." }
foreach ($port in 25, 110, 143) {
$listening = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if (-not $listening) { throw "Service is running but not listening on $port." }
Write-Host "Listening on $port."
}
- name: COM server is registered and instantiable
shell: pwsh
run: |
# Creating the object proves the installer registered the out-of-process
# COM server (LocalServer32). The admin password is not known after a
# silent install, so authentication is deliberately not attempted; a
# working service that listens on all three ports (checked above) is the
# evidence that DBSetupQuick built the database.
$app = New-Object -ComObject hMailServer.Application
if ($null -eq $app) { throw "Could not instantiate the hMailServer.Application COM object." }
Write-Host "COM object created; server product string: $($app.Product)"
# Kept whatever happened. On a green run it is a few KB nobody reads; on a
# hang or a failure it is the only record of how far the installer got, and
# the runner it was written on is discarded minutes later.
- name: Keep the install log
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: install-log
path: ${{ runner.temp }}/hmailserver-install.log
if-no-files-found: warn
- name: Uninstall cleanly
if: ${{ always() }}
shell: pwsh
run: |
Stop-Service hMailServer -ErrorAction SilentlyContinue
$uninst = Get-ChildItem 'C:\Program Files*\hMailServer\unins*.exe' -ErrorAction SilentlyContinue | Select-Object -First 1
if ($uninst) {
$p = Start-Process -FilePath $uninst.FullName -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Wait -PassThru
Write-Host "Uninstaller exit code: $($p.ExitCode)"
} else {
Write-Warning "No uninstaller found - the runner is ephemeral, so this is not fatal."
}