Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 33 additions & 13 deletions authority-host/windows/install-release.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,32 +123,51 @@ 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
}
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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
82 changes: 82 additions & 0 deletions tests/windows/authority-install-obsolete-cleanup.test.ps1
Original file line number Diff line number Diff line change
@@ -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'
Loading