Files
fusion/.github/workflows/desktop-windows.yml
gsxdsm 95e011f890 fix(core): auto-repair empty non-UTF-8 embedded Postgres clusters on boot (#2286)
Users whose embedded cluster was initdb'd with an OS-locale encoding by
a pre-fix version now self-heal with zero manual steps: on the
encoding-conversion schema failure the startup factory proves the
cluster is non-UTF-8 AND empty (the baseline transaction never applied,
so no schema or migrated data can exist) and that this process owns the
postmaster, then deletes the data dir and reboots once with the UTF-8
initdb defaults. Joined instances and unproven states keep the manual
re-init hint; one retry ever, so no loops.

Verified on the elevated windows-latest runner: CI seeds a real WIN1252
cluster via initdb and proves a stock 'fn serve' auto-recovers it to a
healthy /api/health (run 29633351848, all jobs green). Also caps the
desktop-windows embedded-PG smoke at 30 min and adds a skip input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:18:40 -07:00

224 lines
12 KiB
YAML

name: Desktop Windows Build
on:
workflow_dispatch:
inputs:
skip_pg_smoke:
description: "Skip the embedded-PG smoke (already covered by verify-elevated-restricted.yml)"
type: boolean
default: false
jobs:
build-windows-exe:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# FNXC:WindowsDesktopPackaging 2026-07-15-00:55:
# The embedded-PG smoke boots postgres under a non-admin helper user
# (fusion-pg). The FIRST Start-Process -Credential for that user loads its
# Windows profile hive (~10-20s), which would blow a test's 15s budget.
# Create the user and warm its profile once here, outside any test window;
# the launcher resets the user's password before each run, but the warmed
# profile persists, so every later launch is ~0.5s.
# FNXC:WindowsDesktopPackaging 2026-07-18-01:40:
# The smoke wedged a runner for 45+ min with Start-Process -Wait and no
# step timeout (baseline ~18 min). Cap it, and allow skipping it via
# dispatch input when the dedicated verify-elevated-restricted workflow
# already proves embedded PG on this ref — packaging does not depend on it.
- name: Prewarm embedded-PG helper user profile
if: ${{ !inputs.skip_pg_smoke }}
shell: pwsh
run: |
$user = "fusion-pg"
# Throwaway password for the ephemeral helper user (the launcher resets
# it before each run); generated at runtime to avoid a hardcoded literal.
$pass = "Fx9!" + ([guid]::NewGuid().ToString("N")) + "#kP"
net user $user $pass /add /y 2>&1 | Out-Null
$sec = ConvertTo-SecureString $pass -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("$env:COMPUTERNAME\$user", $sec)
[void](Start-Process -FilePath cmd.exe -ArgumentList '/c','exit' -Credential $cred -Wait -WindowStyle Hidden)
Write-Host "prewarmed $user profile"
# FNXC:DesktopEmbeddedPostgres 2026-07-14-09:39:
# The manual Windows installer path must boot the same embedded database
# payload used by Local mode before it can publish an installer artifact.
# FNXC:WindowsDesktopPackaging 2026-07-15-02:40:
# The runner executes jobs elevated, and PostgreSQL refuses an elevated
# (admin) token. Run the WHOLE smoke AS the non-admin helper user
# (fusion-pg): the test process, its tmpdir() data dirs, AND postgres all
# run as fusion-pg, so postgres inherits a non-admin token and boots via
# the normal embedded-postgres path — no in-launcher Start-Process
# -Credential / staging / process-kill races.
- name: Smoke embedded Postgres on Windows
if: ${{ !inputs.skip_pg_smoke }}
timeout-minutes: 30
shell: pwsh
run: |
$user = "fusion-pg"
$pass = "Fx9!" + ([guid]::NewGuid().ToString("N")) + "#kP"
net user $user $pass /y 2>&1 | Out-Null
# FNXC:WindowsDesktopPackaging 2026-07-15-11:25:
# Full recursive grants on the workspace + pnpm store (proven green on
# win-pg-diag). Narrow grants miss pnpm resolution targets and exit 1
# with no useful signal. Capture the bat log so failures surface.
Write-Host "granting ACL (workspace + tooling) for $user..."
icacls $env:GITHUB_WORKSPACE /grant "*S-1-5-32-545:(OI)(CI)M" /T /C 2>&1 | Out-Null
if (Test-Path D:\.pnpm-store) {
icacls D:\.pnpm-store /grant "*S-1-5-32-545:(OI)(CI)RX" /T /C 2>&1 | Out-Null
}
icacls C:\Users\runneradmin /grant "*S-1-5-32-545:RX" /C 2>&1 | Out-Null
if (Test-Path C:\Users\runneradmin\setup-pnpm) {
icacls C:\Users\runneradmin\setup-pnpm /grant "*S-1-5-32-545:(OI)(CI)RX" /T /C 2>&1 | Out-Null
}
$nodeDir = Split-Path (Get-Command node).Source -Parent
icacls $nodeDir /grant "*S-1-5-32-545:(OI)(CI)RX" /T /C 2>&1 | Out-Null
# Traversable HOME/TEMP for the helper user (its tmpdir() lands here).
$h = "C:\fusionpg-home"
New-Item -ItemType Directory -Force -Path "$h\tmp" | Out-Null
icacls $h /grant "*S-1-5-32-545:(OI)(CI)F" /T /C 2>&1 | Out-Null
$pnpmDir = Split-Path (Get-Command pnpm).Source -Parent
$bat = Join-Path $h "smoke.bat"
$log = Join-Path $h "smoke.log"
Set-Content -Path $bat -Encoding ASCII -Value @(
"@echo off",
"set `"USERPROFILE=$h`"",
"set `"APPDATA=$h\AppData\Roaming`"",
"set `"LOCALAPPDATA=$h\AppData\Local`"",
"set `"TEMP=$h\tmp`"",
"set `"TMP=$h\tmp`"",
"set `"PATH=$nodeDir;$pnpmDir;%PATH%`"",
"cd /d $env:GITHUB_WORKSPACE",
"call pnpm --filter @fusion/core test:embedded-postgres > `"$log`" 2>&1",
"exit /b %ERRORLEVEL%"
)
Write-Host "running embedded-PG smoke as $user..."
$sec = ConvertTo-SecureString $pass -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("$env:COMPUTERNAME\$user", $sec)
$p = Start-Process -FilePath "cmd.exe" -ArgumentList '/c',$bat -Credential $cred -Wait -PassThru -WindowStyle Hidden
if (Test-Path $log) {
Write-Host "----- smoke.log (tail) -----"
Get-Content $log -Tail 200
} else {
Write-Host "smoke.log missing (bat may not have started)"
}
if ($p.ExitCode -ne 0) { Write-Error "embedded-PG smoke failed (exit $($p.ExitCode))"; exit 1 }
# FNXC:WindowsDesktopPackaging 2026-07-01-19:45:
# Mirror release.yml: build every workspace package's tsc dist (incl.
# @fusion/core and @fusion/engine, which are gitignored) before packaging.
# Without this the embedded Local runtime's `import("@fusion/engine")`
# resolves to an empty dist and the app crashes with ERR_MODULE_NOT_FOUND.
# `@fusion/desktop build` now also self-builds these, so this is belt-and-
# suspenders parity that additionally covers any other workspace runtime dep.
- name: Build workspace
run: pnpm build
- name: Build desktop package
run: pnpm --filter @fusion/desktop build
# Code-signing hardening is intentionally deferred to FN-5592.
- name: Package signed Windows EXE
if: ${{ env.WINDOWS_CERTIFICATE_BASE64 != '' }}
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --publish never
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package unsigned Windows EXE
if: ${{ env.WINDOWS_CERTIFICATE_BASE64 == '' }}
run: pnpm --filter @fusion/desktop exec electron-builder --projectDir deploy --win --x64 --publish never
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify signed artifacts
if: ${{ env.WINDOWS_CERTIFICATE_BASE64 != '' }}
shell: pwsh
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
run: |
$exes = Get-ChildItem packages/desktop/dist-electron -Filter *.exe
if ($exes.Count -eq 0) { Write-Error "No EXE artifacts produced"; exit 1 }
foreach ($exe in $exes) {
$sig = Get-AuthenticodeSignature $exe.FullName
Write-Host "$($exe.Name): $($sig.Status)"
if ($sig.Status -ne 'Valid') { Write-Error "Signature invalid: $($exe.Name) ($($sig.Status))"; exit 1 }
}
- name: Verify Windows runtime resources
shell: pwsh
run: |
# FNXC:WindowsDesktopPackaging 2026-07-01-08:08:
# The Windows app must install Electron's root .pak runtime resources;
# missing chrome_100_percent.pak, chrome_200_percent.pak, or resources.pak
# leaves Fusion.exe unable to start even when the NSIS installer succeeds.
$requiredResources = @('chrome_100_percent.pak', 'chrome_200_percent.pak', 'resources.pak')
$unpackedRoots = Get-ChildItem packages/desktop/dist-electron -Directory -Filter 'win*-unpacked'
if ($unpackedRoots.Count -eq 0) { Write-Error "No win-unpacked directory produced"; exit 1 }
foreach ($root in $unpackedRoots) {
foreach ($resource in $requiredResources) {
$resourcePath = Join-Path $root.FullName $resource
if (!(Test-Path $resourcePath)) { Write-Error "Missing Electron runtime resource: $resourcePath"; exit 1 }
}
}
$nsis = Get-ChildItem packages/desktop/dist-electron -Filter 'Fusion-*-win-*.exe' | Where-Object { $_.Name -notmatch '-portable\.exe$' }
$portable = Get-ChildItem packages/desktop/dist-electron -Filter 'Fusion-*-win-*-portable.exe'
if ($nsis.Count -eq 0) { Write-Error "No NSIS installer artifact produced"; exit 1 }
if ($portable.Count -eq 0) { Write-Error "No portable EXE artifact produced"; exit 1 }
- name: Verify packaged app.asar assets
shell: pwsh
run: |
# FNXC:WindowsDesktopPackaging 2026-07-03-15:40:
# Field report Issue 5: the packaged desktop shipped without preload.js and
# dead-ended on "can't reach the Fusion backend" (preload absence is silent —
# the contextBridge never installs window.fusionShell/fusionAPI). scripts/build.ts
# verifies the pre-package staging tree; this asserts the SHIPPED app.asar itself
# contains the Electron main/preload/renderer entrypoints, since only the packed
# asar reflects what a user installs.
$required = @('dist/main.js', 'dist/preload.js', 'dist/client/index.html')
$unpackedRoots = Get-ChildItem packages/desktop/dist-electron -Directory -Filter 'win*-unpacked'
if ($unpackedRoots.Count -eq 0) { Write-Error "No win-unpacked directory produced"; exit 1 }
foreach ($root in $unpackedRoots) {
$asar = Join-Path $root.FullName 'resources/app.asar'
if (!(Test-Path $asar)) { Write-Error "Missing packaged app.asar: $asar"; exit 1 }
$entries = npx --yes @electron/asar list $asar
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to list app.asar: $asar"; exit 1 }
$normalized = $entries | ForEach-Object { $_.TrimStart('/','\').Replace('\','/') }
foreach ($asset in $required) {
if ($normalized -notcontains $asset) {
Write-Error "app.asar is missing required Electron asset '$asset' in $($root.Name); refusing to ship an incomplete package"
exit 1
}
}
Write-Host "$($root.Name)/resources/app.asar contains all required Electron assets"
}
# Automated publish is intentionally deferred to FN-5593.
# Keep a single artifact; filenames include -x64 / -arm64 so both arches are captured.
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: fusion-desktop-windows
path: |
packages/desktop/dist-electron/*.exe
packages/desktop/dist-electron/*.blockmap