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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
contents: write # needed to create the GitHub Release and upload its assets via `gh`
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
Expand Down Expand Up @@ -117,7 +117,7 @@ jobs:
vendor_path: scripts/vendor/
steps:
- name: Checkout installer repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand Down
34 changes: 25 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ jobs:
lint-shell:
name: Lint shell
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand Down Expand Up @@ -51,9 +52,10 @@ jobs:
lint-powershell:
name: Lint PowerShell
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand Down Expand Up @@ -98,9 +100,10 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand All @@ -126,14 +129,15 @@ jobs:
test-shell-musl:
name: Test shell (musl / alpine)
runs-on: ubuntu-latest
timeout-minutes: 20
container:
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
steps:
- name: Install checkout compatibility packages
run: apk add --no-cache git gcompat libstdc++

- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand All @@ -146,33 +150,45 @@ jobs:
test-powershell:
name: Test PowerShell
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

# The suite is written for Pester v5; v6 discovers zero tests, so the
# version is pinned and every run fails if nothing was discovered.
- name: Install Pester
shell: pwsh
run: Install-Module -Name Pester -MinimumVersion 5.5 -Force -SkipPublisherCheck
run: Install-Module -Name Pester -MinimumVersion 5.5 -MaximumVersion 5.99 -Force -SkipPublisherCheck

- name: Run Pester (PowerShell 7)
shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.5 -MaximumVersion 5.99 -Force
$config = New-PesterConfiguration
$config.Run.Path = "tests/Install.Tests.ps1"
$config.Run.Exit = $true
$config.Run.PassThru = $true
$config.Output.Verbosity = "Detailed"
$config.Should.ErrorAction = "Stop"
Invoke-Pester -Configuration $config
$result = Invoke-Pester -Configuration $config
Comment on lines 172 to +176
if (-not $result -or $result.TotalCount -lt 1) {
throw "Pester discovered zero tests"
}

- name: Run Pester (Windows PowerShell 5.1)
shell: powershell
run: |
Import-Module Pester -MinimumVersion 5.5 -Force
Import-Module Pester -MinimumVersion 5.5 -MaximumVersion 5.99 -Force
$config = New-PesterConfiguration
$config.Run.Path = "tests/Install.Tests.ps1"
$config.Run.Exit = $true
$config.Run.PassThru = $true
$config.Output.Verbosity = "Detailed"
$config.Should.ErrorAction = "Stop"
Invoke-Pester -Configuration $config
$result = Invoke-Pester -Configuration $config
Comment on lines 187 to +191
if (-not $result -or $result.TotalCount -lt 1) {
throw "Pester discovered zero tests"
}
2 changes: 1 addition & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

Expand Down
34 changes: 28 additions & 6 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,16 @@ function Invoke-Download {

if ($env:CLOUDSMITH_CLI_ALLOW_INSECURE_URLS -eq "true") {
Invoke-WithRetry {
Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Destination -TimeoutSec 300 | Out-Null
# Windows PowerShell applies wildcard matching to -OutFile, so
# download to a temp path and copy to the destination literally.
$tempFile = [System.IO.Path]::GetTempFileName()
try {
Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $tempFile -TimeoutSec 300 | Out-Null
[System.IO.File]::Copy($tempFile, $Destination, $true)
}
finally {
[System.IO.File]::Delete($tempFile)
}
Comment thread
BartoszBlizniak marked this conversation as resolved.
}
return
}
Expand Down Expand Up @@ -412,7 +421,18 @@ try {
Write-InstallerLog "downloading Cloudsmith CLI $resolvedVersion"
Invoke-Download -Uri $archiveUrl -Destination $archiveFile

$actualSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archiveFile).Hash.ToLowerInvariant()
# Hash via .NET rather than Get-FileHash: concurrent Windows PowerShell
# startups can race on the module analysis cache and fail cmdlet discovery.
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$archiveStream = [System.IO.File]::OpenRead($archiveFile)
try {
$hashBytes = $sha256.ComputeHash($archiveStream)
}
finally {
$archiveStream.Dispose()
$sha256.Dispose()
}
$actualSha256 = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
if ($actualSha256 -ne $expectedSha256) {
throw "install.ps1: archive checksum mismatch"
}
Expand Down Expand Up @@ -443,19 +463,21 @@ try {
$utf8WithoutBom = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllLines((Join-Path $stagedDirectory ".cloudsmith-installation"), $installationMetadata, $utf8WithoutBom)

# Directory.Move treats both paths literally; Move-Item -Destination chokes
# on wildcard characters like [ ] in the install root.
$temporaryFinal = Join-Path $finalParent (".cloudsmith.new." + [Guid]::NewGuid().ToString("N"))
$oldFinal = Join-Path $finalParent (".cloudsmith.old." + [Guid]::NewGuid().ToString("N"))
Move-Item -LiteralPath $stagedDirectory -Destination $temporaryFinal
[System.IO.Directory]::Move($stagedDirectory, $temporaryFinal)

if (Test-Path -LiteralPath $binDirectory) {
Move-Item -LiteralPath $binDirectory -Destination $oldFinal
[System.IO.Directory]::Move($binDirectory, $oldFinal)
}
try {
Move-Item -LiteralPath $temporaryFinal -Destination $binDirectory
[System.IO.Directory]::Move($temporaryFinal, $binDirectory)
}
catch {
if (Test-Path -LiteralPath $oldFinal) {
Move-Item -LiteralPath $oldFinal -Destination $binDirectory -ErrorAction SilentlyContinue
try { [System.IO.Directory]::Move($oldFinal, $binDirectory) } catch { Write-Warning "Failed to restore previous installation directory during rollback: $_" }
}
throw
}
Expand Down
35 changes: 23 additions & 12 deletions tests/Install.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ $script:RunningOnWindows = if ($PSVersionTable.PSVersion.Major -ge 6) { $IsWindo
Describe 'Cloudsmith CLI installer (install.ps1)' -Tag 'Windows' -Skip:(-not $script:RunningOnWindows) {

BeforeAll {
$script:InstallScriptPath = (Resolve-Path (Join-Path $PSScriptRoot '..' 'install.ps1')).ProviderPath
Import-Module (Join-Path $PSScriptRoot 'helpers' 'Fixture.psm1') -Force
# Nested Join-Path: the multi-segment form is not available in Windows PowerShell 5.1.
$script:InstallScriptPath = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'install.ps1')).ProviderPath
Import-Module (Join-Path (Join-Path $PSScriptRoot 'helpers') 'Fixture.psm1') -Force

$script:DefaultTarget = 'windows-x86_64'

$script:CandidateHosts = @()
if (Get-Command 'pwsh' -ErrorAction SilentlyContinue) { $script:CandidateHosts += 'pwsh' }
if (Get-Command 'powershell.exe' -ErrorAction SilentlyContinue) { $script:CandidateHosts += 'powershell.exe' }

# Runs install.ps1 as a child process of $HostExe. -EnvironmentOverrides sets env vars for
# the duration of the call ($null removes one) and restores the previous values afterward.
function Invoke-Installer {
Expand All @@ -37,6 +34,9 @@ Describe 'Cloudsmith CLI installer (install.ps1)' -Tag 'Windows' -Skip:(-not $sc
}

try {
# Windows PowerShell turns redirected native stderr into error records,
# which terminate under Pester's ErrorAction Stop; scoped to this function.
$ErrorActionPreference = 'Continue'
$stdOutLines = @(& $HostExe -NoProfile -NoLogo -ExecutionPolicy Bypass -File $script:InstallScriptPath @ScriptArguments 2> $stdErrPath)
$exitCode = $LASTEXITCODE
}
Expand Down Expand Up @@ -108,9 +108,15 @@ Describe 'Cloudsmith CLI installer (install.ps1)' -Tag 'Windows' -Skip:(-not $sc
)
}

foreach ($currentHostExe in $script:CandidateHosts) {
# Runs at discovery (BeforeAll has not executed yet); -ForEach carries the
# host into the run phase as $currentHostExe.
$candidateHosts = @()
if (Get-Command 'pwsh' -ErrorAction SilentlyContinue) { $candidateHosts += 'pwsh' }
if (Get-Command 'powershell.exe' -ErrorAction SilentlyContinue) { $candidateHosts += 'powershell.exe' }

foreach ($currentHostExe in $candidateHosts) {

Context "Host: $currentHostExe" {
Context "Host: $currentHostExe" -ForEach @(@{ currentHostExe = $currentHostExe }) {

BeforeEach {
$script:FixtureDir = Join-Path $TestDrive ("fixture-" + [Guid]::NewGuid().ToString('N'))
Expand Down Expand Up @@ -224,10 +230,10 @@ Describe 'Cloudsmith CLI installer (install.ps1)' -Tag 'Windows' -Skip:(-not $sc
)

$first = Invoke-Installer -HostExe $currentHostExe -ScriptArguments $scriptArgs
$first.ExitCode | Should -Be 0
$first.ExitCode | Should -Be 0 -Because "stderr: $($first.StdErr)"

$second = Invoke-Installer -HostExe $currentHostExe -ScriptArguments ($scriptArgs + '-Force')
$second.ExitCode | Should -Be 0
$second.ExitCode | Should -Be 0 -Because "stderr: $($second.StdErr)"

$expectedBinDir = Get-BinDirectory -InstallRoot $installRoot -Version '3.2.1' -Target $script:DefaultTarget
$expectedExecutable = Join-Path $expectedBinDir 'cloudsmith.exe'
Expand Down Expand Up @@ -570,12 +576,17 @@ Describe 'Cloudsmith CLI installer (install.ps1)' -Tag 'Windows' -Skip:(-not $sc
-RedirectStandardOutput $out1 -RedirectStandardError $err1
$proc2 = Start-Process -FilePath $currentHostExe -ArgumentList $argumentString -NoNewWindow -PassThru `
-RedirectStandardOutput $out2 -RedirectStandardError $err2
# Cache handles now, or ExitCode is null in Windows PowerShell once the process exits.
$null = $proc1.Handle
$null = $proc2.Handle

$proc1.WaitForExit(60000) | Out-Null
$proc2.WaitForExit(60000) | Out-Null

$proc1.ExitCode | Should -Be 0
$proc2.ExitCode | Should -Be 0
$stderr1 = if (Test-Path -LiteralPath $err1) { Get-Content -Raw -LiteralPath $err1 } else { '' }
$stderr2 = if (Test-Path -LiteralPath $err2) { Get-Content -Raw -LiteralPath $err2 } else { '' }
$proc1.ExitCode | Should -Be 0 -Because "stderr: $stderr1"
$proc2.ExitCode | Should -Be 0 -Because "stderr: $stderr2"

$expectedExecutable = Join-Path (Get-BinDirectory -InstallRoot $installRoot -Version '7.7.7' -Target $script:DefaultTarget) 'cloudsmith.exe'
Test-Path -LiteralPath $expectedExecutable | Should -BeTrue
Expand Down
Loading
Loading