feat(release): publish green master pushes as an opt-in development build

dev-build.yml runs after Cross-platform tests pass on a master push,
packages that commit as X.Y.Z-dev.N (the minor after the newest vX.Y.Z
tag, N the commit count) and replaces the single rolling dev-build
pre-release. Only its publish job has release write access, and a run
whose commit is no longer master's head publishes nothing.

Tools > Options > Include development builds lets an installed copy take
that pre-release; everyone else still sees full releases only.
Publish-Windows.ps1 accepts -dev.N versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-27 18:31:36 -04:00
1 parent 9b4c1f51d0
commit bc15c362e1
10 files changed
+196 -12

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
name: Development build
# Every master push whose cross-platform tests pass becomes the rolling
# `dev-build` pre-release: X.Y.Z-dev.N, where X.Y.Z is the minor after the
# newest release tag and N the commit count. Installed copies take it only
# with Tools > Options > Include development builds.
on:
workflow_run:
workflows: ['Cross-platform tests']
types: [completed]
branches: [master]
permissions:
contents: read
concurrency:
group: dev-build
cancel-in-progress: true
jobs:
package:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: windows-2022
timeout-minutes: 30
defaults:
run:
shell: pwsh
env:
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
DOTNET_NOLOGO: '1'
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: '8.0.x'
- name: Resolve development version
id: version
run: |
$releases = git tag --list 'v*' | Where-Object { $_ -match '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$' } |
ForEach-Object { [version]$_.Substring(1) } | Sort-Object
if (-not $releases) { throw 'No vX.Y.Z release tag found.' }
$latest = @($releases)[-1]
$count = (git rev-list --count $env:SOURCE_SHA).Trim()
if ($LASTEXITCODE -ne 0) { throw 'Cannot count commits.' }
$version = "$($latest.Major).$($latest.Minor + 1).0-dev.$count"
Write-Host "Development version $version (after v$latest)"
"version=$version" >> $env:GITHUB_OUTPUT
- name: Publish and verify Windows package
run: ./scripts/Publish-Windows.ps1 -Version ${{ steps.version.outputs.version }}
- name: Upload development build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: OpenNest-dev-build
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: 7
compression-level: 0
publish:
needs: package
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
actions: read
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
SOURCE_SHA: ${{ github.event.workflow_run.head_sha }}
VERSION: ${{ needs.package.outputs.version }}
steps:
- name: Skip if master has moved on
id: current
run: |
head=$(gh api "repos/$GH_REPO/commits/master" --jq .sha)
if [ "$head" != "$SOURCE_SHA" ]; then
echo "master is at $head; its own run publishes the newer build."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Download development build
if: steps.current.outputs.skip != 'true'
run: gh run download "$GITHUB_RUN_ID" -n OpenNest-dev-build -D dev
- name: Verify checksums
if: steps.current.outputs.skip != 'true'
working-directory: dev
run: |
sha256sum -c ./*.zip.sha256
(cd velopack && sha256sum -c "../OpenNest.v$VERSION.velopack.sha256")
- name: Replace the dev-build pre-release
if: steps.current.outputs.skip != 'true'
working-directory: dev
run: |
if gh release view dev-build >/dev/null 2>&1; then
gh release delete dev-build --cleanup-tag --yes
fi
cat > notes.md <<EOF
Automatic development build of master at $SOURCE_SHA.
It may contain unfinished features. Installed copies receive it only with
**Tools > Options > Include development builds**; full releases are unaffected.
EOF
gh release create dev-build ./*.zip ./*.sha256 velopack/* \
--target "$SOURCE_SHA" --prerelease --latest=false \
--title "OpenNest $VERSION (development build, ${SOURCE_SHA:0:8})" --notes-file notes.md
- name: Read back the pre-release
if: steps.current.outputs.skip != 'true'
run: |
gh release view dev-build --json isPrerelease,isDraft,tagName,assets \
--jq '"prerelease=\(.isPrerelease) draft=\(.isDraft) assets=\(.assets|length)"' | tee readback.txt
grep -qx 'prerelease=true draft=false assets=8' readback.txt
[ "$(gh api "repos/$GH_REPO/git/ref/tags/dev-build" --jq .object.sha)" = "$SOURCE_SHA" ]
+1 -1
View File
@@ -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): GitHub Releases by default, `UpdateFeedUrl` for a self-hosted feed, release Velopack assets and `Deploy-OpenNest.ps1`.
- [Desktop auto-update](docs/auto-update.md): GitHub Releases by default, opt-in rolling `dev-build` pre-release from green master pushes, `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.
+16 -3
View File
@@ -49,6 +49,7 @@
this.updateGroupBox = new System.Windows.Forms.GroupBox();
this.updateFeedUrlBox = new System.Windows.Forms.TextBox();
this.setUpdateFeedButton = new System.Windows.Forms.Button();
this.includeDevBuildsBox = new System.Windows.Forms.CheckBox();
this.ledgerGroupBox = new System.Windows.Forms.GroupBox();
this.ledgerUrlLabel = new System.Windows.Forms.Label();
this.ledgerUrlBox = new System.Windows.Forms.TextBox();
@@ -203,7 +204,7 @@
// strategyGroupBox
//
this.strategyGroupBox.Controls.Add(this.strategyGrid);
this.strategyGroupBox.Location = new System.Drawing.Point(12, 457);
this.strategyGroupBox.Location = new System.Drawing.Point(12, 485);
this.strategyGroupBox.Name = "strategyGroupBox";
this.strategyGroupBox.Size = new System.Drawing.Size(684, 180);
this.strategyGroupBox.TabIndex = 2;
@@ -262,9 +263,10 @@
//
this.updateGroupBox.Controls.Add(this.updateFeedUrlBox);
this.updateGroupBox.Controls.Add(this.setUpdateFeedButton);
this.updateGroupBox.Controls.Add(this.includeDevBuildsBox);
this.updateGroupBox.Location = new System.Drawing.Point(12, 391);
this.updateGroupBox.Name = "updateGroupBox";
this.updateGroupBox.Size = new System.Drawing.Size(684, 60);
this.updateGroupBox.Size = new System.Drawing.Size(684, 88);
this.updateGroupBox.TabIndex = 3;
this.updateGroupBox.TabStop = false;
this.updateGroupBox.Text = "Auto-update feed URL (blank uses GitHub Releases; set per machine)";
@@ -287,6 +289,16 @@
this.setUpdateFeedButton.UseVisualStyleBackColor = true;
this.setUpdateFeedButton.Click += new System.EventHandler(this.SetUpdateFeed_Click);
//
// includeDevBuildsBox
//
this.includeDevBuildsBox.AutoSize = true;
this.includeDevBuildsBox.Location = new System.Drawing.Point(12, 58);
this.includeDevBuildsBox.Name = "includeDevBuildsBox";
this.includeDevBuildsBox.Size = new System.Drawing.Size(460, 20);
this.includeDevBuildsBox.TabIndex = 2;
this.includeDevBuildsBox.Text = "Include development builds (latest master; takes effect after restart)";
this.includeDevBuildsBox.UseVisualStyleBackColor = true;
//
// ledgerGroupBox
//
this.ledgerGroupBox.Controls.Add(this.ledgerUrlLabel);
@@ -362,7 +374,7 @@
this.AcceptButton = this.saveButton;
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.CancelButton = this.cancelButton;
this.ClientSize = new System.Drawing.Size(708, 697);
this.ClientSize = new System.Drawing.Size(708, 725);
this.Controls.Add(this.strategyGroupBox);
this.Controls.Add(this.updateGroupBox);
this.Controls.Add(this.ledgerGroupBox);
@@ -415,6 +427,7 @@
private System.Windows.Forms.GroupBox updateGroupBox;
private System.Windows.Forms.TextBox updateFeedUrlBox;
private System.Windows.Forms.Button setUpdateFeedButton;
private System.Windows.Forms.CheckBox includeDevBuildsBox;
private System.Windows.Forms.GroupBox ledgerGroupBox;
private System.Windows.Forms.Label ledgerUrlLabel;
private System.Windows.Forms.TextBox ledgerUrlBox;
+2
View File
@@ -82,6 +82,7 @@ namespace OpenNest.Forms
{
serverUrlBox.Text = NestStorage.Settings.ServerUrl;
updateFeedUrlBox.Text = Settings.Default.UpdateFeedUrl;
includeDevBuildsBox.Checked = Settings.Default.UpdateIncludeDevBuilds;
ledgerUrlBox.Text = MaterialLedgerStore.Settings.BaseUrl;
ledgerKeyBox.Text = MaterialLedgerStore.Settings.ApiKey;
checkBox1.Checked = Settings.Default.CreateNewNestOnOpen;
@@ -105,6 +106,7 @@ namespace OpenNest.Forms
private void SaveSettings()
{
Settings.Default.CreateNewNestOnOpen = checkBox1.Checked;
Settings.Default.UpdateIncludeDevBuilds = includeDevBuildsBox.Checked;
Settings.Default.AutoSizePlateFactor = (double)numericUpDown1.Value;
Settings.Default.CuttingPlanEffort = (double)planningEffortBox.Value;
Settings.Default.ActiveColorScheme =
+13
View File
@@ -307,6 +307,19 @@ namespace OpenNest.Properties {
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
// Also offer the rolling dev-build pre-release from GitHub Releases.
public bool UpdateIncludeDevBuilds {
get {
return ((bool)(this["UpdateIncludeDevBuilds"]));
}
set {
this["UpdateIncludeDevBuilds"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Laser")]
+3
View File
@@ -74,6 +74,9 @@
<Setting Name="UpdateFeedUrl" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="UpdateIncludeDevBuilds" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="DrawingLibraryCutType" Type="System.String" Scope="User">
<Value Profile="(Default)">Laser</Value>
</Setting>
+5 -4
View File
@@ -16,7 +16,8 @@ namespace OpenNest
/// </summary>
/// <remarks>
/// A blank <see cref="Settings.UpdateFeedUrl"/> (the default) checks the
/// public GitHub Releases for full, non-pre-release versions. A URL set in
/// public GitHub Releases for full versions, plus the rolling development
/// pre-release when <see cref="Settings.UpdateIncludeDevBuilds"/> is set. 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.
/// </remarks>
@@ -33,12 +34,12 @@ namespace OpenNest
public event Action<string> UpdateReady;
public UpdaterService()
: this(Settings.Default.UpdateFeedUrl) { }
: this(Settings.Default.UpdateFeedUrl, Settings.Default.UpdateIncludeDevBuilds) { }
public UpdaterService(string feedUrl)
public UpdaterService(string feedUrl, bool includeDevBuilds)
{
IUpdateSource source = string.IsNullOrWhiteSpace(feedUrl)
? new GithubSource(GitHubRepositoryUrl, null, false)
? new GithubSource(GitHubRepositoryUrl, null, includeDevBuilds)
: new SimpleWebSource(feedUrl);
_manager = new UpdateManager(source);
}
+21 -1
View File
@@ -13,7 +13,8 @@ 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.
machine behind one address). Pre-releases are offered only as the development
build below.
To use a self-hosted feed instead, set **Tools > Options > Auto-update feed URL**
(`OpenNest.Properties.Settings.UpdateFeedUrl`, saved for the current Windows
@@ -39,6 +40,25 @@ 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`.
## Development builds
`.github/workflows/dev-build.yml` runs after every master push whose
`Cross-platform tests` pass (that workflow includes the Windows desktop tests).
It packages that commit with `Publish-Windows.ps1` as `X.Y.Z-dev.N`, where
`X.Y.Z` is the minor after the newest `vX.Y.Z` tag and `N` the commit count, so
each build outranks the last and the next full release outranks them all. It then
replaces the single rolling `dev-build` pre-release (tag `dev-build`, never marked
Latest) with the same eight assets as a release. A run whose commit is no longer
master's head publishes nothing; the newer run does.
Installed copies take it only with **Tools > Options > Include development
builds** (`UpdateIncludeDevBuilds`, restart to apply); everyone else sees full
releases only. Velopack reads the ten newest GitHub releases, so keep a single
rolling pre-release: a pile of them could push the newest full release out of
that window. Turning the option off leaves a copy on its development build until
a higher full release appears. The workflow's publish job is the only automation
with release write access; it is limited to this pre-release.
## Hosting a feed
Any static file server that can serve a `vpk pack` output directory works.
+3 -1
View File
@@ -34,7 +34,9 @@ pulls from GitHub every hour; it refuses pushes.
engine name is rejected.
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. The separate [development build](auto-update.md#development-builds)
workflow does replace the rolling `dev-build` pre-release after each green master
push (owner-approved); it is not a release and never touches `vX.Y.Z` tags. Once present on the default branch, it can also be dispatched manually
with an `X.Y.Z` version. Running/dispatching via a PAT needs suitable Actions
permissions; public run metadata alone does not prove dispatch access.
+4 -2
View File
@@ -1,7 +1,8 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$')]
# X.Y.Z for releases; X.Y.Z-dev.N for the automatic development build.
[ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-dev\.(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.
@@ -67,7 +68,8 @@ try {
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" }
$numericVersion = $Version -replace '-.*$', ''
if ($assemblyVersion.ToString() -ne "$numericVersion.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.' }