diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..9d58daf9 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "vpk": { + "version": "1.2.0", + "commands": [ + "vpk" + ], + "rollForward": false + } + } +} diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index b3d2a0ee..549bac0a 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -96,6 +96,11 @@ jobs: path: | artifacts/*.zip artifacts/*.sha256 + artifacts/velopack/OpenNest-win-Setup.exe + artifacts/velopack/OpenNest-win-Portable.zip + artifacts/velopack/*-full.nupkg + artifacts/velopack/releases.win.json + artifacts/velopack/RELEASES if-no-files-found: error retention-days: 14 compression-level: 0 diff --git a/AGENTS.md b/AGENTS.md index 751d1e90..b0d34992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,6 @@ Read the relevant contract before changing its behavior: - [Cut-time estimates](docs/cut-time-estimates.md): simulator/Calculate Cut Time acceleration planner and placeholder machine limits - [Cincinnati CL](docs/cincinnati-post-output.md) and [CI Fiber](docs/cincinnati-ci-fiber-post-output.md) - [Fill verification](docs/performance/fill-verification.md): opt-in benchmarks, frozen oracles, Debug-only counters and predictor initialization. Zero Release counters do not prove work removal. -- [Desktop auto-update](docs/auto-update.md): Velopack feed, `UpdateFeedUrl` setting and `Deploy-OpenNest.ps1`. +- [Desktop auto-update](docs/auto-update.md): GitHub Releases by default, `UpdateFeedUrl` for a self-hosted feed, release Velopack assets and `Deploy-OpenNest.ps1`. - [Undo and Redo](docs/undo-redo.md): whole-nest history, the recording rule, what a restore rebuilds and what is excluded. - [Desktop help](docs/desktop-help.md): WebView2 help window, F1 topic IDs and the pinned help release both packaging scripts install. diff --git a/OpenNest/Forms/OptionsForm.Designer.cs b/OpenNest/Forms/OptionsForm.Designer.cs index 0a1f6ec6..860d0759 100644 --- a/OpenNest/Forms/OptionsForm.Designer.cs +++ b/OpenNest/Forms/OptionsForm.Designer.cs @@ -267,7 +267,7 @@ this.updateGroupBox.Size = new System.Drawing.Size(684, 60); this.updateGroupBox.TabIndex = 3; this.updateGroupBox.TabStop = false; - this.updateGroupBox.Text = "Auto-update feed URL (blank disables update checks; set per machine)"; + this.updateGroupBox.Text = "Auto-update feed URL (blank uses GitHub Releases; set per machine)"; // // updateFeedUrlBox // diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs index 9819ce28..b035ddbf 100644 --- a/OpenNest/Forms/OptionsForm.cs +++ b/OpenNest/Forms/OptionsForm.cs @@ -252,14 +252,14 @@ namespace OpenNest.Forms Settings.Default.UpdateFeedUrl = ""; Settings.Default.Save(); updateFeedUrlBox.Text = ""; - MessageBox.Show(this, "Update feed URL cleared. Update checks are disabled.", + MessageBox.Show(this, "Update feed URL cleared. Updates will come from GitHub Releases after a restart.", "Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (!NestStorageSettings.TryNormalizeServerUrl(text, out var url)) { - MessageBox.Show(this, "Enter a valid http or https feed URL, or clear the field to disable updates.", + MessageBox.Show(this, "Enter a valid http or https feed URL, or clear the field to update from GitHub Releases.", "Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } diff --git a/OpenNest/Properties/Settings.Designer.cs b/OpenNest/Properties/Settings.Designer.cs index 56a0d0ef..601d0461 100644 --- a/OpenNest/Properties/Settings.Designer.cs +++ b/OpenNest/Properties/Settings.Designer.cs @@ -296,8 +296,8 @@ namespace OpenNest.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - // Velopack update feed base URL (e.g. http://host:port). Blank disables - // update checks; each machine sets its own value via Options, never committed. + // Velopack update feed base URL (e.g. http://host:port). Blank uses GitHub + // Releases; each machine sets its own value via Options, never committed. public string UpdateFeedUrl { get { return ((string)(this["UpdateFeedUrl"])); diff --git a/OpenNest/UpdaterService.cs b/OpenNest/UpdaterService.cs index 90abcf76..102e71fd 100644 --- a/OpenNest/UpdaterService.cs +++ b/OpenNest/UpdaterService.cs @@ -15,14 +15,15 @@ namespace OpenNest /// once the user agrees to restart. /// /// - /// The feed URL is never hard-coded: it is read from the per-machine - /// setting (Options dialog), which is - /// user-scoped and lives outside source control. A blank URL disables - /// update checks entirely, which is the default for every install. + /// A blank (the default) checks the + /// public GitHub Releases for full, non-pre-release versions. A URL set in + /// the Options dialog points this machine at a self-hosted feed instead; + /// that URL is operator infrastructure and never lives in source control. /// public sealed class UpdaterService : IDisposable { private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(30); + private const string GitHubRepositoryUrl = "https://github.com/ajisaacs/OpenNest"; private readonly UpdateManager _manager; private Timer _timer; @@ -36,10 +37,10 @@ namespace OpenNest public UpdaterService(string feedUrl) { - if (string.IsNullOrWhiteSpace(feedUrl)) - return; - - _manager = new UpdateManager(new SimpleWebSource(feedUrl)); + IUpdateSource source = string.IsNullOrWhiteSpace(feedUrl) + ? new GithubSource(GitHubRepositoryUrl, null, false) + : new SimpleWebSource(feedUrl); + _manager = new UpdateManager(source); } /// True once a downloaded update is waiting for . diff --git a/README.md b/README.md index 1421de6c..531f5897 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A Windows desktop application for CNC nesting — imports DXF drawings, arranges - Windows 10+ for the desktop app; the console, API, shared desktop library (`OpenNest.Desktop`) and most test projects build on Linux/macOS too. `OpenNest.Avalonia` is an early cross-platform desktop prototype, not yet a usable replacement. - [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build from source. -Prebuilt Windows downloads are on [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), or [build from source](#build-test-run) to run the latest code. Release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. To update, close OpenNest and extract the new release into a new folder; ZIP releases do not auto-update (optional installer-based updates need a separately hosted [Velopack feed](docs/auto-update.md)). All nesting engines (Default, Rectangles, Irregular, Circles, FlatBar and the fill strategies) are built in; see [nesting engines](docs/nesting-engines.md). +Prebuilt Windows downloads are on [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), or [build from source](#build-test-run) to run the latest code. Release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. For automatic updates, install with `OpenNest-win-Setup.exe` or extract `OpenNest-win-Portable.zip` instead: those copies download new full releases from GitHub in the background and restart only when you agree ([auto-update](docs/auto-update.md)). The plain ZIP does not auto-update; to update it, close OpenNest and extract the new release into a new folder. All nesting engines (Default, Rectangles, Irregular, Circles, FlatBar and the fill strategies) are built in; see [nesting engines](docs/nesting-engines.md). ## Build, Test, Run diff --git a/docs/auto-update.md b/docs/auto-update.md index 5da6af78..b2584f3d 100644 --- a/docs/auto-update.md +++ b/docs/auto-update.md @@ -1,32 +1,49 @@ # Desktop auto-update The Windows desktop app (`OpenNest.exe`) can self-update via -[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls a static -file feed every 30 minutes, downloads a newer release in the background, and +[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls its update +source every 30 minutes, downloads a newer release in the background, and only restarts to apply it after the user confirms (a clickable status-bar item appears once a download finishes). It never force-restarts; an open, unsaved nest is never discarded automatically. -## Configuring the feed +## Where updates come from -The feed URL is saved for the current Windows user -(`OpenNest.Properties.Settings.UpdateFeedUrl`), set from **Tools > Options > -Auto-update feed URL**. It is blank by default, which disables update checks -entirely — this is intentional: the feed URL is operator infrastructure, not -something that belongs in source control, so no default ships in the repo. -Enter a feed's base URL (`http://host:port`) and restart the app to pick it -up. +By default an installed copy checks the public +[GitHub Releases](https://github.com/ajisaacs/OpenNest/releases) for full, +non-pre-release versions (`GithubSource`, unauthenticated: GitHub allows 60 API +requests per hour per IP address, ample for a 30-minute poll but shared by every +machine behind one address). Pre-releases are never offered. -Velopack only applies updates to an installed copy (`UpdateManager.IsInstalled`): -one installed with the `Setup.exe` that `Deploy-OpenNest.ps1` produces. The ZIPs on -GitHub Releases and ordinary build output are not Velopack installations and never -auto-update, even with a feed URL configured. +To use a self-hosted feed instead, set **Tools > Options > Auto-update feed URL** +(`OpenNest.Properties.Settings.UpdateFeedUrl`, saved for the current Windows +user) to the feed's base URL (`http://host:port`) and restart. A self-hosted +feed URL is operator infrastructure and never belongs in source control; clearing +the field returns to GitHub Releases. + +Velopack only applies updates to a Velopack installation (`UpdateManager.IsInstalled`): +a copy installed with `OpenNest-win-Setup.exe` or extracted from +`OpenNest-win-Portable.zip`. The plain `OpenNest.vX.Y.Z.win-x64.zip` and ordinary +build output contain no `Update.exe` and never auto-update; moving from one of +them to an updating copy is a one-time manual install. + +## Release packages + +`scripts/Publish-Windows.ps1` packs the same smoke-tested folder as the plain ZIP +with the `vpk` CLI pinned in `.config/dotnet-tools.json` (keep it equal to the +`Velopack` package version), into `velopack/`: `OpenNest-win-Setup.exe`, +`OpenNest-win-Portable.zip`, `OpenNest-X.Y.Z-full.nupkg`, `releases.win.json` +and `RELEASES`, with their hashes in `OpenNest.vX.Y.Z.velopack.sha256`. Installed +copies find an update through the `releases.win.json` and full package attached to +the newest full GitHub Release, so all five must be uploaded with it (see +[releasing](releasing.md#publish)). Packages are full only; delta packages would +need the previous release's packages beside the output before `vpk pack`. ## Hosting a feed Any static file server that can serve a `vpk pack` output directory works. `scripts/Deploy-OpenNest.ps1` builds a self-contained `win-x64` publish, -packs it with the `vpk` CLI (`dotnet tool install -g vpk`), and `scp`s the +packs it with the pinned `vpk` CLI, and `scp`s the result to a directory you provide: ```powershell @@ -36,4 +53,5 @@ result to a directory you provide: Bump `` in `OpenNest/OpenNest.csproj` before each run. This is independent of [the GitHub release process](releasing.md) and of `Publish-Windows.ps1` — the latter always overrides `` via -`-p:Version`, so the two don't conflict. +`-p:Version`, so the two don't conflict. Machines on a self-hosted feed only see +versions pushed to that feed. diff --git a/docs/releasing.md b/docs/releasing.md index 25f43418..f5d084f2 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -27,7 +27,8 @@ pulls from GitHub every hour; it refuses pushes. (see [desktop help](desktop-help.md)), and `build-info.json` identifying the exact source commit and help version. The nesting engines are built into `OpenNest.Engine.dll`; no external engine repository or plug-in - DLL is packaged. `scripts/ReleaseSmoke` loads the packaged engine assembly and + DLL is packaged. Its `velopack/` folder holds the self-updating packages built + from the same folder; see [desktop auto-update](auto-update.md#release-packages). `scripts/ReleaseSmoke` loads the packaged engine assembly and checks that every built-in engine instantiates from it, that the renamed `Opus55NestingEngine` selection resolves to Irregular, and that an unknown engine name is rejected. @@ -54,6 +55,11 @@ After the candidate passes, integrate the release tooling into the chosen branch create an annotated `vX.Y.Z` tag on the exact release commit and push it to GitHub. Use the successful **tag build's** artifacts for the GitHub Release; do not substitute packages built from another commit. +Upload the plain ZIP, its `.sha256`, `OpenNest.vX.Y.Z.velopack.sha256` and all five +files in `velopack/` as release assets, keeping their names. Installed copies update +only from the newest full (non-pre-release) Release that carries +`releases.win.json` and its `-full.nupkg`; a full Release without them leaves every +installation on its current version. Verify the public asset names, sizes, download/checksum, and release status after upload. Do not overwrite an existing release/tag or asset silently. diff --git a/scripts/Deploy-OpenNest.ps1 b/scripts/Deploy-OpenNest.ps1 index bcf6a691..fc06f4b4 100644 --- a/scripts/Deploy-OpenNest.ps1 +++ b/scripts/Deploy-OpenNest.ps1 @@ -42,12 +42,10 @@ if ($csprojContent -notmatch '(?[\d.]+)') { throw "Could n $Version = $Matches.v Write-Host "Packaging OpenNest v$Version" -# Ensure the vpk (Velopack) CLI is available -if (-not (Get-Command vpk -ErrorAction SilentlyContinue)) { - Write-Host "Installing vpk CLI (dotnet tool)..." - dotnet tool install -g vpk - if ($LASTEXITCODE -ne 0) { throw "dotnet tool install vpk failed" } -} +# The vpk (Velopack) CLI is pinned in .config/dotnet-tools.json to match the Velopack package +Push-Location $RepoRoot +try { & dotnet tool restore } finally { Pop-Location } +if ($LASTEXITCODE -ne 0) { throw "dotnet tool restore failed" } $PublishDir = Join-Path $RepoRoot 'OpenNest\bin\publish-tmp' $ReleasesDir = Join-Path $RepoRoot 'OpenNest\bin\releases' @@ -61,7 +59,8 @@ $helpVersion = & (Join-Path $ScriptDir 'Install-Help.ps1') -Destination (Join-Pa Write-Host "Packaged help $helpVersion" Write-Host "Packing Velopack release v$Version..." -& vpk pack -u OpenNest -v $Version -p $PublishDir -e OpenNest.exe -o $ReleasesDir +Push-Location $RepoRoot +try { & dotnet tool run vpk pack -u OpenNest -v $Version -p $PublishDir -e OpenNest.exe -o $ReleasesDir } finally { Pop-Location } if ($LASTEXITCODE -ne 0) { throw "vpk pack failed with exit code $LASTEXITCODE" } Write-Host "Pushing release feed to $FeedHost..." diff --git a/scripts/Publish-Windows.ps1 b/scripts/Publish-Windows.ps1 index eaa84702..15de46af 100644 --- a/scripts/Publish-Windows.ps1 +++ b/scripts/Publish-Windows.ps1 @@ -114,5 +114,32 @@ try { $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 }