ci(release): bundle and verify external nesting engines
This commit is contained in:
@@ -24,7 +24,7 @@ NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO),
|
|||||||
|
|
||||||
### Windows release packaging
|
### Windows release packaging
|
||||||
|
|
||||||
The GitHub `Windows release build` workflow runs on `release/vX.Y.Z` branches, `vX.Y.Z` tags, or manual dispatch. It builds with .NET 8 on `windows-2022`, runs all four test projects in Release plus the main Debug suite, and executes `scripts/Publish-Windows.ps1`. The script creates a self-contained win-x64 desktop ZIP with all three shipped posts, validates its contents/version, launches the extracted app, and writes a SHA-256 checksum. It refuses an existing output directory. Workflow artifacts are release candidates, not automatically published releases; follow [the release procedure](docs/releasing.md). Keep Gitea authoritative for Git refs.
|
The GitHub `Windows release build` workflow runs on `release/vX.Y.Z` branches, `vX.Y.Z` tags, or manual dispatch. It builds with .NET 8 on `windows-2022`, runs all four test projects in Release plus the main Debug suite, and executes `scripts/Publish-Windows.ps1`. The script creates a self-contained win-x64 desktop ZIP with all three shipped posts plus pinned Gpt6Astra/Opus55/Qwen38FlashNext plug-ins from `scripts/external-engines.json`, runs their tests against this host, validates package contents/version and registry discovery (including a missing-DLL failure case), launches the extracted app, and writes a SHA-256 checksum. It refuses an existing output directory. Workflow artifacts are release candidates, not automatically published releases; follow [the release procedure](docs/releasing.md). Keep Gitea authoritative for Git refs.
|
||||||
|
|
||||||
### Fill performance verification
|
### Fill performance verification
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ A Windows desktop application for CNC nesting — imports DXF drawings, arranges
|
|||||||
- Windows 10+ for the desktop app; the console, API, and most test projects build on Linux/macOS too.
|
- Windows 10+ for the desktop app; the console, API, and most test projects build on Linux/macOS too.
|
||||||
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build from source.
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build from source.
|
||||||
|
|
||||||
Windows release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. Use the ZIP and SHA-256 checksum from [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), not the source-code archives.
|
Windows release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. The package includes the Gpt6Astra, Opus55, and Qwen38FlashNext engine plug-ins. Use the ZIP and SHA-256 checksum from [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), not the source-code archives.
|
||||||
|
|
||||||
## Build, Test, Run
|
## Build, Test, Run
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -19,7 +19,13 @@ Git refs are owned by Gitea (`aj/OpenNest`) and push-mirrored to GitHub
|
|||||||
4. Download the `OpenNest-X.Y.Z-win-x64` artifact and verify its `.sha256`.
|
4. Download the `OpenNest-X.Y.Z-win-x64` artifact and verify its `.sha256`.
|
||||||
It contains `OpenNest.vX.Y.Z.win-x64.zip`, with the .NET runtime, native
|
It contains `OpenNest.vX.Y.Z.win-x64.zip`, with the .NET runtime, native
|
||||||
dependencies, shipped configurations, all three post-processors, license,
|
dependencies, shipped configurations, all three post-processors, license,
|
||||||
and `build-info.json` identifying the exact source commit.
|
and `build-info.json` identifying the exact source commit. Gpt6Astra, Opus55,
|
||||||
|
and Qwen38FlashNext are bundled in `Engines/` with their MIT license and source
|
||||||
|
manifest. `scripts/external-engines.json` pins the external repository revision;
|
||||||
|
no moving branch or prebuilt third-party DLL is used. The script tests and
|
||||||
|
builds each engine against this host, then exercises actual packaged registry
|
||||||
|
discovery and an intentionally missing-DLL failure case. Keep the explicit
|
||||||
|
engine allowlist; never package the template, shared test kit, or test DLLs.
|
||||||
|
|
||||||
The workflow has read-only repository permissions and does **not** publish
|
The workflow has read-only repository permissions and does **not** publish
|
||||||
releases. Once present on the default branch, it can also be dispatched manually
|
releases. Once present on the default branch, it can also be dispatched manually
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$PSNativeCommandUseErrorActionPreference = $false
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
if (-not $IsWindows) { throw 'Packaging includes a desktop smoke test and requires Windows.' }
|
if (-not $IsWindows) { throw 'Packaging includes a desktop smoke test and requires Windows.' }
|
||||||
|
|
||||||
@@ -37,11 +38,41 @@ try {
|
|||||||
# Gravograph's serial-port runtime is not in the desktop project's dependency graph.
|
# 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 }
|
if ($name -eq 'GravographIS') { Copy-Item "$staging/System.IO.Ports.dll" $package }
|
||||||
}
|
}
|
||||||
|
$engineManifestPath = Join-Path $PSScriptRoot 'external-engines.json'
|
||||||
|
$engineManifest = Get-Content $engineManifestPath -Raw | ConvertFrom-Json
|
||||||
|
if ($engineManifest.commit -notmatch '^[0-9a-f]{40}$') { throw 'Engines must be pinned to a full commit SHA.' }
|
||||||
|
$engineSource = Join-Path $output 'external-engine-source'
|
||||||
|
git init $engineSource
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Cannot initialize engine checkout.' }
|
||||||
|
git -C $engineSource fetch --depth 1 $engineManifest.repository $engineManifest.commit
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Cannot fetch pinned engines.' }
|
||||||
|
git -C $engineSource checkout --detach FETCH_HEAD
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Cannot check out pinned engines.' }
|
||||||
|
$engineCommit = (git -C $engineSource rev-parse HEAD).Trim()
|
||||||
|
if ($LASTEXITCODE -ne 0 -or $engineCommit -ne $engineManifest.commit) { throw 'Engine source revision mismatch.' }
|
||||||
|
$engineDirectory = Join-Path $package 'Engines'
|
||||||
|
New-Item -ItemType Directory -Path $engineDirectory | Out-Null
|
||||||
|
# A global MSBuild property cannot be normalized by Directory.Build.props: retain the trailing slash.
|
||||||
|
$hostRoot = $root.Replace('\', '/') + '/'
|
||||||
|
foreach ($engine in $engineManifest.engines) {
|
||||||
|
$project = $engine.project
|
||||||
|
$projectDirectory = Join-Path $engineSource $project
|
||||||
|
dotnet test "$projectDirectory/tests/$project.Tests.csproj" -c Release "-p:OpenNestRoot=$hostRoot" "-p:Version=$Version" --logger "trx;LogFileName=$project.trx" --results-directory (Join-Path $root 'TestResults')
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "$project tests failed." }
|
||||||
|
$staging = Join-Path $output "engine-build/$project"
|
||||||
|
dotnet publish "$projectDirectory/$project.csproj" @common --self-contained false "-p:OpenNestRoot=$hostRoot" -o $staging
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "$project publish failed." }
|
||||||
|
Copy-Item "$staging/$project.dll" $engineDirectory
|
||||||
|
}
|
||||||
|
Copy-Item $engineManifestPath (Join-Path $engineDirectory 'manifest.json')
|
||||||
|
Copy-Item (Join-Path $engineSource 'LICENSE') (Join-Path $engineDirectory 'LICENSE.txt')
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path (Join-Path $package 'Schemes') -Force | Out-Null
|
New-Item -ItemType Directory -Path (Join-Path $package 'Schemes') -Force | Out-Null
|
||||||
Copy-Item LICENSE $package
|
Copy-Item LICENSE $package
|
||||||
@{
|
@{
|
||||||
version = $Version
|
version = $Version
|
||||||
sourceCommit = $commit
|
sourceCommit = $commit
|
||||||
|
enginesCommit = $engineCommit
|
||||||
runtime = 'win-x64'
|
runtime = 'win-x64'
|
||||||
selfContained = $true
|
selfContained = $true
|
||||||
} | ConvertTo-Json | Set-Content (Join-Path $package 'build-info.json') -Encoding utf8NoBOM
|
} | ConvertTo-Json | Set-Content (Join-Path $package 'build-info.json') -Encoding utf8NoBOM
|
||||||
@@ -52,8 +83,10 @@ try {
|
|||||||
'onnxruntime.dll', 'System.IO.Ports.dll', 'Configurations/PipeFlangeShape.json',
|
'onnxruntime.dll', 'System.IO.Ports.dll', 'Configurations/PipeFlangeShape.json',
|
||||||
'Posts/OpenNest.Posts.Cincinnati.dll', 'Posts/OpenNest.Posts.Cincinnati.json',
|
'Posts/OpenNest.Posts.Cincinnati.dll', 'Posts/OpenNest.Posts.Cincinnati.json',
|
||||||
'Posts/OpenNest.Posts.CincinnatiCIFiber.dll', 'Posts/OpenNest.Posts.CincinnatiCIFiber.json',
|
'Posts/OpenNest.Posts.CincinnatiCIFiber.dll', 'Posts/OpenNest.Posts.CincinnatiCIFiber.json',
|
||||||
'Posts/OpenNest.Posts.GravographIS.dll', 'LICENSE', 'build-info.json'
|
'Posts/OpenNest.Posts.GravographIS.dll', 'LICENSE', 'build-info.json',
|
||||||
|
'Engines/manifest.json', 'Engines/LICENSE.txt'
|
||||||
)
|
)
|
||||||
|
$required += $engineManifest.engines | ForEach-Object { "Engines/$($_.project).dll" }
|
||||||
foreach ($file in $required) {
|
foreach ($file in $required) {
|
||||||
if (-not (Test-Path (Join-Path $package $file) -PathType Leaf)) { throw "Package missing $file" }
|
if (-not (Test-Path (Join-Path $package $file) -PathType Leaf)) { throw "Package missing $file" }
|
||||||
}
|
}
|
||||||
@@ -73,6 +106,20 @@ try {
|
|||||||
foreach ($file in $required) {
|
foreach ($file in $required) {
|
||||||
if (-not (Test-Path (Join-Path $expanded $file) -PathType Leaf)) { throw "ZIP missing $file" }
|
if (-not (Test-Path (Join-Path $expanded $file) -PathType Leaf)) { throw "ZIP missing $file" }
|
||||||
}
|
}
|
||||||
|
$smokeProject = Join-Path $PSScriptRoot 'ReleaseSmoke/ReleaseSmoke.csproj'
|
||||||
|
dotnet build $smokeProject -c Release
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Engine discovery checker build failed.' }
|
||||||
|
# Verify silent plugin-load failure cannot be mistaken for a successful package.
|
||||||
|
$probe = Join-Path $expanded "Engines/$($engineManifest.engines[0].project).dll"
|
||||||
|
Move-Item $probe "$probe.disabled"
|
||||||
|
try {
|
||||||
|
dotnet run --project $smokeProject -c Release --no-build -- $expanded
|
||||||
|
if ($LASTEXITCODE -eq 0) { throw 'Missing-engine failure case was not detected.' }
|
||||||
|
}
|
||||||
|
finally { Move-Item "$probe.disabled" $probe }
|
||||||
|
dotnet run --project $smokeProject -c Release --no-build -- $expanded
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Packaged engine discovery failed.' }
|
||||||
|
|
||||||
$process = Start-Process (Join-Path $expanded 'OpenNest.exe') -WorkingDirectory $expanded -PassThru
|
$process = Start-Process (Join-Path $expanded 'OpenNest.exe') -WorkingDirectory $expanded -PassThru
|
||||||
try {
|
try {
|
||||||
if (-not $process.WaitForInputIdle(30000)) { throw 'Desktop did not become idle within 30 seconds.' }
|
if (-not $process.WaitForInputIdle(30000)) { throw 'Desktop did not become idle within 30 seconds.' }
|
||||||
@@ -80,10 +127,10 @@ try {
|
|||||||
do {
|
do {
|
||||||
$process.Refresh()
|
$process.Refresh()
|
||||||
if ($process.HasExited) { throw "Desktop exited during startup: $($process.ExitCode)" }
|
if ($process.HasExited) { throw "Desktop exited during startup: $($process.ExitCode)" }
|
||||||
if ($process.MainWindowHandle -ne 0 -and $process.MainWindowTitle -eq 'OpenNest') { break }
|
if ($process.MainWindowHandle -ne 0 -and $process.MainWindowTitle -match '^OpenNest(?: - \[.*\])?$') { break }
|
||||||
Start-Sleep -Milliseconds 250
|
Start-Sleep -Milliseconds 250
|
||||||
} while ([DateTime]::UtcNow -lt $deadline)
|
} while ([DateTime]::UtcNow -lt $deadline)
|
||||||
if ($process.MainWindowHandle -eq 0 -or $process.MainWindowTitle -ne 'OpenNest') {
|
if ($process.MainWindowHandle -eq 0 -or $process.MainWindowTitle -notmatch '^OpenNest(?: - \[.*\])?$') {
|
||||||
throw "Expected OpenNest main window, found '$($process.MainWindowTitle)'."
|
throw "Expected OpenNest main window, found '$($process.MainWindowTitle)'."
|
||||||
}
|
}
|
||||||
Write-Host 'PASS: packaged OpenNest main window opened on Windows.'
|
Write-Host 'PASS: packaged OpenNest main window opened on Windows.'
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (args.Length != 1)
|
||||||
|
throw new ArgumentException("Usage: ReleaseSmoke <extracted-package-directory>");
|
||||||
|
|
||||||
|
var package = Path.GetFullPath(args[0]);
|
||||||
|
// No project references: resolve against the extracted release, never the build tree.
|
||||||
|
AssemblyLoadContext.Default.Resolving += (_, name) =>
|
||||||
|
{
|
||||||
|
var path = Path.Combine(package, name.Name + ".dll");
|
||||||
|
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var engineAssembly = Assembly.LoadFrom(Path.Combine(package, "OpenNest.Engine.dll"));
|
||||||
|
var registry = engineAssembly.GetType("OpenNest.Engine.Jobs.NestingEngineRegistry", true)!;
|
||||||
|
var engineDirectory = Path.Combine(package, "Engines");
|
||||||
|
registry.GetMethod("LoadPlugins")!.Invoke(null, [engineDirectory]);
|
||||||
|
using var manifest = JsonDocument.Parse(File.ReadAllText(Path.Combine(engineDirectory, "manifest.json")));
|
||||||
|
foreach (var entry in manifest.RootElement.GetProperty("engines").EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = entry.GetProperty("registryName").GetString()!;
|
||||||
|
var expected = Path.Combine(engineDirectory, entry.GetProperty("project").GetString() + ".dll");
|
||||||
|
if (!File.Exists(expected))
|
||||||
|
throw new FileNotFoundException("Required engine missing", expected);
|
||||||
|
var engine = registry.GetMethod("Create")!.Invoke(null, [name])!;
|
||||||
|
if (!string.Equals(engine.GetType().Assembly.Location, expected, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException($"Engine {name} was not loaded from the release package.");
|
||||||
|
Console.WriteLine($"PASS: {name} loaded and instantiated from {expected}");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(exception);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"repository": "https://git.thecozycat.net/aj/OpenNest-Engines.git",
|
||||||
|
"commit": "33cc2ee810a9408a4e720eecd5be30271f87d720",
|
||||||
|
"engines": [
|
||||||
|
{ "project": "OpenNest.Engine.Gpt6Astra", "registryName": "Gpt6AstraNestingEngine" },
|
||||||
|
{ "project": "OpenNest.Engine.Opus55", "registryName": "Opus55NestingEngine" },
|
||||||
|
{ "project": "OpenNest.Engine.Qwen38FlashNext", "registryName": "Qwen38FlashNextNestingEngine" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user