From 4d5cdcfad8c1995158c3f865f781b88ae83688ad Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:25:17 +0100 Subject: [PATCH 1/5] test: reproduce locked obsolete Authority runtime and legacy launcher cleanup failure --- ...uthority-install-obsolete-cleanup.test.ps1 | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/windows/authority-install-obsolete-cleanup.test.ps1 diff --git a/tests/windows/authority-install-obsolete-cleanup.test.ps1 b/tests/windows/authority-install-obsolete-cleanup.test.ps1 new file mode 100644 index 00000000..148a5331 --- /dev/null +++ b/tests/windows/authority-install-obsolete-cleanup.test.ps1 @@ -0,0 +1,82 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$SourceDir, + [Parameter(Mandatory = $true)] + [string]$ExpectedVersion, + [Parameter(Mandatory = $true)] + [string]$ExpectedSourceCommit, + [Parameter(Mandatory = $true)] + [string]$InstallDir +) + +$ErrorActionPreference = 'Stop' + +# Exercise the real release installer under Windows, not a mock of Remove-Item. +$installer = Join-Path $PSScriptRoot '..\..\authority-host\windows\install-release.ps1' +$appRoot = Join-Path $InstallDir 'app' +$obsoleteDir = Join-Path $appRoot 'v0.0.1' +$lockedDll = Join-Path $obsoleteDir 'clrjit.dll' +$legacyExe = Join-Path $InstallDir 'GitHubDeliveryAuthority.exe' +$unknownDir = Join-Path $appRoot 'operator-data' +$stateFile = Join-Path $InstallDir 'authority.db' +$trustFile = Join-Path $InstallDir 'trust-store.json' + +New-Item -ItemType Directory -Force -Path $obsoleteDir | Out-Null +New-Item -ItemType Directory -Force -Path $unknownDir | Out-Null +[IO.File]::WriteAllText($lockedDll, 'locked obsolete runtime') +[IO.File]::WriteAllText($legacyExe, 'locked legacy launcher') +[IO.File]::WriteAllText($stateFile, 'persistent state') +[IO.File]::WriteAllText($trustFile, 'persistent trust') + +$lockedRuntime = $null +$lockedLauncher = $null +try { + $lockedRuntime = [IO.File]::Open($lockedDll, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) + $lockedLauncher = [IO.File]::Open($legacyExe, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) + + # The updater invokes Windows PowerShell 5.1; match that boundary. + $output = & powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ` + -File $installer ` + -SourceDir $SourceDir ` + -ExpectedVersion $ExpectedVersion ` + -ExpectedSourceCommit $ExpectedSourceCommit ` + -InstallDir $InstallDir ` + -SkipStart 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Authority installer failed on locked obsolete files: $($output | Out-String)" + } + + $record = Get-Content (Join-Path $InstallDir 'authority-host-install.json') -Raw | ConvertFrom-Json + if ($record.version -ne $ExpectedVersion -or $record.sourceCommit -ne $ExpectedSourceCommit.ToLowerInvariant()) { + throw 'Authority install record was not committed to the new release.' + } + if (-not (Test-Path (Join-Path $appRoot "v$ExpectedVersion\GitHubDeliveryAuthority.exe") -PathType Leaf)) { + throw 'Authority new-version executable is missing.' + } + if (-not (Test-Path $lockedDll -PathType Leaf) -or -not (Test-Path $legacyExe -PathType Leaf)) { + throw 'Locked obsolete files were unexpectedly removed during installation.' + } + if (($output | Out-String) -notmatch 'deferred cleanup') { + throw 'Authority installer did not surface the deferred cleanup warning.' + } +} finally { + if ($lockedLauncher) { $lockedLauncher.Dispose() } + if ($lockedRuntime) { $lockedRuntime.Dispose() } +} + +# Later reinstallation must retry cleanup rather than permanently suppress it. +$output = & powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass ` + -File $installer ` + -SourceDir $SourceDir ` + -ExpectedVersion $ExpectedVersion ` + -ExpectedSourceCommit $ExpectedSourceCommit ` + -InstallDir $InstallDir ` + -SkipStart 2>&1 +if ($LASTEXITCODE -ne 0) { throw "Authority cleanup retry failed: $($output | Out-String)" } +if (Test-Path $obsoleteDir) { throw 'Obsolete runtime was not removed after its lock cleared.' } +if (Test-Path $legacyExe) { throw 'Obsolete legacy launcher was not removed after its lock cleared.' } +if (-not (Test-Path $unknownDir -PathType Container)) { throw 'Unknown application data was deleted.' } +if (([IO.File]::ReadAllText($stateFile)) -ne 'persistent state') { throw 'Authority state was changed.' } +if (([IO.File]::ReadAllText($trustFile)) -ne 'persistent trust') { throw 'Authority trust state was changed.' } +Write-Host 'Authority obsolete-file cleanup regression: PASS' From 21592a86532194f9b34f81284a4d6c2185685e52 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:25:39 +0100 Subject: [PATCH 2/5] ci: exercise Windows Authority upgrade with locked obsolete runtime and launcher --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1525780c..d7bed6c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -300,3 +300,12 @@ jobs: if (Test-Path $Diagnostics -PathType Leaf) { Get-Content $Diagnostics } throw "Installed Authority XAML smoke test failed with exit code $LASTEXITCODE." } + + # Exercise the real Windows installer with old clrjit.dll and legacy + # launcher handles held open. A successful new version must not roll back. + & pwsh -NoProfile -File tests/windows/authority-install-obsolete-cleanup.test.ps1 ` + -SourceDir $Prepared.source ` + -ExpectedVersion $Version ` + -ExpectedSourceCommit $env:GITHUB_SHA ` + -InstallDir (Join-Path $env:RUNNER_TEMP 'github-delivery-authority-obsolete-cleanup-smoke') + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } From 737240501fa8b00664f34142ded3a9bfb29d0887 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:29:54 +0100 Subject: [PATCH 3/5] fix: defer locked obsolete Authority cleanup after install commit --- authority-host/windows/install-release.ps1 | 46 ++++++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/authority-host/windows/install-release.ps1 b/authority-host/windows/install-release.ps1 index 84044295..df393e6e 100644 --- a/authority-host/windows/install-release.ps1 +++ b/authority-host/windows/install-release.ps1 @@ -76,6 +76,7 @@ if (-not $previousExe) { } $stoppedBroker = $false +$installCommitted = $false try { New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null Copy-Item (Join-Path $SourceDir '*') $stagingDir -Recurse -Force @@ -122,24 +123,42 @@ try { $recordJson = ($installRecord | ConvertTo-Json) + [Environment]::NewLine [IO.File]::WriteAllText($recordTemp, $recordJson, $utf8NoBom) Move-Item -Force $recordTemp $recordPath + $installCommitted = $true - # Version directories are release-owned and self-contained. Once the new - # version is active, remove obsolete release versions so updates do not - # accumulate full runtimes. Root state and unknown app content are preserved. + # After activation, removal of obsolete runtimes is best-effort. A locked + # old DLL or launcher must not roll back a successfully committed version. + # Unremoved versions are retried on the next installation. $targetName = 'v' + $ExpectedVersion - $obsoleteVersionDirs = @( - Get-ChildItem -LiteralPath $appRoot -Directory -Force | Where-Object { - $_.Name -match '^v\d+\.\d+\.\d+$' -and $_.Name -ne $targetName + try { + $obsoleteVersionDirs = @( + Get-ChildItem -LiteralPath $appRoot -Directory -Force -ErrorAction Stop | Where-Object { + $_.Name -match '^v\d+\.\d+\.\d+$' -and $_.Name -ne $targetName + } + ) + foreach ($obsoleteVersionDir in $obsoleteVersionDirs) { + try { + Remove-Item -LiteralPath $obsoleteVersionDir.FullName -Recurse -Force -ErrorAction Stop + } + catch { + Write-Warning "Authority deferred cleanup of obsolete version $($obsoleteVersionDir.Name): $($_.Exception.Message)" + } } - ) - foreach ($obsoleteVersionDir in $obsoleteVersionDirs) { - Remove-Item -LiteralPath $obsoleteVersionDir.FullName -Recurse -Force + } + catch { + Write-Warning "Authority deferred cleanup of obsolete version directories: $($_.Exception.Message)" } - # Remove only the obsolete root-level launcher from the legacy layout. State - # (`authority.db`, trust-store.json) and unknown user files are deliberately preserved. + # Only the legacy root-level launcher is removed; persistent state + # (authority.db, trust-store.json) and unknown user files are preserved. $legacyExe = Join-Path $InstallDir 'GitHubDeliveryAuthority.exe' - if (Test-Path $legacyExe -PathType Leaf) { Remove-Item $legacyExe -Force } + if (Test-Path $legacyExe -PathType Leaf) { + try { + Remove-Item -LiteralPath $legacyExe -Force -ErrorAction Stop + } + catch { + Write-Warning "Authority deferred cleanup of obsolete legacy launcher: $($_.Exception.Message)" + } + } if (-not $SkipStart) { Start-Process $installedExe @@ -147,7 +166,8 @@ try { Write-Host "Installed GitHub Delivery Authority $ExpectedVersion to $targetDir" } catch { - if ($stoppedBroker -and -not $SkipStart -and $previousExe -and (Test-Path $previousExe -PathType Leaf)) { + # Never restart an obsolete broker after the new install record commits. + if (-not $installCommitted -and $stoppedBroker -and -not $SkipStart -and $previousExe -and (Test-Path $previousExe -PathType Leaf)) { Start-Process $previousExe } throw From 1cd9b9b7bdd1a67084ef5921e81fd6b1309062bd Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:30:23 +0100 Subject: [PATCH 4/5] chore: bump github-delivery version to 1.8.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b6c308c..1a3c41ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-delivery", - "version": "1.8.4", + "version": "1.8.5", "description": "Guided installer and safety tooling for GitHub Delivery workflows", "type": "module", "engines": { From 46d5d28dcb3714f886d00ed9d9897dabf1d820f0 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:30:27 +0100 Subject: [PATCH 5/5] docs: prepare v1.8.5 changelog for deferred Authority cleanup --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932989c8..94291325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to `github-delivery` are documented here. ## [Unreleased] +## [1.8.5] - 2026-11-04 + +### Changed + +- Bumped the package version from `1.8.4` to `1.8.5`. + +### Fixed + +- Windows Approval GUI upgrades now defer cleanup of locked obsolete runtime directories and the legacy root launcher instead of reporting a failed installation after the new GUI install record is committed. Cleanup emits a warning and is retried on the next installation, while new-version installation failures remain fatal. The installer will not restart an obsolete broker after activation, preventing mismatched skill/GUI versions and unnecessary skill rollbacks (PR #492). +- Added a Windows regression that holds real file handles on an obsolete `clrjit.dll` and launcher during an upgrade, checks persisted state and unknown content, and verifies deferred cleanup completes after handles clear (PR #492). + ## [1.8.4] - 2026-11-04 ### Changed