diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be0482f..b9b0232 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -538,41 +538,23 @@ jobs:
Pop-Location
exit 0
- - name: Apply retention rule (only latest z in minor survives)
- if: steps.vercheck.outputs.bumped == 'true' && steps.releasecheck.outputs.exists == 'false'
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- $ver = '${{ steps.vercheck.outputs.version }}'
- $vParts = $ver -split '\.'
- if ($vParts.Length -ne 3) {
- Write-Host "Unexpected version format '$ver' — skipping retention cleanup"
- exit 0
- }
- $thisMinor = "$($vParts[0]).$($vParts[1])" # e.g. "1.98"
- # Enumerate ALL existing releases (not just immediate prev) so we correctly
- # collapse the 1.98.x lineup down to just v1.98.8 even if v1.98.7 was missed
- # and v1.98.6 is still the latest existing release. End-state: only the new
- # `$ver` release exists in this minor.
- $allReleases = gh release list --json tagName --limit 200 | ConvertFrom-Json
- $global:LASTEXITCODE = 0
- $toDelete = @()
- foreach ($r in $allReleases) {
- $tag = $r.tagName
- $tagVer = $tag -replace '^v', ''
- $tagParts = $tagVer -split '\.'
- if ($tagParts.Length -eq 3 -and "$($tagParts[0]).$($tagParts[1])" -eq $thisMinor -and $tagVer -ne $ver) {
- $toDelete += $tag
- }
- }
- if ($toDelete.Count -eq 0) {
- Write-Host "Retention: no older releases in minor $thisMinor — nothing to delete"
- } else {
- Write-Host "Retention: deleting $($toDelete.Count) older $thisMinor.x release(s) so only v$ver survives"
- foreach ($tag in $toDelete) {
- Write-Host " -> gh release delete $tag --cleanup-tag"
- gh release delete $tag --yes --cleanup-tag 2>$null
- $global:LASTEXITCODE = 0
- }
- }
- exit 0
+ # NO RELEASE RETENTION / DELETION STEP — deliberately removed, do not re-add.
+ #
+ # A retention step used to delete every older patch release within the same minor so
+ # that only the newest survived. That silently broke the package managers, because a
+ # published package manifest pins the release asset URL for its own version and those
+ # registries keep approved versions forever:
+ #
+ # - Chocolatey: the approved rackstack 1.99.0 package downloads
+ # .../releases/download/v1.99.0/RackStack.exe via Get-ChocolateyWebFile. Retention
+ # deleted that release when v1.99.1 shipped, so `choco install rackstack` returned
+ # 404 for roughly two months before anyone noticed (found 2026-07-29).
+ # - Chocolatey moderation: v1.122.2 failed automated validation with "unable to find
+ # a package" because v1.122.3 published while v1.122.2 was still in the review
+ # queue, and retention deleted the release out from under the moderator.
+ # - winget: the same hazard was already documented in dist/winget/README.md.
+ #
+ # GitHub release assets on a public repo do not count against the Actions artifact
+ # storage quota, so there is no storage argument for deleting them. If disk hygiene
+ # ever matters, prune Actions *artifacts* — never releases that a published package
+ # points at. Run-Tests section 206 asserts this workflow contains no release deletion.
diff --git a/README.md b/README.md
index c443cae..6223721 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
-
+
diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1
index 5343871..7651695 100644
--- a/Tests/Run-Tests.ps1
+++ b/Tests/Run-Tests.ps1
@@ -10470,6 +10470,53 @@ catch {
Write-TestResult "Release Version Consistency Tests" $false $_.Exception.Message
}
+# ============================================================================
+# SECTION 206: RELEASES ARE NEVER DELETED (package managers pin release URLs)
+# ============================================================================
+# A published package manifest pins the release asset URL for its own version, and the
+# registries keep approved versions forever. Deleting a release therefore breaks installs
+# permanently and irrecoverably — the EXE cannot be rebuilt byte-identically, so the
+# published checksum can never be satisfied again.
+#
+# This is not hypothetical. The approved Chocolatey package rackstack 1.99.0 downloads
+# .../releases/download/v1.99.0/RackStack.exe; a retention step deleted that release when
+# v1.99.1 shipped, and `choco install rackstack` returned 404 for about two months before
+# it was noticed (2026-07-29). The same step also broke the v1.122.2 Chocolatey moderation
+# submission mid-review. Prune Actions artifacts if storage ever matters — never releases.
+Write-SectionHeader "SECTION 206: RELEASES ARE NEVER DELETED"
+
+try {
+ $wfDir206 = Join-Path $script:ModuleRoot '.github\workflows'
+ if (Test-Path -LiteralPath $wfDir206) {
+ $deleters206 = @(
+ Get-ChildItem -Path $wfDir206 -Filter '*.yml' -File |
+ Where-Object {
+ $raw = Get-Content $_.FullName -Raw
+ # `gh release delete`, or the REST equivalent against /releases/
+ ($raw -match 'gh\s+release\s+delete') -or
+ ($raw -match '(?i)-X\s+DELETE[^\r\n]*\/releases\/')
+ } | ForEach-Object { $_.Name }
+ )
+ Write-TestResult "Releases: no workflow deletes a GitHub release" `
+ ($deleters206.Count -eq 0) $(if ($deleters206.Count) { "found in: $($deleters206 -join ', ')" } else { "" })
+
+ # The tag must survive too — a deleted tag breaks source-based installs and the
+ # cosign certificate-identity that references the ref.
+ $tagKillers206 = @(
+ Get-ChildItem -Path $wfDir206 -Filter '*.yml' -File |
+ Where-Object { (Get-Content $_.FullName -Raw) -match '--cleanup-tag' } |
+ ForEach-Object { $_.Name }
+ )
+ Write-TestResult "Releases: no workflow deletes a release tag" `
+ ($tagKillers206.Count -eq 0) $(if ($tagKillers206.Count) { "found in: $($tagKillers206 -join ', ')" } else { "" })
+ } else {
+ Write-TestResult "Releases: workflow directory present" -Skipped -Message "no .github/workflows in this layout"
+ }
+}
+catch {
+ Write-TestResult "Release Retention Tests" $false $_.Exception.Message
+}
+
# ============================================================================
# SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase)
# ============================================================================
diff --git a/dist/winget/README.md b/dist/winget/README.md
index 311de4b..558a6a6 100644
--- a/dist/winget/README.md
+++ b/dist/winget/README.md
@@ -32,12 +32,17 @@ ci.yml step -- do not hand-maintain these files after that.
update the version, URL, SHA-256, and `ReleaseDate` in all three
files, then run `winget validate --manifest `.
-## Caveat: release retention vs. winget URLs
-
-The ci.yml retention step deletes the previous patch release within a
-minor when a new one publishes. The winget manifest for a version points
-at that version's release asset URL, so when the next patch ships, the
-current winget manifest's InstallerUrl goes dead until the auto-submitted
-update PR for the new version merges in winget-pkgs (moderation can take
-days). During that window `winget install TheAbider.RackStack` fails
-hash/download; installs recover as soon as the update PR merges.
+## Release retention: releases are never deleted
+
+Earlier versions of `ci.yml` deleted the previous patch release within a
+minor when a new one published, which meant a published winget manifest's
+`InstallerUrl` went dead as soon as the next patch shipped.
+
+That retention step has been removed. Release assets are permanent, because
+every package registry pins the asset URL for its own version and keeps
+approved versions indefinitely. The same defect silently broke the approved
+Chocolatey package (`choco install rackstack` returned 404 for about two
+months) before it was found on 2026-07-29.
+
+Do not reintroduce release deletion. `Run-Tests` section 206 fails the suite
+if any workflow calls `gh release delete` or `--cleanup-tag`.