diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 00000000..a7a7bc41 --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -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 < 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" ] diff --git a/AGENTS.md b/AGENTS.md index b0d34992..5c517b1a 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): 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. diff --git a/OpenNest/Forms/OptionsForm.Designer.cs b/OpenNest/Forms/OptionsForm.Designer.cs index c79cc480..4c185582 100644 --- a/OpenNest/Forms/OptionsForm.Designer.cs +++ b/OpenNest/Forms/OptionsForm.Designer.cs @@ -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; diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs index b035ddbf..db003b8a 100644 --- a/OpenNest/Forms/OptionsForm.cs +++ b/OpenNest/Forms/OptionsForm.cs @@ -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 = diff --git a/OpenNest/Properties/Settings.Designer.cs b/OpenNest/Properties/Settings.Designer.cs index 601d0461..5c78dd40 100644 --- a/OpenNest/Properties/Settings.Designer.cs +++ b/OpenNest/Properties/Settings.Designer.cs @@ -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")] diff --git a/OpenNest/Properties/Settings.settings b/OpenNest/Properties/Settings.settings index 836515bb..583200c9 100644 --- a/OpenNest/Properties/Settings.settings +++ b/OpenNest/Properties/Settings.settings @@ -74,6 +74,9 @@ + + False + Laser diff --git a/OpenNest/UpdaterService.cs b/OpenNest/UpdaterService.cs index 102e71fd..1f5a91fa 100644 --- a/OpenNest/UpdaterService.cs +++ b/OpenNest/UpdaterService.cs @@ -16,7 +16,8 @@ namespace OpenNest /// /// /// A blank (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 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. /// @@ -33,12 +34,12 @@ namespace OpenNest public event Action 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); } diff --git a/docs/auto-update.md b/docs/auto-update.md index b2584f3d..f23ae60d 100644 --- a/docs/auto-update.md +++ b/docs/auto-update.md @@ -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. diff --git a/docs/releasing.md b/docs/releasing.md index f28346d0..cba1cc04 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -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. diff --git a/scripts/Publish-Windows.ps1 b/scripts/Publish-Windows.ps1 index 15de46af..a57db0bb 100644 --- a/scripts/Publish-Windows.ps1 +++ b/scripts/Publish-Windows.ps1 @@ -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.' }