From 0e162d50319cb8f6706e87ed7c2f3ec924d45e42 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 7 Jun 2026 15:42:15 +0000 Subject: [PATCH 1/2] ci(deploy): publish stable release on every main push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the dev-suffix prerelease path. Every push to main now produces a stable v$next-patch release with make_latest=legacy, so the wraithsec-site auto-update probe (which calls /releases/latest) tracks main within minutes of a merge instead of stalling on the last manually-tagged release. - Removes the v$NEXT-dev.$RUN_NUMBER prerelease build path. - Removes the unused RUN_NUMBER env var from the derive-version step. - Tag-triggered runs unchanged — explicit backports / hotfixes still work. - Stale-VERSION guard still aborts when VERSION lags behind the highest tag. - Header comment block rewritten to reflect new model. --- .github/workflows/deploy.yml | 57 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b39aaa6..f026fa9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,11 +1,16 @@ # ═══════════════════════════════════════════════════════════════════════════════ # WRAITH — Build & Release # -# Trigger : push to main → prerelease build (v$NEXT-dev.$RUN, never latest) -# push of v*.*.* tag → stable release (latest gated by SemVer compare) +# Trigger : push to main → stable release (next patch, latest by SemVer) +# push of v*.*.* tag → stable release (explicit tag, latest by SemVer) # -# To cut a stable release: bump VERSION on main, wait for the prerelease build to -# go green, then `git tag vX.Y.Z && git push origin vX.Y.Z`. +# Every push to main publishes a stable release using the next available patch +# in the major.minor line declared by VERSION. The GitHub server picks Latest +# by SemVer compare (`make_latest: legacy`), so the wraithsec-site auto-update +# probe (which calls /releases/latest) always sees the freshest stable within +# minutes of a merge — no manual tag step required. +# +# Tag pushes still work for explicit backports / hotfixes off a non-main branch. # # Artifact: WRAITH--win-x64.zip # ├── WRAITH.exe (self-contained, Windows-branded, .NET runtime + icon @@ -62,7 +67,6 @@ jobs: env: REF_TYPE: ${{ github.ref_type }} REF_NAME: ${{ github.ref_name }} - RUN_NUMBER: ${{ github.run_number }} run: | git fetch --tags --quiet @@ -82,7 +86,12 @@ jobs: Write-Host "Stable release: $v" } else { - # ── Prerelease build path (main push) ───────────────────────── + # ── Stable release path (main push) ─────────────────────────── + # Every merge to main publishes a stable release; the patch + # number is auto-incremented from the highest existing tag in + # the major.minor line declared by VERSION. The site fetches + # /releases/latest, so this keeps "newest stable" tracking + # main without requiring a manual `git tag` step per merge. $raw = (Get-Content VERSION -Raw).Trim() $parts = $raw.Split('.') if ($parts.Count -lt 2 -or $parts.Count -gt 3) { @@ -92,8 +101,7 @@ jobs: $minor = [int]$parts[1] $seedPatch = if ($parts.Count -eq 3) { [int]$parts[2] } else { 0 } - # Next patch in this major.minor — only stable tags count, - # prereleases (v*-dev.N) must not influence the patch number. + # Next patch in this major.minor — only stable tags count. $maxPatch = -1 foreach ($t in (git tag -l "v${major}.${minor}.*" 2>$null)) { if ($t -match "^v(\d+)\.(\d+)\.(\d+)$" -and [int]$Matches[1] -eq $major -and [int]$Matches[2] -eq $minor) { @@ -104,10 +112,11 @@ jobs: $patch = [Math]::Max($seedPatch, $maxPatch + 1) $nextStable = [version]"$major.$minor.$patch" - # Stale-VERSION guard against the highest existing stable tag. - # Prevents the foot-gun where VERSION lags behind the latest - # stable release — without this guard a main push could ship - # a prerelease that, once promoted, would downgrade users. + # Stale-VERSION guard. If VERSION lags behind the highest + # existing stable tag (e.g. operator forgot to bump after a + # backport bumped the previous minor), the build aborts with + # instructions instead of publishing a release the SemVer + # comparer would reject as latest. $maxTag = $null foreach ($t in (git tag -l "v*" 2>$null)) { if ($t -match '^v(\d+\.\d+\.\d+)$') { @@ -124,27 +133,23 @@ jobs: # the message as an array + -join instead. $msg = @( "VERSION file is stale." - " Would build prerelease for: v$nextStable" - " Highest existing stable tag: v$maxTag" + " Would build stable release for: v$nextStable" + " Highest existing stable tag: v$maxTag" "" "Bump VERSION to '$suggested' (or higher) and push again." ) -join [Environment]::NewLine throw $msg } - # Prereleases carry the run number so multiple main pushes for - # the same upcoming stable version don't collide on the same tag. - $v = "v$nextStable-dev.$($env:RUN_NUMBER)" - $packVer = "$nextStable-dev.$($env:RUN_NUMBER)" - # AssemblyVersion is the 4-part numeric and can't carry -dev.N. - # Velopack's portable-mode fallback compares this against the - # latest .nupkg version, so it must reflect the next-stable - # target — not the previous stable, or the popup will fire - # forever for portable users on the prior release. + $v = "v$nextStable" + $packVer = "$nextStable" $assemblyVer = "$nextStable.0" - $prerelease = 'true' - $makeLatest = 'false' - Write-Host "Prerelease build: $v (next stable would be v$nextStable)" + $prerelease = 'false' + # 'legacy' = GitHub server picks Latest by SemVer compare, so + # this release becomes Latest iff its version beats every other + # tag. Backports off older branches won't demote a newer main. + $makeLatest = 'legacy' + Write-Host "Stable release (main push): $v" } # NOTE: the tag is deliberately NOT pushed here. softprops/action-gh-release From 0b632850c076aa451c42e555b5cea9d993a99f08 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 7 Jun 2026 15:56:37 +0000 Subject: [PATCH 2/2] ci(deploy): serialize main-push runs via workflow concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two overlapping main pushes could both compute the same $nextStable (neither tag exists yet at version-derivation time), then race at the softprops/action-gh-release step — the loser fails with "tag already exists". Concurrency group=deploy-${ref}, cancel-in-progress=false queues the second run so it sees the first run's published tag and picks the next patch. Tag-push runs are scoped by their unique tag ref so they never collide with each other or with main pushes. --- .github/workflows/deploy.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f026fa9..7f6d866 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,18 @@ on: tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] permissions: - contents: write - security-events: write + contents: write + security-events: write + +# Serialize runs per ref so two overlapping main pushes can't both +# compute the same $nextStable and race the tag creation. cancel-in-progress +# is deliberately false — each merge to main is its own release, so the +# second run waits for the first to publish (and bump the tag space) before +# it derives its own version. Tag-push runs are scoped by their unique tag +# ref, so they never collide. +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: false jobs: build-and-release: