Files
OpenNest/scripts/Publish-Windows.ps1
T
ajandClaude Opus 5.5 7fe4de4b6d feat(release): ship self-updating Velopack packages on GitHub Releases
Publish-Windows.ps1 now packs the smoke-tested folder with the pinned vpk
CLI (.config/dotnet-tools.json, 1.2.0 to match the Velopack package) into
velopack/: Setup.exe, Portable.zip, the full nupkg, releases.win.json and
RELEASES, checked for the full package and the portable layout and hashed
into OpenNest.vX.Y.Z.velopack.sha256. The release workflow uploads them
with the candidate.

With no feed URL set, installed copies now check GitHub Releases (full
releases only) instead of skipping update checks; a URL in Options still
selects a self-hosted feed. Deploy-OpenNest.ps1 uses the pinned vpk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:52:30 -04:00

146 lines
7.8 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$')]
[string] $Version,
[string] $OutputDirectory = 'artifacts',
# A built opennest-help site/ folder to package instead of the release pinned in help.json.
[string] $HelpDirectory
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
Set-StrictMode -Version Latest
if (-not $IsWindows) { throw 'Packaging includes a desktop smoke test and requires Windows.' }
$root = Split-Path $PSScriptRoot -Parent
Push-Location $root
try {
$output = [IO.Path]::GetFullPath($OutputDirectory)
if (Test-Path $output) { throw "Refusing to overwrite existing output: $output" }
$commit = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve source commit.' }
New-Item -ItemType Directory -Path $output | Out-Null
$package = Join-Path $output 'OpenNest'
$common = @('-c', 'Release', '-r', 'win-x64', "-p:Version=$Version", '-p:DebugType=None', '-p:DebugSymbols=false')
dotnet publish OpenNest/OpenNest.csproj @common --self-contained true -o $package
if ($LASTEXITCODE -ne 0) { throw 'Desktop publish failed.' }
# Build hooks copy to bin/, not PublishDir. Explicitly package every shipped post.
$posts = Join-Path $package 'Posts'
New-Item -ItemType Directory -Path $posts -Force | Out-Null
foreach ($name in @('Cincinnati', 'CincinnatiCIFiber', 'GravographIS')) {
$project = "OpenNest.Posts.$name"
$staging = Join-Path $output "post-build/$name"
dotnet publish "Posts/$project/$project.csproj" @common --self-contained false -o $staging
if ($LASTEXITCODE -ne 0) { throw "$project publish failed." }
Copy-Item "$staging/$project.dll" $posts
if (Test-Path "$staging/$project.json") { Copy-Item "$staging/$project.json" $posts }
# Gravograph's serial-port runtime is not in the desktop project's dependency graph.
if ($name -eq 'GravographIS') { Copy-Item "$staging/System.IO.Ports.dll" $package }
}
# Help ships as the opennest-help release pinned in help.json, verified against its hash.
$helpVersion = & (Join-Path $PSScriptRoot 'Install-Help.ps1') -Destination (Join-Path $package 'Help') -HelpDirectory $HelpDirectory
New-Item -ItemType Directory -Path (Join-Path $package 'Schemes') -Force | Out-Null
Copy-Item LICENSE $package
@{
version = $Version
sourceCommit = $commit
runtime = 'win-x64'
selfContained = $true
help = $helpVersion
} | ConvertTo-Json | Set-Content (Join-Path $package 'build-info.json') -Encoding utf8NoBOM
$required = @(
'OpenNest.exe', 'OpenNest.dll', 'OpenNest.Core.dll', 'OpenNest.Engine.dll',
'OpenNest.runtimeconfig.json', 'coreclr.dll', 'System.Windows.Forms.dll',
'onnxruntime.dll', 'System.IO.Ports.dll', 'Configurations/PipeFlangeShape.json',
'Posts/OpenNest.Posts.Cincinnati.dll', 'Posts/OpenNest.Posts.Cincinnati.json',
'Posts/OpenNest.Posts.CincinnatiCIFiber.dll', 'Posts/OpenNest.Posts.CincinnatiCIFiber.json',
'Posts/OpenNest.Posts.GravographIS.dll', 'Help/index.html', 'Help/topics.json',
'LICENSE', 'build-info.json'
)
foreach ($file in $required) {
if (-not (Test-Path (Join-Path $package $file) -PathType Leaf)) { throw "Package missing $file" }
}
$assemblyVersion = [Reflection.AssemblyName]::GetAssemblyName((Join-Path $package 'OpenNest.dll')).Version
if ($assemblyVersion.ToString() -ne "$Version.0") { throw "Wrong assembly version: $assemblyVersion" }
$runtime = Get-Content (Join-Path $package 'OpenNest.runtimeconfig.json') -Raw | ConvertFrom-Json
if (-not $runtime.runtimeOptions.includedFrameworks) { throw 'Package is not self-contained.' }
$zipName = "OpenNest.v$Version.win-x64.zip"
$zip = Join-Path $output $zipName
Compress-Archive -Path "$package/*" -DestinationPath $zip
# Exercise the actual ZIP, not the build tree. This checks startup and post discovery,
# not interactive CAD workflows or machine/serial/GPU operation.
$expanded = Join-Path $output 'smoke-test'
Expand-Archive -Path $zip -DestinationPath $expanded
foreach ($file in $required) {
if (-not (Test-Path (Join-Path $expanded $file) -PathType Leaf)) { throw "ZIP missing $file" }
}
# Built-in engines ship inside OpenNest.Engine.dll: check the packaged registry, not the build tree.
$smokeProject = Join-Path $PSScriptRoot 'ReleaseSmoke/ReleaseSmoke.csproj'
dotnet build $smokeProject -c Release
if ($LASTEXITCODE -ne 0) { throw 'Engine registry checker build failed.' }
dotnet run --project $smokeProject -c Release --no-build -- $expanded
if ($LASTEXITCODE -ne 0) { throw 'Packaged engine registry check failed.' }
$process = Start-Process (Join-Path $expanded 'OpenNest.exe') -WorkingDirectory $expanded -PassThru
try {
if (-not $process.WaitForInputIdle(30000)) { throw 'Desktop did not become idle within 30 seconds.' }
$deadline = [DateTime]::UtcNow.AddSeconds(15)
do {
$process.Refresh()
if ($process.HasExited) { throw "Desktop exited during startup: $($process.ExitCode)" }
if ($process.MainWindowHandle -ne 0 -and $process.MainWindowTitle -match '^OpenNest(?: - \[.*\])?$') { break }
Start-Sleep -Milliseconds 250
} while ([DateTime]::UtcNow -lt $deadline)
if ($process.MainWindowHandle -eq 0 -or $process.MainWindowTitle -notmatch '^OpenNest(?: - \[.*\])?$') {
throw "Expected OpenNest main window, found '$($process.MainWindowTitle)'."
}
Write-Host 'PASS: packaged OpenNest main window opened on Windows.'
}
finally {
if (-not $process.HasExited) {
$null = $process.CloseMainWindow()
if (-not $process.WaitForExit(5000)) { $process.Kill(); $process.WaitForExit() }
}
$process.Dispose()
}
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $zipName" | Set-Content "$zip.sha256" -Encoding ascii
Write-Host "Verified $zipName ($hash), source $commit"
# Velopack packages of the same verified folder: Setup.exe and the portable ZIP
# self-update from GitHub Releases; the plain ZIP above never does.
dotnet tool restore
if ($LASTEXITCODE -ne 0) { throw 'dotnet tool restore failed.' }
$velopack = Join-Path $output 'velopack'
dotnet tool run vpk pack -u OpenNest -v $Version -p $package -e OpenNest.exe -o $velopack --packTitle OpenNest
if ($LASTEXITCODE -ne 0) { throw 'vpk pack failed.' }
$assets = @('OpenNest-win-Setup.exe', 'OpenNest-win-Portable.zip', "OpenNest-$Version-full.nupkg", 'releases.win.json', 'RELEASES')
foreach ($file in $assets) {
if (-not (Test-Path (Join-Path $velopack $file) -PathType Leaf)) { throw "Velopack output missing $file" }
}
$feed = Get-Content (Join-Path $velopack 'releases.win.json') -Raw | ConvertFrom-Json
if (@($feed.Assets | Where-Object { $_.Version -eq $Version -and $_.Type -eq 'Full' }).Count -ne 1) {
throw "releases.win.json does not list the $Version full package."
}
$portable = Join-Path $output 'velopack-portable'
Expand-Archive -Path (Join-Path $velopack 'OpenNest-win-Portable.zip') -DestinationPath $portable
foreach ($file in @('Update.exe') + ($required | ForEach-Object { "current/$_" })) {
if (-not (Test-Path (Join-Path $portable $file) -PathType Leaf)) { throw "Portable ZIP missing $file" }
}
$sums = foreach ($file in $assets) {
"$((Get-FileHash (Join-Path $velopack $file) -Algorithm SHA256).Hash.ToLowerInvariant()) $file"
}
$sums | Set-Content (Join-Path $output "OpenNest.v$Version.velopack.sha256") -Encoding ascii
Write-Host "Packed Velopack release $Version"
}
finally { Pop-Location }