mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 18:21:55 -04:00
feat(desktop): add Velopack auto-update support
Lets the desktop app self-update like CncSender, but only restarts after the user confirms via a clickable status-bar prompt -- an open, unsaved nest must never be discarded automatically. The feed URL is a blank-by- default, per-machine setting (File > Options), since it's operator infrastructure that doesn't belong in source control; Deploy-OpenNest.ps1 packages releases for whatever feed host/path the operator supplies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3bcc77145
commit
18d63ffd61
12 files changed
+344
-3
No files matched your search
@@ -62,3 +62,4 @@ Read the relevant contract before changing its behavior:
|
||||
- [Pre-post verification](docs/post-verification.md)
|
||||
- [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`.
|
||||
Generated
+2
@@ -20,6 +20,8 @@ namespace OpenNest.Forms
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
if (disposing)
|
||||
updater?.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,57 @@ namespace OpenNest.Forms
|
||||
|
||||
engineComboBox.SelectedItem = EngineSelection.EngineName;
|
||||
engineComboBox.SelectedIndexChanged += EngineComboBox_SelectedIndexChanged;
|
||||
|
||||
InitializeUpdater();
|
||||
}
|
||||
|
||||
private global::OpenNest.UpdaterService updater;
|
||||
private ToolStripStatusLabel updateStatusLabel;
|
||||
|
||||
/// <summary>
|
||||
/// Starts background update checks against the per-machine feed URL
|
||||
/// (Options dialog; blank disables checks). Never restarts on its
|
||||
/// own -- shows a clickable status item once an update is ready and
|
||||
/// restarts only when the user clicks it.
|
||||
/// </summary>
|
||||
private void InitializeUpdater()
|
||||
{
|
||||
updateStatusLabel = new ToolStripStatusLabel
|
||||
{
|
||||
Visible = false,
|
||||
ForeColor = Color.DarkGreen,
|
||||
Font = new Font(statusLabel1.Font, FontStyle.Bold),
|
||||
};
|
||||
updateStatusLabel.Click += (_, _) =>
|
||||
{
|
||||
if (!updater.HasPendingUpdate)
|
||||
return;
|
||||
|
||||
var result = MessageBox.Show(
|
||||
this,
|
||||
"An OpenNest update has been downloaded. Restart now to apply it? Any unsaved nest will be lost.",
|
||||
"Update ready",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question
|
||||
);
|
||||
if (result == DialogResult.Yes)
|
||||
updater.ApplyAndRestart();
|
||||
};
|
||||
statusStrip1.Items.Insert(statusStrip1.Items.IndexOf(gpuStatusLabel), updateStatusLabel);
|
||||
|
||||
updater = new global::OpenNest.UpdaterService();
|
||||
updater.UpdateReady += version =>
|
||||
{
|
||||
if (IsHandleCreated)
|
||||
BeginInvoke(new System.Action(() => ShowUpdateReady(version)));
|
||||
};
|
||||
updater.Start();
|
||||
}
|
||||
|
||||
private void ShowUpdateReady(string version)
|
||||
{
|
||||
updateStatusLabel.Text = $"Update {version} ready - click to restart";
|
||||
updateStatusLabel.Visible = true;
|
||||
}
|
||||
|
||||
private Nest CreateDefaultNest()
|
||||
|
||||
Generated
+42
-3
@@ -44,10 +44,14 @@
|
||||
this.serverGroupBox = new System.Windows.Forms.GroupBox();
|
||||
this.serverUrlBox = new System.Windows.Forms.TextBox();
|
||||
this.setServerButton = new System.Windows.Forms.Button();
|
||||
this.updateGroupBox = new System.Windows.Forms.GroupBox();
|
||||
this.updateFeedUrlBox = new System.Windows.Forms.TextBox();
|
||||
this.setUpdateFeedButton = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
|
||||
this.tableLayoutPanel1.SuspendLayout();
|
||||
this.bottomPanel1.SuspendLayout();
|
||||
this.serverGroupBox.SuspendLayout();
|
||||
this.updateGroupBox.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// checkBox1
|
||||
@@ -139,7 +143,7 @@
|
||||
this.bottomPanel1.Controls.Add(this.cancelButton);
|
||||
this.bottomPanel1.Controls.Add(this.saveButton);
|
||||
this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.bottomPanel1.Location = new System.Drawing.Point(0, 443);
|
||||
this.bottomPanel1.Location = new System.Drawing.Point(0, 509);
|
||||
this.bottomPanel1.Name = "bottomPanel1";
|
||||
this.bottomPanel1.Size = new System.Drawing.Size(708, 50);
|
||||
this.bottomPanel1.TabIndex = 1;
|
||||
@@ -162,7 +166,7 @@
|
||||
// strategyGroupBox
|
||||
//
|
||||
this.strategyGroupBox.Controls.Add(this.strategyGrid);
|
||||
this.strategyGroupBox.Location = new System.Drawing.Point(12, 253);
|
||||
this.strategyGroupBox.Location = new System.Drawing.Point(12, 319);
|
||||
this.strategyGroupBox.Name = "strategyGroupBox";
|
||||
this.strategyGroupBox.Size = new System.Drawing.Size(684, 180);
|
||||
this.strategyGroupBox.TabIndex = 2;
|
||||
@@ -217,13 +221,43 @@
|
||||
this.setServerButton.UseVisualStyleBackColor = true;
|
||||
this.setServerButton.Click += new System.EventHandler(this.SetServer_Click);
|
||||
//
|
||||
// updateGroupBox
|
||||
//
|
||||
this.updateGroupBox.Controls.Add(this.updateFeedUrlBox);
|
||||
this.updateGroupBox.Controls.Add(this.setUpdateFeedButton);
|
||||
this.updateGroupBox.Location = new System.Drawing.Point(12, 253);
|
||||
this.updateGroupBox.Name = "updateGroupBox";
|
||||
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)";
|
||||
//
|
||||
// updateFeedUrlBox
|
||||
//
|
||||
this.updateFeedUrlBox.Location = new System.Drawing.Point(12, 24);
|
||||
this.updateFeedUrlBox.Name = "updateFeedUrlBox";
|
||||
this.updateFeedUrlBox.PlaceholderText = "http://host:port";
|
||||
this.updateFeedUrlBox.Size = new System.Drawing.Size(485, 22);
|
||||
this.updateFeedUrlBox.TabIndex = 0;
|
||||
//
|
||||
// setUpdateFeedButton
|
||||
//
|
||||
this.setUpdateFeedButton.Location = new System.Drawing.Point(504, 21);
|
||||
this.setUpdateFeedButton.Name = "setUpdateFeedButton";
|
||||
this.setUpdateFeedButton.Size = new System.Drawing.Size(168, 28);
|
||||
this.setUpdateFeedButton.TabIndex = 1;
|
||||
this.setUpdateFeedButton.Text = "Save Feed URL";
|
||||
this.setUpdateFeedButton.UseVisualStyleBackColor = true;
|
||||
this.setUpdateFeedButton.Click += new System.EventHandler(this.SetUpdateFeed_Click);
|
||||
//
|
||||
// OptionsForm
|
||||
//
|
||||
this.AcceptButton = this.saveButton;
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.CancelButton = this.cancelButton;
|
||||
this.ClientSize = new System.Drawing.Size(708, 493);
|
||||
this.ClientSize = new System.Drawing.Size(708, 559);
|
||||
this.Controls.Add(this.strategyGroupBox);
|
||||
this.Controls.Add(this.updateGroupBox);
|
||||
this.Controls.Add(this.serverGroupBox);
|
||||
this.Controls.Add(this.tableLayoutPanel1);
|
||||
this.Controls.Add(this.bottomPanel1);
|
||||
@@ -242,6 +276,8 @@
|
||||
this.bottomPanel1.ResumeLayout(false);
|
||||
this.serverGroupBox.ResumeLayout(false);
|
||||
this.serverGroupBox.PerformLayout();
|
||||
this.updateGroupBox.ResumeLayout(false);
|
||||
this.updateGroupBox.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
@@ -263,5 +299,8 @@
|
||||
private System.Windows.Forms.GroupBox serverGroupBox;
|
||||
private System.Windows.Forms.TextBox serverUrlBox;
|
||||
private System.Windows.Forms.Button setServerButton;
|
||||
private System.Windows.Forms.GroupBox updateGroupBox;
|
||||
private System.Windows.Forms.TextBox updateFeedUrlBox;
|
||||
private System.Windows.Forms.Button setUpdateFeedButton;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ namespace OpenNest.Forms
|
||||
private void LoadSettings()
|
||||
{
|
||||
serverUrlBox.Text = NestStorage.Settings.ServerUrl;
|
||||
updateFeedUrlBox.Text = Settings.Default.UpdateFeedUrl;
|
||||
checkBox1.Checked = Settings.Default.CreateNewNestOnOpen;
|
||||
numericUpDown1.Value = (decimal)Settings.Default.AutoSizePlateFactor;
|
||||
|
||||
@@ -167,5 +168,32 @@ namespace OpenNest.Forms
|
||||
"Server URL", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUpdateFeed_Click(object sender, EventArgs e)
|
||||
{
|
||||
var text = updateFeedUrlBox.Text?.Trim() ?? "";
|
||||
if (text.Length == 0)
|
||||
{
|
||||
Settings.Default.UpdateFeedUrl = "";
|
||||
Settings.Default.Save();
|
||||
updateFeedUrlBox.Text = "";
|
||||
MessageBox.Show(this, "Update feed URL cleared. Update checks are disabled.",
|
||||
"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.",
|
||||
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Settings.Default.UpdateFeedUrl = url;
|
||||
Settings.Default.Save();
|
||||
updateFeedUrlBox.Text = url;
|
||||
MessageBox.Show(this, "Update feed URL saved. Restart OpenNest for the new feed to take effect.",
|
||||
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Forms;
|
||||
using Velopack;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -9,6 +10,11 @@ namespace OpenNest
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
// Must run before anything else: on first launch after an update,
|
||||
// Velopack re-invokes the new exe with hidden hook arguments and
|
||||
// this call handles them and exits, so the UI below never runs.
|
||||
VelopackApp.Build().Run();
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<AssemblyName>OpenNest</AssemblyName>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationHighDpiMode>DpiUnaware</ApplicationHighDpiMode>
|
||||
<Version>0.4.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Controls\LayoutViewGL.cs" />
|
||||
@@ -31,6 +32,7 @@
|
||||
<ProjectReference Include="..\Posts\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Posts\OpenNest.Posts.CincinnatiCIFiber\OpenNest.Posts.CincinnatiCIFiber.csproj" ReferenceOutputAssembly="false" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
<PackageReference Include="Velopack" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
|
||||
Generated
+14
@@ -256,5 +256,19 @@ namespace OpenNest.Properties {
|
||||
this["ActiveColorScheme"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[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.
|
||||
public string UpdateFeedUrl {
|
||||
get {
|
||||
return ((string)(this["UpdateFeedUrl"]));
|
||||
}
|
||||
set {
|
||||
this["UpdateFeedUrl"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,5 +62,8 @@
|
||||
<Setting Name="ActiveColorScheme" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">Classic</Value>
|
||||
</Setting>
|
||||
<Setting Name="UpdateFeedUrl" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Properties;
|
||||
using Velopack;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Periodically checks a Velopack release feed for updates and downloads
|
||||
/// them in the background. Never restarts the app on its own -- an open,
|
||||
/// unsaved nest must not be discarded out from under the user -- so
|
||||
/// callers must observe <see cref="UpdateReady"/> and call
|
||||
/// <see cref="ApplyAndRestart"/> once the user agrees to restart.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The feed URL is never hard-coded: it is read from the per-machine
|
||||
/// <see cref="Settings.UpdateFeedUrl"/> 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.
|
||||
/// </remarks>
|
||||
public sealed class UpdaterService : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(30);
|
||||
|
||||
private readonly UpdateManager _manager;
|
||||
private Timer _timer;
|
||||
private UpdateInfo _pendingUpdate;
|
||||
|
||||
/// <summary>Raised on the thread pool once an update has finished downloading.</summary>
|
||||
public event Action<string> UpdateReady;
|
||||
|
||||
public UpdaterService()
|
||||
: this(Settings.Default.UpdateFeedUrl) { }
|
||||
|
||||
public UpdaterService(string feedUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(feedUrl))
|
||||
return;
|
||||
|
||||
_manager = new UpdateManager(new SimpleWebSource(feedUrl));
|
||||
}
|
||||
|
||||
/// <summary>True once a downloaded update is waiting for <see cref="ApplyAndRestart"/>.</summary>
|
||||
public bool HasPendingUpdate => _pendingUpdate != null;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_manager is null || !_manager.IsInstalled)
|
||||
return;
|
||||
|
||||
_timer = new Timer(_ => _ = TickAsync(), null, CheckInterval, CheckInterval);
|
||||
}
|
||||
|
||||
/// <summary>Applies the downloaded update and restarts the process. Call only after the user agrees.</summary>
|
||||
public void ApplyAndRestart()
|
||||
{
|
||||
if (_manager != null && _pendingUpdate != null)
|
||||
_manager.ApplyUpdatesAndRestart(_pendingUpdate);
|
||||
}
|
||||
|
||||
public void Dispose() => _timer?.Dispose();
|
||||
|
||||
private async Task TickAsync()
|
||||
{
|
||||
if (_manager is null || _pendingUpdate != null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var newVersion = await _manager.CheckForUpdatesAsync();
|
||||
if (newVersion == null)
|
||||
return;
|
||||
|
||||
await _manager.DownloadUpdatesAsync(newVersion);
|
||||
_pendingUpdate = newVersion;
|
||||
UpdateReady?.Invoke(newVersion.TargetFullRelease.Version.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Network hiccup or feed unreachable -- retry next tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
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
|
||||
|
||||
The feed URL is a per-machine setting
|
||||
(`OpenNest.Properties.Settings.UpdateFeedUrl`), set from **File > 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.
|
||||
|
||||
Velopack only applies updates to an installed copy (`UpdateManager.IsInstalled`);
|
||||
running from a build output directory never attempts an update.
|
||||
|
||||
## 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
|
||||
result to a directory you provide:
|
||||
|
||||
```powershell
|
||||
./scripts/Deploy-OpenNest.ps1 -FeedHost <host> -FeedUser <user> -RemoteDataDir <path>
|
||||
```
|
||||
|
||||
Bump `<Version>` 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 `<Version>` via
|
||||
`-p:Version`, so the two don't conflict.
|
||||
@@ -0,0 +1,72 @@
|
||||
<#
|
||||
Package a Velopack release of the desktop app and push it to a self-hosted
|
||||
update feed (a static file server serving the `vpk pack` output). Installed
|
||||
clients configured with that feed URL (Options > Auto-update feed URL) pull
|
||||
and apply updates themselves.
|
||||
|
||||
This is separate from scripts/Publish-Windows.ps1 and the GitHub release
|
||||
process (docs/releasing.md): it targets a privately hosted feed, not a
|
||||
public GitHub Release, and never embeds that feed's host or path in source.
|
||||
|
||||
Bump <Version> in OpenNest/OpenNest.csproj before running this for a new
|
||||
release.
|
||||
|
||||
Examples:
|
||||
powershell -ExecutionPolicy Bypass -File scripts/Deploy-OpenNest.ps1 `
|
||||
-FeedHost myhost -FeedUser me -RemoteDataDir ~/docker/opennest-releases/data
|
||||
#>
|
||||
|
||||
Param(
|
||||
[string]$PublishConfiguration = "Release",
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FeedHost,
|
||||
[string]$FeedUser = "aj",
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RemoteDataDir
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ScriptDir = Split-Path -Parent $PSCommandPath
|
||||
$RepoRoot = if ((Split-Path -Leaf $ScriptDir) -eq 'scripts') { Split-Path -Parent $ScriptDir } else { $ScriptDir }
|
||||
Write-Host "Repository root: $RepoRoot"
|
||||
|
||||
$ProjectPath = Join-Path $RepoRoot 'OpenNest\OpenNest.csproj'
|
||||
if (-not (Test-Path -LiteralPath $ProjectPath)) { throw "Project not found at: $ProjectPath" }
|
||||
|
||||
$csprojContent = Get-Content -LiteralPath $ProjectPath -Raw
|
||||
if ($csprojContent -notmatch '<Version>(?<v>[\d.]+)</Version>') { throw "Could not find <Version> in $ProjectPath" }
|
||||
$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" }
|
||||
}
|
||||
|
||||
$PublishDir = Join-Path $RepoRoot 'OpenNest\bin\publish-tmp'
|
||||
$ReleasesDir = Join-Path $RepoRoot 'OpenNest\bin\releases'
|
||||
if (Test-Path $PublishDir) { Remove-Item $PublishDir -Recurse -Force }
|
||||
|
||||
Write-Host "Publishing self-contained build..."
|
||||
& dotnet publish $ProjectPath -c $PublishConfiguration -r win-x64 --self-contained true -o $PublishDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" }
|
||||
|
||||
Write-Host "Packing Velopack release v$Version..."
|
||||
& vpk pack -u OpenNest -v $Version -p $PublishDir -e OpenNest.exe -o $ReleasesDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "vpk pack failed with exit code $LASTEXITCODE" }
|
||||
|
||||
Write-Host "Pushing release feed to $FeedHost..."
|
||||
& scp -r "$ReleasesDir\*" "${FeedUser}@${FeedHost}:${RemoteDataDir}/"
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp failed" }
|
||||
|
||||
Write-Host "Deployment complete. Machines already configured with this feed URL will pick up v$Version within 30 minutes."
|
||||
Write-Host ""
|
||||
Write-Host "First-time setup on a machine (one-time per machine):"
|
||||
Write-Host " 1. Copy $ReleasesDir\OpenNest-win-Setup.exe to the machine and run it."
|
||||
Write-Host " 2. In OpenNest, open File > Options and set 'Auto-update feed URL' to"
|
||||
Write-Host " your feed's base URL (e.g. http://${FeedHost}:<port>), then restart OpenNest."
|
||||
Write-Host " This value is per-machine and never stored in source control."
|
||||
Reference in new issue
Block a user