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
10 changes: 10 additions & 0 deletions .changesets/1792687232-e2151e8b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1792687232-e2151e8b
features:
- core
targets:
- cli
type: fix
bump: patch
description: keep a failed temporary directory cleanup in the Windows install script from being reported as an installation failure
author: AshGodfrey
date: "2026-10-22"
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,30 @@ jobs:
uses: ./.github/workflows/test-cli.yml
with:
test-group: "review"
test-cli-windows-installer:
needs: [detect-targets]
if: needs.detect-targets.outputs.run-cli == 'true'
name: Test CLI - windows installer (${{ matrix.shell }})
timeout-minutes: 15
runs-on: windows-latest
permissions:
contents: "read"
strategy:
fail-fast: false
matrix:
shell: [powershell, pwsh]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Test generated Windows installer
shell: ${{ matrix.shell }}
run: ./scripts/test-windows-installer.ps1
test-cli:
if: always() && needs.detect-targets.outputs.run-cli == 'true'
needs:
- detect-targets
- test-cli-windows-installer
- test-cli-primary
- test-cli-secondary
- test-cli-tertiary
Expand Down
27 changes: 21 additions & 6 deletions pkg/generate/snapshots/cli_release_go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -784,8 +784,13 @@ function Install-CLI {
}
finally {
# Cleanup
if (Test-Path $tempDir) {
Remove-Item $tempDir -Recurse -Force
try {
if (Test-Path -LiteralPath $tempDir) {
[System.IO.Directory]::Delete($tempDir, $true)
}
}
catch {
Write-ColorOutput "Warning: could not remove temporary directory ${tempDir}: $_" -Color Yellow
}
}
}
Expand Down Expand Up @@ -1829,8 +1834,13 @@ function Install-CLI {
}
finally {
# Cleanup
if (Test-Path $tempDir) {
Remove-Item $tempDir -Recurse -Force
try {
if (Test-Path -LiteralPath $tempDir) {
[System.IO.Directory]::Delete($tempDir, $true)
}
}
catch {
Write-ColorOutput "Warning: could not remove temporary directory ${tempDir}: $_" -Color Yellow
}
}
}
Expand Down Expand Up @@ -2933,8 +2943,13 @@ function Install-CLI {
}
finally {
# Cleanup
if (Test-Path $tempDir) {
Remove-Item $tempDir -Recurse -Force
try {
if (Test-Path -LiteralPath $tempDir) {
[System.IO.Directory]::Delete($tempDir, $true)
}
}
catch {
Write-ColorOutput "Warning: could not remove temporary directory ${tempDir}: $_" -Color Yellow
}
}
}
Expand Down
113 changes: 113 additions & 0 deletions scripts/test-windows-installer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#Requires -Version 5.1
<#
Exercises the generated Windows installer offline: the release download and
version lookup are replaced by local functions, the installer is run against a
locally built archive, and the installed binary, download URL, and temporary
directory cleanup are checked. On Windows a second run holds a file in the
temporary directory open to confirm a cleanup failure does not fail the install.
#>
[CmdletBinding()]
param(
[string]$InstallerPath = (Join-Path $PSScriptRoot '..\zSDKs\sdk-cli\scripts\install.ps1')

@cubic-dev-ai cubic-dev-ai Bot Oct 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The default InstallerPath uses backslash separators, which only resolve on Windows. Use forward slashes so the path also resolves under pwsh on Linux/macOS, where this script is meant to run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/test-windows-installer.ps1, line 11:

<comment>The default `InstallerPath` uses backslash separators, which only resolve on Windows. Use forward slashes so the path also resolves under pwsh on Linux/macOS, where this script is meant to run.</comment>

<file context>
@@ -0,0 +1,113 @@
+#>
+[CmdletBinding()]
+param(
+    [string]$InstallerPath = (Join-Path $PSScriptRoot '..\zSDKs\sdk-cli\scripts\install.ps1')
+)
+
</file context>
Suggested change
[string]$InstallerPath = (Join-Path $PSScriptRoot '..\zSDKs\sdk-cli\scripts\install.ps1')
[string]$InstallerPath = (Join-Path $PSScriptRoot '../zSDKs/sdk-cli/scripts/install.ps1')
Fix with cubic

)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

$InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
$isWindowsHost = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)

function Assert-True {
param([bool]$Condition, [string]$Message)
if (-not $Condition) {
throw "Assertion failed: $Message"
}
Write-Host "ok: $Message"
}

function New-InstallerSandbox {
$root = Join-Path ([System.IO.Path]::GetTempPath()) "installer-test-$([guid]::NewGuid())"

@cubic-dev-ai cubic-dev-ai Bot Oct 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Sandbox roots are never deleted, and the second sandbox is created inside the first one's temp directory because GetTempPath() returns the TEMP value that Invoke-Installer already changed. Build the root from the original temp path captured once at script start, and remove the root in the finally block after disposing the handle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/test-windows-installer.ps1, line 29:

<comment>Sandbox roots are never deleted, and the second sandbox is created inside the first one's temp directory because `GetTempPath()` returns the TEMP value that `Invoke-Installer` already changed. Build the root from the original temp path captured once at script start, and remove the root in the `finally` block after disposing the handle.</comment>

<file context>
@@ -0,0 +1,113 @@
+}
+
+function New-InstallerSandbox {
+    $root = Join-Path ([System.IO.Path]::GetTempPath()) "installer-test-$([guid]::NewGuid())"
+    foreach ($name in 'temp', 'localappdata', 'install', 'payload') {
+        New-Item -ItemType Directory -Path (Join-Path $root $name) -Force | Out-Null
</file context>
Fix with cubic

foreach ($name in 'temp', 'localappdata', 'install', 'payload') {
New-Item -ItemType Directory -Path (Join-Path $root $name) -Force | Out-Null
}
$binary = Join-Path (Join-Path $root 'payload') 'cli.exe'
Set-Content -LiteralPath $binary -Value "stub binary $([guid]::NewGuid())" -NoNewline -Encoding ascii
$archive = Join-Path $root 'cli_Windows.zip'
Compress-Archive -LiteralPath $binary -DestinationPath $archive
[pscustomobject]@{
Root = $root
Temp = Join-Path $root 'temp'
InstallDir = Join-Path $root 'install'
Binary = $binary
Archive = $archive
}
}

function Invoke-Installer {
param($Sandbox)
$env:TEMP = $Sandbox.Temp
$env:LOCALAPPDATA = Join-Path $Sandbox.Root 'localappdata'
$env:CLI_INSTALL_DIR = $Sandbox.InstallDir
$env:CLI_VERSION = 'v0.0.0-test'
if (-not $env:PROCESSOR_ARCHITECTURE) {
$env:PROCESSOR_ARCHITECTURE = 'AMD64'
}
$global:InstallerTestArchive = $Sandbox.Archive
$global:InstallerTestDownloads = @()
& $InstallerPath *>&1 | ForEach-Object { "$_" }

@cubic-dev-ai cubic-dev-ai Bot Oct 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Running the installer here writes the sandbox install directory into the persistent User PATH, and nothing restores it. Each run adds another temp installer-test-<guid> entry. Snapshot the User PATH before the tests and restore it in the finally block.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/test-windows-installer.ps1, line 57:

<comment>Running the installer here writes the sandbox install directory into the persistent User PATH, and nothing restores it. Each run adds another temp `installer-test-<guid>` entry. Snapshot the User PATH before the tests and restore it in the `finally` block.</comment>

<file context>
@@ -0,0 +1,113 @@
+    }
+    $global:InstallerTestArchive = $Sandbox.Archive
+    $global:InstallerTestDownloads = @()
+    & $InstallerPath *>&1 | ForEach-Object { "$_" }
+}
+
</file context>
Fix with cubic

}

function global:Invoke-RestMethod {
throw 'the installer must not look up the latest release when CLI_VERSION is set'
}

function global:Invoke-WebRequest {
param([string]$Uri, [string]$OutFile, [switch]$UseBasicParsing)
$global:InstallerTestDownloads += $Uri
Copy-Item -LiteralPath $global:InstallerTestArchive -Destination $OutFile
}

$global:InstallerTestOpenHandle = $null
function global:Expand-Archive {
param([string]$Path, [string]$DestinationPath, [switch]$Force)
Microsoft.PowerShell.Archive\Expand-Archive -Path $Path -DestinationPath $DestinationPath -Force:$Force
if ($global:InstallerTestHoldExtractedBinary) {
$extracted = Join-Path $DestinationPath 'cli.exe'
$global:InstallerTestOpenHandle = [System.IO.File]::Open($extracted, 'Open', 'Read', 'Read')
}
}

try {
Write-Host '--- install succeeds and cleans up'
$global:InstallerTestHoldExtractedBinary = $false
$sandbox = New-InstallerSandbox
$output = Invoke-Installer $sandbox
$installed = Join-Path $sandbox.InstallDir 'cli.exe'
Assert-True (Test-Path -LiteralPath $installed) 'binary is copied into CLI_INSTALL_DIR'
Assert-True ((Get-Content -LiteralPath $installed -Raw) -eq (Get-Content -LiteralPath $sandbox.Binary -Raw)) 'installed binary matches the archived one'
Assert-True ($global:InstallerTestDownloads.Count -eq 1) 'exactly one archive download'
Assert-True ($global:InstallerTestDownloads[0] -like '*/releases/download/v0.0.0-test/cli_Windows_*.zip') "download URL names the pinned version: $($global:InstallerTestDownloads[0])"
Assert-True (@(Get-ChildItem -LiteralPath $sandbox.Temp -Force).Count -eq 0) 'temporary directory is removed'
Assert-True (($output -join "`n") -match 'Installation successful') 'installer reports success'

if ($isWindowsHost) {
Write-Host '--- install succeeds when the temporary directory cannot be removed'
$global:InstallerTestHoldExtractedBinary = $true
$sandbox = New-InstallerSandbox
$output = Invoke-Installer $sandbox
$installed = Join-Path $sandbox.InstallDir 'cli.exe'
Assert-True (Test-Path -LiteralPath $installed) 'binary is copied despite the held temporary file'
Assert-True (($output -join "`n") -match 'Warning: could not remove temporary directory') 'installer warns about the leftover temporary directory'
Assert-True (($output -join "`n") -match 'Installation successful') 'installer still reports success'
Assert-True (@(Get-ChildItem -LiteralPath $sandbox.Temp -Force).Count -eq 1) 'temporary directory is left in place'
$global:InstallerTestOpenHandle.Dispose()
}
else {
Write-Host '--- skipping held-file cleanup case: open files can be deleted on this platform'
}
}
finally {
if ($global:InstallerTestOpenHandle) {
$global:InstallerTestOpenHandle.Dispose()
}
}
9 changes: 7 additions & 2 deletions templates/templates/cli/release/install.ps1.stmpl
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@ function Install-CLI {
}
finally {
# Cleanup
if (Test-Path $tempDir) {
Remove-Item $tempDir -Recurse -Force
try {
if (Test-Path -LiteralPath $tempDir) {
[System.IO.Directory]::Delete($tempDir, $true)
}
}
catch {
Write-ColorOutput "Warning: could not remove temporary directory ${tempDir}: $_" -Color Yellow
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion zSDKs/sdk-cli/.speakeasy/gen.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions zSDKs/sdk-cli/scripts/install.ps1

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading