Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/appliance.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Appliance

# Builds the downloadable virtual appliances for ALL hypervisors in one job: customizes the Ubuntu 24.04
# cloud image with libguestfs (no Hyper-V host / nested virt needed) into a Gen2/UEFI image with SQL Server
# Express + Dispatch baked in, emits it as VHDX (Hyper-V), OVA (VMware), and qcow2 (KVM/Proxmox), then boots
# cloud image with libguestfs (no Hyper-V host / nested virt needed) into a Gen2/UEFI image with PostgreSQL
# + Dispatch baked in, emits it as VHDX (Hyper-V), OVA (VMware), and qcow2 (KVM/Proxmox), then boots
# it under QEMU/UEFI to prove the service comes up on first boot. Artifacts: dispatch-appliance-hyperv,
# dispatch-appliance-vmware, dispatch-appliance-kvm.
on:
Expand All @@ -20,7 +20,7 @@ jobs:
# single image, then uploads them as three separate downloads near the end. The name reflects that
# it is not Hyper-V-only.
build-appliances:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

Expand Down Expand Up @@ -135,7 +135,7 @@ jobs:
sudo convert /tmp/screen.ppm "$1" 2>/dev/null && echo "captured $1" || echo "no screenshot"
}
ok=0
for i in $(seq 1 90); do # up to ~8 min (KVM boot is fast; this is mostly SQL first-run)
for i in $(seq 1 90); do # up to ~8 min (KVM boot is fast; this is mostly PostgreSQL first-run)
if curl -fsSk -m 3 https://localhost:8420/health >/dev/null 2>&1; then ok=1; break; fi
[ "$i" = 18 ] && screenshot boot-90s.png # ~90s in: capture whatever's on screen
sleep 5
Expand Down
32 changes: 18 additions & 14 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,29 @@ env:

jobs:
build-test:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]

services:
# SQL Server 2022 service container (GitHub-hosted runners are free for this public repo). Same wire
# protocol / T-SQL as the dev Azure SQL Edge container; app code is unchanged. The Data test fixture
# waits up to 90s for the engine to accept connections, so no container health-cmd is needed.
sql:
image: mcr.microsoft.com/mssql/server:2022-latest
# PostgreSQL service container. The Data test fixture waits up to 90s for the engine to accept
# connections; the health-cmd gates job steps until pg_isready succeeds.
db:
image: postgres:17
env:
ACCEPT_EULA: "Y"
# Dev-only SA password. Override by setting the MSSQL_SA_PASSWORD repo/org secret; the literal is
# Dev-only password. Override by setting the POSTGRES_PASSWORD repo/org secret; the literal is
# only a fallback and grants nothing beyond the ephemeral CI container.
MSSQL_SA_PASSWORD: ${{ secrets.MSSQL_SA_PASSWORD || 'Dispatch_Dev_Pass123' }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'Dispatch_Dev_Pass123' }}
ports:
- 1433:1433
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 12

env:
# Drives the Dispatch.Data integration tests against the SQL service container.
DISPATCH_TEST_SQL: "Server=localhost,1433;User Id=sa;Password=${{ secrets.MSSQL_SA_PASSWORD || 'Dispatch_Dev_Pass123' }};TrustServerCertificate=True;Encrypt=True"
# Drives the Dispatch.Data integration tests against the Postgres service container. No Database= here;
# the fixture creates a uniquely-named database per run and drops it afterwards.
DISPATCH_TEST_SQL: "Host=localhost;Port=5432;Username=postgres;Password=${{ secrets.POSTGRES_PASSWORD || 'Dispatch_Dev_Pass123' }}"
# Make the integration tests fail loudly if the connection string is ever missing, instead of skipping.
DISPATCH_REQUIRE_SQL: "1"
DOTNET_NOLOGO: "true"
Expand Down Expand Up @@ -83,7 +87,7 @@ jobs:
# Keep em dashes out of the tree: they break Windows PowerShell parsing (a mis-decoded em-dash byte
# becomes a smart-quote string delimiter) and the project standardizes on ASCII '-'.
no-em-dash:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5
- name: Reject em dashes (use ASCII '-')
Expand All @@ -103,7 +107,7 @@ jobs:
trigger-appliance:
needs: [build-test, no-em-dash]
if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'feat/relay-engine-web-providers') }}
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
permissions:
actions: write
steps:
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Docker

# Builds the container image and runs the full stack (Dispatch + SQL via docker-compose), failing unless
# Builds the container image and runs the full stack (Dispatch + PostgreSQL via docker-compose), failing unless
# the service comes up and serves /health - the container equivalent of the installer smoke tests. Docker is
# native on the runner, so this needs no QEMU/nested virtualization. The release workflow builds+pushes the
# multi-arch image; this proves it actually runs before that.
Expand All @@ -20,24 +20,24 @@ on:

jobs:
build-and-smoke:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

- name: Build image + start the stack (Dispatch + SQL)
- name: Build image + start the stack (Dispatch + PostgreSQL)
run: docker compose up --build -d

- name: Wait for the dashboard /health
run: |
ok=0
for _ in $(seq 1 60); do # up to 5 min: image already built; SQL start + DB init + service
for _ in $(seq 1 60); do # up to 5 min: image already built; PostgreSQL start + DB init + service
if curl -fsSk -m 5 https://localhost:8420/health >/dev/null 2>&1; then ok=1; break; fi
sleep 5
done
if [ "$ok" != 1 ]; then
echo "=== compose ps ==="; docker compose ps
echo "=== dispatch logs ==="; docker compose logs --tail 150 dispatch || true
echo "=== sql logs ==="; docker compose logs --tail 40 sql || true
echo "=== postgresql logs ==="; docker compose logs --tail 40 db || true
echo "dispatch /health did not respond"; exit 1
fi
echo "Docker stack /health OK"
Expand Down
48 changes: 26 additions & 22 deletions .github/workflows/installers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ name: Installers

# Validates the installer artifacts on GitHub-hosted runners (free for this public repo). Non-destructive
# by default: the Windows job builds the MSI + Burn bundle (proving the WiX sources + net472 launcher
# compile) and the Linux job lints install.sh. The actual SQL-Express install + service smoke test is
# destructive (it installs SQL Server on the runner), so it only runs when dispatched with full_install=true.
# compile) and the Linux job lints install.sh. The actual PostgreSQL install + service smoke test is
# destructive (it installs PostgreSQL on the runner), so it only runs when dispatched with full_install=true.
on:
workflow_dispatch:
inputs:
full_install:
description: "Also run the real SQL install + service smoke test (mutates the runner)"
description: "Also run the real PostgreSQL install + service smoke test (mutates the runner)"
type: boolean
default: false
push:
Expand All @@ -24,7 +24,7 @@ env:

jobs:
windows-installer:
runs-on: windows-latest
runs-on: [self-hosted, windows, x64]
steps:
- uses: actions/checkout@v5

Expand Down Expand Up @@ -83,19 +83,19 @@ jobs:
working-directory: installer/windows
run: wix build Dispatch.wxs -arch x64 -d PublishDir=publish -ext WixToolset.Firewall.wixext -o Dispatch.msi

- name: Build the SQL Express launcher
run: dotnet build installer/windows/sql-express/SqlExpressLauncher.csproj -c Release
- name: Build the PostgreSQL launcher
run: dotnet build installer/windows/postgres/PostgresLauncher.csproj -c Release

- name: Build the bundle
working-directory: installer/windows
shell: pwsh
run: |
wix build bundle/Bundle.wxs -ext WixToolset.BootstrapperApplications.wixext -ext WixToolset.Util.wixext `
-bindpath "SqlLauncher=sql-express/bin/Release/net472" -bindpath "Msi=." -o DispatchSetup.exe
-bindpath "PgLauncher=postgres/bin/Release/net472" -bindpath "Msi=." -o DispatchSetup.exe

# CI installer builds are intentionally unsigned (signing happens only on tagged releases via
# release.yml + Azure Artifact Signing). This artifact proves the WiX sources + launcher compile.
# DispatchSetup.exe is the single distributable file - it embeds Dispatch.msi and chains SQL Express.
# DispatchSetup.exe is the single distributable file - it embeds Dispatch.msi and chains PostgreSQL.
# (The MSI is still built above to prove the WiX sources compile and to embed in the bundle.)
- name: Upload installer artifact
uses: actions/upload-artifact@v6
Expand All @@ -104,7 +104,7 @@ jobs:
path: installer/windows/DispatchSetup.exe
if-no-files-found: error

# Real end-to-end check: run the bundle (installs SQL Express + the service) and confirm the Windows
# Real end-to-end check: run the bundle (installs PostgreSQL + the service) and confirm the Windows
# service starts and serves /health. Runs on every installer-path push and on manual dispatch, so a
# service that won't start (bad config, missing files, crash on boot) fails CI instead of the customer.
- name: Full install + smoke
Expand Down Expand Up @@ -246,7 +246,7 @@ jobs:
# the Actions run page). This needs NO tag and creates NO release - releases are only for permanent
# public download links. Grab "dispatch-linux-tarball" from the run to test install.sh --prebuilt.
linux-tarball:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

Expand Down Expand Up @@ -291,9 +291,12 @@ jobs:
stage="dispatch-ci-linux-x64"
mkdir -p "staging/${stage}/bin"
cp -r publish/linux/. "staging/${stage}/bin/"
cp installer/linux/install.sh "staging/${stage}/install.sh"
cp installer/linux/dispatch.service "staging/${stage}/dispatch.service"
chmod +x "staging/${stage}/install.sh" "staging/${stage}/bin/Dispatch.Service"
# Ship the same files as the release tarball (install.sh installs all of these systemd units).
cp installer/linux/install.sh installer/linux/dispatch.service \
installer/linux/dispatch-updater.service installer/linux/dispatch-update.path \
installer/linux/dispatch-update.sh "staging/${stage}/"
cp src/Dispatch.Core/Updates/dispatch-update-public.pem "staging/${stage}/"
chmod +x "staging/${stage}/install.sh" "staging/${stage}/dispatch-update.sh" "staging/${stage}/bin/Dispatch.Service"
tar -czf "${stage}.tar.gz" -C staging "${stage}"
ls -lh "${stage}.tar.gz"

Expand All @@ -305,20 +308,20 @@ jobs:
if-no-files-found: error

# Real end-to-end check (mirrors the Windows full-install smoke): install the just-built self-contained
# tarball exactly as a user would - install.sh --prebuilt --install-sql, which installs SQL Server
# tarball exactly as a user would - install.sh --prebuilt --install-postgres, which installs PostgreSQL
# locally and provisions the DispatchLog DB - then confirm the systemd service starts and serves
# /health. Runs on installer-path pushes and manual dispatch so a service that won't start (or a broken
# SQL bootstrap) fails CI instead of the customer.
# PostgreSQL bootstrap) fails CI instead of the customer.
- name: Full install + smoke
if: ${{ github.event_name == 'push' || inputs.full_install == true }}
env:
SA_PW: "Ci-Smoke-Passw0rd!"
DB_PW: "Ci-Smoke-Passw0rd!"
run: |
set -euo pipefail
mkdir -p /tmp/extract && tar -xzf dispatch-ci-linux-x64.tar.gz -C /tmp/extract
dir=/tmp/extract/dispatch-ci-linux-x64
sudo "$dir/install.sh" --prebuilt "$dir/bin" --install-sql \
--sa-password "$SA_PW" --admin-password "$SA_PW"
sudo "$dir/install.sh" --prebuilt "$dir/bin" --install-postgres \
--db-password "$DB_PW" --admin-password "$DB_PW"

ok=0
for _ in $(seq 1 36); do
Expand All @@ -329,7 +332,7 @@ jobs:
echo "=== dispatch service ==="; sudo systemctl status dispatch --no-pager || true
echo "=== journal ==="; sudo journalctl -u dispatch --no-pager -n 80 || true
echo "=== app logs ==="; sudo tail -n 60 /var/log/dispatch/*.log 2>/dev/null || true
echo "=== sql server ==="; sudo systemctl status mssql-server --no-pager 2>/dev/null || true
echo "=== postgresql ==="; sudo systemctl status postgresql --no-pager 2>/dev/null || true
echo "health check failed"; exit 1
fi
curl -fsSk https://localhost:8420/health; echo " <- /health OK"
Expand All @@ -347,7 +350,7 @@ jobs:
[ "$tls_ok" = 1 ] || { echo "STARTTLS negotiation failed on all SMTP ports"; exit 1; }

linux-installer:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

Expand All @@ -361,5 +364,6 @@ jobs:
fi

# The end-to-end install + "service actually runs" smoke lives in the linux-tarball job below - it
# installs the real self-contained tarball (install.sh --prebuilt) against a SQL container and checks
# /health, mirroring the Windows full-install smoke. This job stays a fast lint of install.sh.
# installs the real self-contained tarball (install.sh --prebuilt) against a locally-installed
# PostgreSQL and checks /health, mirroring the Windows full-install smoke. This job stays a fast lint
# of install.sh.
27 changes: 13 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Release
#
# This builds the distributable artifacts, stamps them with the tag version, optionally Authenticode-signs
# the Windows installer via Azure Artifact Signing, and publishes a GitHub Release with:
# - DispatchSetup.exe the single-file Windows installer (embeds the signed MSI + chains SQL Express)
# - DispatchSetup.exe the single-file Windows installer (embeds the signed MSI + chains PostgreSQL)
# - dispatch-<ver>-linux.tar.gz universal Linux bundle (x64 + arm64, self-contained)
# - SHA256SUMS checksums for all of the above
#
Expand Down Expand Up @@ -40,7 +40,7 @@ env:

jobs:
windows:
runs-on: windows-latest
runs-on: [self-hosted, windows, x64]
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
Expand Down Expand Up @@ -116,21 +116,21 @@ jobs:
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Build the SQL Express launcher
run: dotnet build installer/windows/sql-express/SqlExpressLauncher.csproj -c Release
- name: Build the PostgreSQL launcher
run: dotnet build installer/windows/postgres/PostgresLauncher.csproj -c Release

- name: Build the bundle
working-directory: installer/windows
shell: pwsh
run: |
wix build bundle/Bundle.wxs -d Version=${{ steps.ver.outputs.version }} `
-ext WixToolset.BootstrapperApplications.wixext -ext WixToolset.Util.wixext `
-bindpath "SqlLauncher=sql-express/bin/Release/net472" -bindpath "Msi=." -o DispatchSetup.exe
-bindpath "PgLauncher=postgres/bin/Release/net472" -bindpath "Msi=." -o DispatchSetup.exe

# --- Sign the Burn bundle (opt-in) --------------------------------------------------------
# A Burn bundle has an attached engine; the engine must be signed too, so detach it, sign it,
# reattach, then sign the final bundle (WiX-recommended; otherwise UAC flags the inner engine).
# installer/windows holds other .exe files (publish/, sql-express/), so each Azure sign step
# installer/windows holds other .exe files (publish/, postgres/), so each Azure sign step
# targets a folder containing ONLY the one file to sign, and we copy the result back in place.
- name: Detach the bundle engine
if: ${{ vars.AZURE_SIGNING_ACCOUNT != '' }}
Expand Down Expand Up @@ -185,12 +185,12 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: windows
# DispatchSetup.exe is the single distributable - it embeds the (signed) MSI and chains SQL Express.
# DispatchSetup.exe is the single distributable - it embeds the (signed) MSI and chains PostgreSQL.
path: installer/windows/DispatchSetup.exe
if-no-files-found: error

linux:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

Expand Down Expand Up @@ -247,11 +247,10 @@ jobs:
Dispatch SMTP Relay ${ver} - Linux (universal: x64 + arm64, self-contained; no .NET SDK required).

install.sh auto-detects your CPU arch (uses bin-x64 or bin-arm64) - no flags needed:
sudo ./install.sh --install-sql --sa-password '<StrongSaPassw0rd!>' --admin-password '<DashboardPassword!>'
sudo ./install.sh --install-postgres --db-password '<StrongDbPassw0rd!>' --admin-password '<DashboardPassword!>'

On arm64, --install-sql runs Azure SQL Edge in a container (SQL Server is amd64-only).
Or use an existing SQL Server / Azure SQL:
sudo ./install.sh --sql-connection 'Server=...;Database=DispatchLog;User Id=...;Password=...;TrustServerCertificate=True;Encrypt=True' --admin-password '<DashboardPassword!>'
Or use an existing PostgreSQL server:
sudo ./install.sh --sql-connection 'Host=...;Port=5432;Database=DispatchLog;Username=...;Password=...' --admin-password '<DashboardPassword!>'

Then open https://localhost:8420 (self-signed cert) and log in with the admin password you set.
EOF
Expand Down Expand Up @@ -311,7 +310,7 @@ jobs:
# Only runs for real tag releases (a dry-run workflow_dispatch skips the push).
docker:
if: ${{ github.ref_type == 'tag' }}
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5

Expand Down Expand Up @@ -352,7 +351,7 @@ jobs:

publish:
needs: [windows, linux]
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:

jobs:
lint:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5
- name: Parse + analyze PowerShell
Expand Down Expand Up @@ -45,7 +45,7 @@ jobs:
Write-Host 'OK: PowerShell parses and lints clean.'

pester:
runs-on: windows-latest
runs-on: [self-hosted, windows, x64]
steps:
- uses: actions/checkout@v5
- name: Pester (Hyper-V import logic, cmdlets mocked)
Expand Down
Loading
Loading