From 9e6abeef74b5b8ae8210781fd0e40493713d735e Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Fri, 8 May 2026 09:52:37 -0400 Subject: [PATCH] Phase 6+7: Inno Setup installer + GitHub Actions release pipeline installer/webhook-server.iss is an Inno Setup 6 script that: - Installs to %ProgramFiles%\WebhookServer - Creates Start Menu folder + GUI shortcut (and optional desktop icon) - Runs install-service.ps1 post-install to register the Windows Service - Runs uninstall-service.ps1 pre-uninstall to remove it - Bundles the webhook-server icon for the installer / uninstaller scripts/build-installer.ps1 is the local build helper: publishes both projects, finds ISCC.exe (PATH or standard install path), compiles the installer with the version pulled from Directory.Build.props, drops the output in dist/. .github/workflows/ci.yml runs build + test on every push/PR to main. .github/workflows/release.yml triggers on v* tags (or manual dispatch), runs tests, installs Inno Setup via choco, builds the installer, and attaches the .exe to a GitHub Release. Pre-1.0 versions are flagged prerelease automatically. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 27 ++++++++++++ .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++++ installer/webhook-server.iss | 79 +++++++++++++++++++++++++++++++++++ scripts/build-installer.ps1 | 68 ++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 installer/webhook-server.iss create mode 100644 scripts/build-installer.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2646469 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore WebhookServer.sln + + - name: Build + run: dotnet build WebhookServer.sln -c Release --no-restore + + - name: Test + run: dotnet test WebhookServer.sln -c Release --no-build --verbosity normal diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b7e6750 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to build (e.g. 0.1.0). Defaults to Directory.Build.props.' + required: false + +jobs: + build-installer: + runs-on: windows-latest + permissions: + contents: write # needed to create releases / upload assets + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Resolve version + id: ver + shell: pwsh + run: | + if ('${{ github.event_name }}' -eq 'push') { + $v = '${{ github.ref_name }}'.TrimStart('v') + } elseif ('${{ inputs.version }}') { + $v = '${{ inputs.version }}' + } else { + [xml]$p = Get-Content Directory.Build.props + $v = $p.Project.PropertyGroup.Version + } + "version=$v" | Out-File $env:GITHUB_OUTPUT -Append + Write-Host "Building version $v" + + - name: Restore + test + run: | + dotnet restore WebhookServer.sln + dotnet test WebhookServer.sln -c Release + + - name: Install Inno Setup + shell: pwsh + run: | + choco install innosetup --no-progress -y + Write-Host "ISCC at: $((Get-Command iscc).Path)" + + - name: Build installer + shell: pwsh + run: ./scripts/build-installer.ps1 -VersionOverride ${{ steps.ver.outputs.version }} + + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: WebhookServer-Setup-${{ steps.ver.outputs.version }} + path: dist/WebhookServer-Setup-*.exe + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + name: Webhook Server ${{ steps.ver.outputs.version }} + tag_name: ${{ github.ref_name }} + draft: false + prerelease: ${{ startsWith(steps.ver.outputs.version, '0.') }} + files: dist/WebhookServer-Setup-*.exe + generate_release_notes: true diff --git a/installer/webhook-server.iss b/installer/webhook-server.iss new file mode 100644 index 0000000..36c7314 --- /dev/null +++ b/installer/webhook-server.iss @@ -0,0 +1,79 @@ +; Inno Setup script for Webhook Server. +; +; Build: iscc /DAppVersion=0.1.0 webhook-server.iss +; Output: ..\dist\WebhookServer-Setup-{AppVersion}.exe +; +; The installer copies published binaries to {pf}\WebhookServer, installs the +; Windows Service via install-service.ps1 post-install, and removes the service +; via uninstall-service.ps1 pre-uninstall. Start Menu gets a single GUI shortcut. + +#ifndef AppVersion + #define AppVersion "0.1.0" +#endif + +#define AppName "Webhook Server" +#define AppPublisher "Justin Paul" +#define AppURL "https://jpaul.me" +#define AppExeName "WebhookServer.Gui.exe" +#define ServiceExeName "WebhookServer.Service.exe" +#define ServiceName "WebhookServer" +#define RepoRoot "..\" + +[Setup] +AppId={{6E3B3C1A-9C20-4F50-B6A8-2B6D6D7E2F11} +AppName={#AppName} +AppVersion={#AppVersion} +AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL=https://github.com/recklessop/webhook-server +AppUpdatesURL=https://github.com/recklessop/webhook-server/releases +DefaultDirName={autopf}\WebhookServer +DefaultGroupName={#AppName} +DisableProgramGroupPage=yes +OutputBaseFilename=WebhookServer-Setup-{#AppVersion} +OutputDir={#RepoRoot}dist +SetupIconFile={#RepoRoot}resources\webhook-server.ico +UninstallDisplayIcon={app}\{#AppExeName} +PrivilegesRequired=admin +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +VersionInfoVersion={#AppVersion}.0 +VersionInfoCompany={#AppPublisher} +VersionInfoProductName={#AppName} + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked + +[Files] +Source: "{#RepoRoot}publish\service\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#RepoRoot}publish\gui\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#RepoRoot}scripts\install-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "{#RepoRoot}scripts\uninstall-service.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "{#RepoRoot}README.md"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#RepoRoot}resources\webhook-server.ico"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\webhook-server.ico" +Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}" +Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\webhook-server.ico"; Tasks: desktopicon + +[Run] +Filename: "powershell.exe"; \ + Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\install-service.ps1"" -BinaryPath ""{app}\{#ServiceExeName}"""; \ + StatusMsg: "Installing Windows Service..."; \ + Flags: runhidden +Filename: "{app}\{#AppExeName}"; \ + Description: "Launch {#AppName}"; \ + Flags: postinstall nowait skipifsilent + +[UninstallRun] +Filename: "powershell.exe"; \ + Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\uninstall-service.ps1"""; \ + Flags: runhidden; \ + RunOnceId: "RemoveWebhookService" diff --git a/scripts/build-installer.ps1 b/scripts/build-installer.ps1 new file mode 100644 index 0000000..af8e060 --- /dev/null +++ b/scripts/build-installer.ps1 @@ -0,0 +1,68 @@ +<# +.SYNOPSIS + End-to-end installer build: publish service + GUI, then run Inno Setup + to produce dist/WebhookServer-Setup-{version}.exe. + +.DESCRIPTION + Reads the version from Directory.Build.props. Requires Inno Setup 6 (ISCC.exe) + on PATH or in the standard install location. CI runs this same script after + setup-dotnet + winget install Inno Setup. +#> +[CmdletBinding()] +param( + [string]$Configuration = 'Release', + [string]$VersionOverride +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-RepoVersion { + $propsPath = Join-Path $repoRoot 'Directory.Build.props' + [xml]$props = Get-Content $propsPath + return $props.Project.PropertyGroup.Version +} + +function Find-InnoCompiler { + $candidates = @( + 'ISCC.exe', # on PATH + 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe', + 'C:\Program Files\Inno Setup 6\ISCC.exe' + ) + foreach ($c in $candidates) { + $cmd = Get-Command $c -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Path } + if (Test-Path $c) { return $c } + } + throw "Inno Setup compiler not found. Install with: winget install JRSoftware.InnoSetup" +} + +$version = if ($VersionOverride) { $VersionOverride } else { Get-RepoVersion } +Write-Host "Building Webhook Server installer v$version" -ForegroundColor Cyan + +# 1. Publish both projects. +$publishSvc = Join-Path $repoRoot 'publish\service' +$publishGui = Join-Path $repoRoot 'publish\gui' +Remove-Item -Recurse -Force $publishSvc, $publishGui -ErrorAction SilentlyContinue + +& dotnet publish (Join-Path $repoRoot 'src\WebhookServer.Service\WebhookServer.Service.csproj') ` + -c $Configuration -r win-x64 --self-contained false -o $publishSvc | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'service publish failed' } + +& dotnet publish (Join-Path $repoRoot 'src\WebhookServer.Gui\WebhookServer.Gui.csproj') ` + -c $Configuration -r win-x64 --self-contained false -o $publishGui | Out-Host +if ($LASTEXITCODE -ne 0) { throw 'GUI publish failed' } + +# 2. Compile installer. +$iscc = Find-InnoCompiler +$iss = Join-Path $repoRoot 'installer\webhook-server.iss' +$dist = Join-Path $repoRoot 'dist' +New-Item -ItemType Directory -Path $dist -Force | Out-Null + +Write-Host "Compiling installer with $iscc" +& $iscc "/DAppVersion=$version" $iss +if ($LASTEXITCODE -ne 0) { throw 'Inno Setup compile failed' } + +$out = Get-Item (Join-Path $dist "WebhookServer-Setup-$version.exe") +Write-Host "" +Write-Host ("Built: {0} ({1:n0} bytes)" -f $out.FullName, $out.Length) -ForegroundColor Green