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 }