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>
224 lines
12 KiB
YAML
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
|