From 2d71a449fe8dfde0ae25c128dcc0d074db11ce98 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 20:25:59 +0200 Subject: [PATCH 01/18] Use gh api for GitHub release metadata lookups --- README.md | 6 ++- action.yml | 112 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 106f70a..12aae68 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,14 @@ jobs: | ----- | -------- | ------- | ----------- | | `Version` | `false` | `latest` | Desired PowerShell Core version (e.g. `7.4.1`, `7.6.0-preview.6`). Use `latest` to install the newest stable release (or newest prerelease when `Prerelease` is `true`). | | `Prerelease` | `false` | `false` | Install a prerelease version. When `true` and `Version` is `latest`, resolves to the latest prerelease. Similar to `-Prerelease` on `Install-PSResource`. | +| `Token` | `false` | `${{ github.token }}` | Token used for GitHub API requests. Set to an empty string (`''`) for anonymous API calls. | +| `Host` | `false` | `github.com` | GitHub host used by the CLI/API path. Keep `github.com` for GitHub.com, or set your GHES hostname. | ## Secrets -This action does **not** require any secrets. +This action does **not** require custom secrets by default because it uses `${{ github.token }}`. + +If needed, provide `Token` with a custom PAT. To force anonymous API access, set `Token: ''`. ## Outputs diff --git a/action.yml b/action.yml index c6dd2d3..2b347b8 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,18 @@ inputs: Similar to the `-Prerelease` switch on `Install-PSResource`. required: false default: 'false' + Token: + description: | + Token used for GitHub API calls. + Defaults to github.token. Set to an empty string for anonymous API access. + required: false + default: ${{ github.token }} + Host: + description: | + GitHub host used by gh CLI for API calls. + Use github.com for GitHub.com or your GHES hostname. + required: false + default: github.com runs: using: composite @@ -34,7 +46,9 @@ runs: env: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} run: | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input # Install-PowerShell @@ -42,16 +56,40 @@ runs: echo "Requested version: [$REQUESTED_VERSION]" echo "Prerelease: [$PRERELEASE]" + github_api_get() { + local endpoint="$1" + if command -v gh >/dev/null 2>&1; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi + + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi + + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi + + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" + } + # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) case "${REQUESTED_VERSION:-}" in [Ll][Aa][Tt][Ee][Ss][Tt]) if [[ "$PRERELEASE" == "true" ]]; then REQUESTED_VERSION=$( - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' | + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' ) if [[ -z "$REQUESTED_VERSION" ]]; then @@ -61,11 +99,7 @@ runs: echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" else REQUESTED_VERSION=$( - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/PowerShell/PowerShell/releases/latest | + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | jq -r '.tag_name' | sed 's/^v//' ) if [[ -z "$REQUESTED_VERSION" ]]; then @@ -99,11 +133,7 @@ runs: # Query GitHub Releases API for the actual asset URLs to handle naming # convention differences across releases (e.g. powershell-preview_ vs powershell_). RELEASE_JSON=$( - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" + github_api_get "repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" ) if [[ -z "$RELEASE_JSON" ]]; then echo "Error: Failed to fetch release info for v${REQUESTED_VERSION} from GitHub." @@ -222,7 +252,9 @@ runs: env: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} run: | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input # Install-PowerShell @@ -230,16 +262,40 @@ runs: echo "Requested version: [$REQUESTED_VERSION]" echo "Prerelease: [$PRERELEASE]" + github_api_get() { + local endpoint="$1" + if command -v gh >/dev/null 2>&1; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi + + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi + + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi + + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" + } + # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) case "${REQUESTED_VERSION:-}" in [Ll][Aa][Tt][Ee][Ss][Tt]) if [[ "$PRERELEASE" == "true" ]]; then REQUESTED_VERSION=$( - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' | + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' ) if [[ -z "$REQUESTED_VERSION" ]]; then @@ -249,11 +305,7 @@ runs: echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" else REQUESTED_VERSION=$( - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/PowerShell/PowerShell/releases/latest | + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | jq -r '.tag_name' | sed 's/^v//' ) if [[ -z "$REQUESTED_VERSION" ]]; then @@ -321,7 +373,7 @@ runs: env: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ inputs.Token }} run: | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input # Install-PowerShell @@ -333,9 +385,11 @@ runs: if ($req -and $req.Trim().ToLower() -eq 'latest') { $headers = @{ 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" 'X-GitHub-Api-Version' = '2022-11-28' } + if ($env:GITHUB_TOKEN) { + $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" + } if ($env:PRERELEASE -eq 'true') { $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers $latestRelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 From 515e13535ffea481270ac275ecad1673df6219a6 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 21:26:07 +0200 Subject: [PATCH 02/18] Add action tests for default, explicit, and anonymous token modes --- .github/workflows/Action-Test.yml | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 891c862..a04b472 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -109,3 +109,92 @@ jobs: } Write-Host "Prerelease check passed: '$installed' contains a prerelease segment." } + + ActionTestTokenUseCases: + name: 'ubuntu-latest - [Token use cases]' + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Action-Test (Default token behavior) + uses: ./ + with: + Version: latest + + - name: Verify default token behavior + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Default token behavior failed: expected $expected but got $installed" + } + + - name: Action-Test (Explicit token input) + uses: ./ + with: + Version: latest + Token: ${{ github.token }} + + - name: Verify explicit token input behavior + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Explicit token behavior failed: expected $expected but got $installed" + } + + - name: Action-Test (Anonymous mode) + uses: ./ + with: + Version: latest + Token: '' + + - name: Verify anonymous mode behavior + shell: pwsh + run: | + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Anonymous mode failed: expected $expected but got $installed" + } From 560cd6617c07dc5e0c4263c5210fb43133d8f1fc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 21:31:05 +0200 Subject: [PATCH 03/18] Move workflow verification scripts into reusable script file --- .github/scripts/Verify-InstalledVersion.ps1 | 93 +++++++++++++++ .github/workflows/Action-Test.yml | 124 +------------------- 2 files changed, 97 insertions(+), 120 deletions(-) create mode 100644 .github/scripts/Verify-InstalledVersion.ps1 diff --git a/.github/scripts/Verify-InstalledVersion.ps1 b/.github/scripts/Verify-InstalledVersion.ps1 new file mode 100644 index 0000000..fb9a3e7 --- /dev/null +++ b/.github/scripts/Verify-InstalledVersion.ps1 @@ -0,0 +1,93 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $RequestedVersion, + + [Parameter()] + [string] $GitHubToken +) + +function Get-GitHubApiHeaders { + [CmdletBinding()] + param( + [Parameter()] + [string] $Token + ) + + $headers = @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + + if (-not [string]::IsNullOrWhiteSpace($Token)) { + $headers['Authorization'] = "Bearer $Token" + } + + return $headers +} + +function Resolve-ExpectedVersion { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Version, + + [Parameter()] + [string] $Token + ) + + $resolvedVersion = $Version + $normalizedVersion = $Version.Trim().ToLower() + $headers = Get-GitHubApiHeaders -Token $Token + + if ($normalizedVersion -eq 'prerelease') { + $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers + $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 + + if (-not $latestPrerelease) { + throw 'No prerelease releases found for PowerShell/PowerShell.' + } + + $resolvedVersion = $latestPrerelease.tag_name.TrimStart('v') + Write-Host "Resolved 'prerelease' -> $resolvedVersion" + } elseif ([string]::IsNullOrWhiteSpace($normalizedVersion) -or $normalizedVersion -in @('latest', 'null')) { + $resolvedVersion = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers + ).tag_name.TrimStart('v') + Write-Host "Resolved 'latest' -> $resolvedVersion" + } + + return $resolvedVersion +} + +$expectedVersion = Resolve-ExpectedVersion -Version $RequestedVersion -Token $GitHubToken + +if ($IsWindows) { + # On Windows, verify via the expected install directory to avoid stale PATH resolution. + $isPrerelease = $expectedVersion -match '-' + $majorVersion = ($expectedVersion -split '[\.-]')[0] + $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } + $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" + + Write-Host "Windows: verifying via subprocess at $pwshPath" + + if (Test-Path $pwshPath) { + $installedVersion = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') + } else { + Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" + $installedVersion = ($PSVersionTable.PSVersion).ToString() + } +} else { + $installedVersion = ($PSVersionTable.PSVersion).ToString() +} + +Write-Host "Installed PowerShell version: $installedVersion" +Write-Host "Expected PowerShell version: $expectedVersion" + +if ($installedVersion -ne $expectedVersion) { + throw "Failed: expected $expectedVersion but got $installedVersion" +} + +if ($RequestedVersion.Trim().ToLower() -eq 'prerelease' -and $installedVersion -notmatch '-') { + throw "Prerelease validation failed: installed version '$installedVersion' does not contain a prerelease segment." +} diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index a04b472..ac3916a 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -39,76 +39,8 @@ jobs: - name: Verify installed version shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - # Requested version that came from the matrix - $requested = '${{ matrix.version }}' - - # When 'prerelease' → resolve to latest prerelease - if ($requested.Trim().ToLower() -eq 'prerelease') { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 - if (-not $latestPrerelease) { - throw "No prerelease releases found for PowerShell/PowerShell." - } - $requested = $latestPrerelease.tag_name.TrimStart('v') - Write-Host "Resolved 'prerelease' → $requested" - } - # When empty / 'null' / 'latest' → resolve to latest stable release - elseif ([string]::IsNullOrWhiteSpace($requested) -or - $requested.Trim().ToLower() -in @('latest','null')) { - - $requested = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - Write-Host "Resolved 'latest' → $requested" - } - - # On Windows, always verify by launching pwsh from the known install directory. - # This avoids relying on PATH resolution, which may still point to the pre-installed - # version if the runner's environment hasn't refreshed after the MSI install. - if ($IsWindows) { - $isPrerelease = $requested -match '-' - $majorVersion = ($requested -split '[\.-]')[0] - $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } - $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" - Write-Host "Windows: verifying via subprocess at $pwshPath" - if (Test-Path $pwshPath) { - $installed = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') - } else { - Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" - $installed = ($PSVersionTable.PSVersion).ToString() - } - } else { - $installed = ($PSVersionTable.PSVersion).ToString() - } - Write-Host "Installed PowerShell version: $installed" - Write-Host "Expected PowerShell version: $requested" - - if ($installed -ne $requested) { - throw "Failed: expected $requested but got $installed" - } - - # For prerelease matrix entries, additionally assert the version string - # contains a prerelease segment so we never silently fall back to stable. - $matrixVersion = '${{ matrix.version }}' - if ($matrixVersion.Trim().ToLower() -eq 'prerelease') { - if ($installed -notmatch '-') { - throw "Prerelease validation failed: installed version '$installed' does not contain a prerelease segment." - } - Write-Host "Prerelease check passed: '$installed' contains a prerelease segment." - } + ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ github.token }}' ActionTestTokenUseCases: name: 'ubuntu-latest - [Token use cases]' @@ -126,25 +58,8 @@ jobs: - name: Verify default token behavior shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Default token behavior failed: expected $expected but got $installed" - } + ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' - name: Action-Test (Explicit token input) uses: ./ @@ -154,25 +69,8 @@ jobs: - name: Verify explicit token input behavior shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Explicit token behavior failed: expected $expected but got $installed" - } + ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' - name: Action-Test (Anonymous mode) uses: ./ @@ -183,18 +81,4 @@ jobs: - name: Verify anonymous mode behavior shell: pwsh run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Anonymous mode failed: expected $expected but got $installed" - } + ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' From 5164cc13c6b9091200e80adc7bb70d2f751ce290 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 21:42:44 +0200 Subject: [PATCH 04/18] Revert "Move workflow verification scripts into reusable script file" This reverts commit 560cd6617c07dc5e0c4263c5210fb43133d8f1fc. --- .github/scripts/Verify-InstalledVersion.ps1 | 93 --------------- .github/workflows/Action-Test.yml | 124 +++++++++++++++++++- 2 files changed, 120 insertions(+), 97 deletions(-) delete mode 100644 .github/scripts/Verify-InstalledVersion.ps1 diff --git a/.github/scripts/Verify-InstalledVersion.ps1 b/.github/scripts/Verify-InstalledVersion.ps1 deleted file mode 100644 index fb9a3e7..0000000 --- a/.github/scripts/Verify-InstalledVersion.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string] $RequestedVersion, - - [Parameter()] - [string] $GitHubToken -) - -function Get-GitHubApiHeaders { - [CmdletBinding()] - param( - [Parameter()] - [string] $Token - ) - - $headers = @{ - 'Accept' = 'application/vnd.github+json' - 'X-GitHub-Api-Version' = '2022-11-28' - } - - if (-not [string]::IsNullOrWhiteSpace($Token)) { - $headers['Authorization'] = "Bearer $Token" - } - - return $headers -} - -function Resolve-ExpectedVersion { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string] $Version, - - [Parameter()] - [string] $Token - ) - - $resolvedVersion = $Version - $normalizedVersion = $Version.Trim().ToLower() - $headers = Get-GitHubApiHeaders -Token $Token - - if ($normalizedVersion -eq 'prerelease') { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers - $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 - - if (-not $latestPrerelease) { - throw 'No prerelease releases found for PowerShell/PowerShell.' - } - - $resolvedVersion = $latestPrerelease.tag_name.TrimStart('v') - Write-Host "Resolved 'prerelease' -> $resolvedVersion" - } elseif ([string]::IsNullOrWhiteSpace($normalizedVersion) -or $normalizedVersion -in @('latest', 'null')) { - $resolvedVersion = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers - ).tag_name.TrimStart('v') - Write-Host "Resolved 'latest' -> $resolvedVersion" - } - - return $resolvedVersion -} - -$expectedVersion = Resolve-ExpectedVersion -Version $RequestedVersion -Token $GitHubToken - -if ($IsWindows) { - # On Windows, verify via the expected install directory to avoid stale PATH resolution. - $isPrerelease = $expectedVersion -match '-' - $majorVersion = ($expectedVersion -split '[\.-]')[0] - $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } - $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" - - Write-Host "Windows: verifying via subprocess at $pwshPath" - - if (Test-Path $pwshPath) { - $installedVersion = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') - } else { - Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" - $installedVersion = ($PSVersionTable.PSVersion).ToString() - } -} else { - $installedVersion = ($PSVersionTable.PSVersion).ToString() -} - -Write-Host "Installed PowerShell version: $installedVersion" -Write-Host "Expected PowerShell version: $expectedVersion" - -if ($installedVersion -ne $expectedVersion) { - throw "Failed: expected $expectedVersion but got $installedVersion" -} - -if ($RequestedVersion.Trim().ToLower() -eq 'prerelease' -and $installedVersion -notmatch '-') { - throw "Prerelease validation failed: installed version '$installedVersion' does not contain a prerelease segment." -} diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index ac3916a..a04b472 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -39,8 +39,76 @@ jobs: - name: Verify installed version shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} run: | - ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ github.token }}' + # Requested version that came from the matrix + $requested = '${{ matrix.version }}' + + # When 'prerelease' → resolve to latest prerelease + if ($requested.Trim().ToLower() -eq 'prerelease') { + $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 + if (-not $latestPrerelease) { + throw "No prerelease releases found for PowerShell/PowerShell." + } + $requested = $latestPrerelease.tag_name.TrimStart('v') + Write-Host "Resolved 'prerelease' → $requested" + } + # When empty / 'null' / 'latest' → resolve to latest stable release + elseif ([string]::IsNullOrWhiteSpace($requested) -or + $requested.Trim().ToLower() -in @('latest','null')) { + + $requested = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + Write-Host "Resolved 'latest' → $requested" + } + + # On Windows, always verify by launching pwsh from the known install directory. + # This avoids relying on PATH resolution, which may still point to the pre-installed + # version if the runner's environment hasn't refreshed after the MSI install. + if ($IsWindows) { + $isPrerelease = $requested -match '-' + $majorVersion = ($requested -split '[\.-]')[0] + $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } + $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" + Write-Host "Windows: verifying via subprocess at $pwshPath" + if (Test-Path $pwshPath) { + $installed = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') + } else { + Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" + $installed = ($PSVersionTable.PSVersion).ToString() + } + } else { + $installed = ($PSVersionTable.PSVersion).ToString() + } + Write-Host "Installed PowerShell version: $installed" + Write-Host "Expected PowerShell version: $requested" + + if ($installed -ne $requested) { + throw "Failed: expected $requested but got $installed" + } + + # For prerelease matrix entries, additionally assert the version string + # contains a prerelease segment so we never silently fall back to stable. + $matrixVersion = '${{ matrix.version }}' + if ($matrixVersion.Trim().ToLower() -eq 'prerelease') { + if ($installed -notmatch '-') { + throw "Prerelease validation failed: installed version '$installed' does not contain a prerelease segment." + } + Write-Host "Prerelease check passed: '$installed' contains a prerelease segment." + } ActionTestTokenUseCases: name: 'ubuntu-latest - [Token use cases]' @@ -58,8 +126,25 @@ jobs: - name: Verify default token behavior shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} run: | - ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Default token behavior failed: expected $expected but got $installed" + } - name: Action-Test (Explicit token input) uses: ./ @@ -69,8 +154,25 @@ jobs: - name: Verify explicit token input behavior shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} run: | - ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Explicit token behavior failed: expected $expected but got $installed" + } - name: Action-Test (Anonymous mode) uses: ./ @@ -81,4 +183,18 @@ jobs: - name: Verify anonymous mode behavior shell: pwsh run: | - ./.github/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' + $expected = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` + -Headers @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + ).tag_name.TrimStart('v') + + $installed = ($PSVersionTable.PSVersion).ToString() + Write-Host "Installed: $installed" + Write-Host "Expected : $expected" + + if ($installed -ne $expected) { + throw "Anonymous mode failed: expected $expected but got $installed" + } From e97f8e74bf45238b74c0cf3808c661b05b281871 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 21:44:51 +0200 Subject: [PATCH 05/18] Move action runtime scripts into OS-specific script files --- action.yml | 477 +----------------------------------- scripts/linux/install.sh | 195 +++++++++++++++ scripts/macos/install.sh | 110 +++++++++ scripts/windows/install.ps1 | 172 +++++++++++++ 4 files changed, 480 insertions(+), 474 deletions(-) create mode 100644 scripts/linux/install.sh create mode 100644 scripts/macos/install.sh create mode 100644 scripts/windows/install.ps1 diff --git a/action.yml b/action.yml index 2b347b8..2218875 100644 --- a/action.yml +++ b/action.yml @@ -49,201 +49,7 @@ runs: GITHUB_TOKEN: ${{ inputs.Token }} GH_TOKEN: ${{ inputs.Token }} GH_HOST: ${{ inputs.Host }} - run: - | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input - # Install-PowerShell - set -e - echo "Requested version: [$REQUESTED_VERSION]" - echo "Prerelease: [$PRERELEASE]" - - github_api_get() { - local endpoint="$1" - if command -v gh >/dev/null 2>&1; then - local gh_args=() - if [[ -n "$GH_HOST" ]]; then - gh_args+=(--hostname "$GH_HOST") - fi - gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" - return - fi - - local auth_header=() - if [[ -n "$GITHUB_TOKEN" ]]; then - auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") - fi - - local api_base="https://api.github.com" - if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then - api_base="https://${GH_HOST}/api/v3" - fi - - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - "${auth_header[@]}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "${api_base}/${endpoint}" - } - - # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) - case "${REQUESTED_VERSION:-}" in - [Ll][Aa][Tt][Ee][Ss][Tt]) - if [[ "$PRERELEASE" == "true" ]]; then - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | - jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." - exit 1 - fi - echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" - else - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases/latest' | - jq -r '.tag_name' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: Failed to resolve latest stable PowerShell release from GitHub." - exit 1 - fi - echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" - fi - ;; - "") - echo "Error: Version input is required (or use 'latest')" - exit 1 - ;; - esac - - DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) - if [[ -n "$DETECTED_VERSION" ]]; then - echo "Currently installed PowerShell version: $DETECTED_VERSION" - else - echo "PowerShell is not currently installed" - fi - - if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then - echo "PowerShell $DETECTED_VERSION already installed. Skipping." - exit 0 - fi - - # Determine Linux distribution type - ARCH=$(dpkg --print-architecture 2>/dev/null || rpm --eval '%{_arch}' 2>/dev/null || echo "x86_64") - - # Query GitHub Releases API for the actual asset URLs to handle naming - # convention differences across releases (e.g. powershell-preview_ vs powershell_). - RELEASE_JSON=$( - github_api_get "repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" - ) - if [[ -z "$RELEASE_JSON" ]]; then - echo "Error: Failed to fetch release info for v${REQUESTED_VERSION} from GitHub." - exit 1 - fi - - # Determine if the requested version is a prerelease (contains a hyphen, e.g. 7.6.0-preview.6) - IS_PRERELEASE=false - if [[ "$REQUESTED_VERSION" == *-* ]]; then - IS_PRERELEASE=true - fi - - if command -v apt-get >/dev/null || command -v dpkg >/dev/null; then - # Debian/Ubuntu based - echo "Detected Debian/Ubuntu based system..." - if [[ "$IS_PRERELEASE" == "true" ]]; then - # Prerelease .deb naming varies across releases: - # Older: powershell-preview_7.4.0-preview.5-1.deb_amd64.deb - # Newer: powershell_7.6.0-preview.6-1.deb_amd64.deb - # Try powershell-preview_ first, then fall back to powershell_ - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-preview_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - if [[ -z "$URL" ]]; then - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - fi - else - # For stable versions, select the powershell package (not powershell-lts or powershell-preview) - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - fi - if [[ -z "$URL" ]]; then - echo "Error: No .deb package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." - exit 1 - fi - DEB_NAME=$(basename "$URL") - echo "Downloading from: $URL" - wget -q "$URL" -O "$DEB_NAME" - - # Remove all existing PowerShell packages to avoid dpkg conflicts - # (powershell, powershell-lts, and powershell-preview all provide /usr/bin/pwsh) - echo "Removing existing PowerShell packages to avoid conflicts..." - sudo dpkg --remove powershell powershell-lts powershell-preview 2>/dev/null || true - - echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." - sudo dpkg -i "$DEB_NAME" || sudo apt-get -f install -y - elif command -v rpm >/dev/null; then - # RHEL/Fedora/CentOS based - echo "Detected RHEL/Fedora/CentOS based system..." - if [[ "$IS_PRERELEASE" == "true" ]]; then - # Prerelease .rpm naming varies across releases: - # Older: powershell-preview-7.4.0_preview.5-1.rh.x86_64.rpm - # Newer: powershell-7.6.0_preview.6-1.rh.x86_64.rpm - # Try powershell-preview first, then fall back to powershell- - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-preview.*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - if [[ -z "$URL" ]]; then - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - fi - else - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - fi - if [[ -z "$URL" ]]; then - echo "Error: No .rpm package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." - exit 1 - fi - RPM_NAME=$(basename "$URL") - echo "Downloading from: $URL" - wget -q "$URL" -O "$RPM_NAME" - - # Remove existing PowerShell packages to avoid conflicts - echo "Removing existing PowerShell packages to avoid conflicts..." - sudo rpm -e powershell powershell-preview 2>/dev/null || true - - echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." - sudo rpm -i "$RPM_NAME" || sudo yum install -y "$RPM_NAME" - else - echo "Unsupported Linux distribution. Cannot determine package format." - exit 1 - fi - - # Determine the install directory and add to PATH before verification. - # Preview builds install to /opt/microsoft/powershell/-preview/ - # which is not on the default PATH after removing the old powershell package. - MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) - if [[ "$IS_PRERELEASE" == "true" ]]; then - INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}-preview" - else - INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}" - fi - if [[ -d "$INSTALL_DIR" ]]; then - export PATH="$INSTALL_DIR:$PATH" - fi - - # Verify installation succeeded - INSTALLED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) - if [[ "$INSTALLED_VERSION" != "$REQUESTED_VERSION" ]]; then - echo "Error: Installation verification failed. Expected $REQUESTED_VERSION but got ${INSTALLED_VERSION:-nothing}." - exit 1 - fi - echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." - - # For prerelease builds, add the install directory to GITHUB_PATH so subsequent - # `shell: pwsh` steps resolve to the version we just installed. - if [[ "$IS_PRERELEASE" == "true" && -d "$INSTALL_DIR" ]]; then - echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" - echo "$INSTALL_DIR" >> "$GITHUB_PATH" - fi + run: bash ./scripts/linux/install.sh # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input - name: Install PowerShell (macOS) if: runner.os == 'macOS' @@ -255,116 +61,7 @@ runs: GITHUB_TOKEN: ${{ inputs.Token }} GH_TOKEN: ${{ inputs.Token }} GH_HOST: ${{ inputs.Host }} - run: - | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input - # Install-PowerShell - set -e - echo "Requested version: [$REQUESTED_VERSION]" - echo "Prerelease: [$PRERELEASE]" - - github_api_get() { - local endpoint="$1" - if command -v gh >/dev/null 2>&1; then - local gh_args=() - if [[ -n "$GH_HOST" ]]; then - gh_args+=(--hostname "$GH_HOST") - fi - gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" - return - fi - - local auth_header=() - if [[ -n "$GITHUB_TOKEN" ]]; then - auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") - fi - - local api_base="https://api.github.com" - if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then - api_base="https://${GH_HOST}/api/v3" - fi - - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - "${auth_header[@]}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "${api_base}/${endpoint}" - } - - # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) - case "${REQUESTED_VERSION:-}" in - [Ll][Aa][Tt][Ee][Ss][Tt]) - if [[ "$PRERELEASE" == "true" ]]; then - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | - jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." - exit 1 - fi - echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" - else - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases/latest' | - jq -r '.tag_name' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: Failed to resolve latest stable PowerShell release from GitHub." - exit 1 - fi - echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" - fi - ;; - "") - echo "Error: Version input is required (or use 'latest')" - exit 1 - ;; - esac - - DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) - if [[ -n "$DETECTED_VERSION" ]]; then - echo "Currently installed PowerShell version: $DETECTED_VERSION" - else - echo "PowerShell is not currently installed" - fi - - if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then - echo "PowerShell $DETECTED_VERSION already installed. Skipping." - exit 0 - fi - - # Determine architecture and download appropriate package - ARCH=$(uname -m) - case "$ARCH" in - "arm64") PKG_NAME="powershell-${REQUESTED_VERSION}-osx-arm64.pkg" ;; - *) PKG_NAME="powershell-${REQUESTED_VERSION}-osx-x64.pkg" ;; - esac - - URL="https://github.com/PowerShell/PowerShell/releases/download/v${REQUESTED_VERSION}/${PKG_NAME}" - echo "Downloading from: $URL" - echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." - - if ! curl -sSL "$URL" -o "$PKG_NAME"; then - echo "Error: Failed to download PowerShell package" - exit 1 - fi - sudo installer -pkg "$PKG_NAME" -target / - echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." - - # For prerelease builds, add the install directory to GITHUB_PATH so subsequent - # `shell: pwsh` steps resolve to the version we just installed. - if [[ "$REQUESTED_VERSION" == *-* ]]; then - MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) - if [[ "$MAJOR_VERSION" =~ ^[0-9]+$ ]]; then - INSTALL_DIR="/usr/local/microsoft/powershell/${MAJOR_VERSION}-preview" - if [[ -d "$INSTALL_DIR" ]]; then - echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" - echo "$INSTALL_DIR" >> "$GITHUB_PATH" - fi - else - echo "Warning: Computed MAJOR_VERSION ('$MAJOR_VERSION') is invalid; skipping GITHUB_PATH update." >&2 - fi - fi + run: bash ./scripts/macos/install.sh # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input - name: Install PowerShell (Windows) if: runner.os == 'Windows' @@ -374,172 +71,4 @@ runs: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} GITHUB_TOKEN: ${{ inputs.Token }} - run: - | # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input - # Install-PowerShell - Write-Host "Requested version: [$env:REQUESTED_VERSION]" - Write-Host "Prerelease: [$env:PRERELEASE]" - - # Resolve 'latest' → concrete version - $req = $env:REQUESTED_VERSION - if ($req -and $req.Trim().ToLower() -eq 'latest') { - $headers = @{ - 'Accept' = 'application/vnd.github+json' - 'X-GitHub-Api-Version' = '2022-11-28' - } - if ($env:GITHUB_TOKEN) { - $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" - } - if ($env:PRERELEASE -eq 'true') { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers - $latestRelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 - if (-not $latestRelease) { - Write-Host "Error: No prerelease PowerShell releases are available from GitHub." - exit 1 - } - $latest = $latestRelease.tag_name.TrimStart('v') - Write-Host "Latest prerelease PowerShell version detected: $latest" - } else { - $latest = (Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers).tag_name.TrimStart('v') - if (-not $latest) { - Write-Host "Error: Failed to resolve latest stable PowerShell release from GitHub." - exit 1 - } - Write-Host "Latest stable PowerShell release detected: $latest" - } - $env:REQUESTED_VERSION = $latest - } elseif ([string]::IsNullOrWhiteSpace($req)) { - Write-Host "Error: Version input is required (or use 'latest')" - exit 1 - } - - # Detect currently installed version (if any) - $detected = $null - try { - $detected = (pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') - Write-Host "Currently installed PowerShell version: $detected" - } catch { - Write-Host "PowerShell is not currently installed" - } - - if ($detected -eq $env:REQUESTED_VERSION) { - Write-Host "PowerShell $detected already installed. Skipping." - exit 0 - } - - # Downgrade detection - # Strip prerelease suffix for [version] comparison (e.g. '7.6.0-preview.6' → '7.6.0') - $isDowngrade = $false - if ($detected -and $detected -ne $env:REQUESTED_VERSION) { - try { - $detectedBase = ($detected -split '-')[0] - $requestedBase = ($env:REQUESTED_VERSION -split '-')[0] - $detectedVersion = [version]$detectedBase - $requestedVersion = [version]$requestedBase - if ($detectedVersion -gt $requestedVersion) { - Write-Host "Downgrade detected: $detected → $($env:REQUESTED_VERSION)" - $isDowngrade = $true - } elseif ($detectedVersion -eq $requestedVersion -and $detected -ne $env:REQUESTED_VERSION) { - # Same base version but different prerelease label — MSI installers cannot - # handle cross-prerelease changes in-place, so force uninstall first. - Write-Host "Prerelease version change detected (same base, different label): $detected → $($env:REQUESTED_VERSION)" - $isDowngrade = $true - } else { - Write-Host "Upgrade detected: $detected → $($env:REQUESTED_VERSION)" - } - } catch { - Write-Host "Warning: Could not compare versions, proceeding with regular installation" - } - } - - # If downgrade → fully uninstall current PowerShell 7 - if ($isDowngrade) { - Write-Host "Uninstalling existing PowerShell version before downgrade..." - - # Search both 64-bit and 32-bit uninstall hives - $regPaths = @( - 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' - ) - - $isDetectedPreview = $detected -match '-preview|-rc' - $pwshEntries = Get-ItemProperty -Path $regPaths -ErrorAction SilentlyContinue | - Where-Object { - $_.Publisher -eq 'Microsoft Corporation' -and - $_.DisplayName -like 'PowerShell 7*' -and - $(if ($isDetectedPreview) { $_.DisplayName -like '*Preview*' } else { $_.DisplayName -notlike '*Preview*' }) -and - $_.DisplayVersion -and - ($_.DisplayVersion -match "^$([regex]::Escape(($detected -split '-')[0]))([.\-]|$)" -or $_.DisplayVersion -eq ($detected -split '-')[0]) - } - - $targetEntry = $pwshEntries | Select-Object -First 1 - if (-not $targetEntry) { - Write-Host "Warning: Could not find an uninstall entry for PowerShell $detected" - } else { - $uninstallCmd = if ($targetEntry.QuietUninstallString) { - $targetEntry.QuietUninstallString - } else { - $targetEntry.UninstallString - } - - # If the uninstall command is MSI-based and lacks /quiet, add it - if ($uninstallCmd -match 'msiexec') { - if ($uninstallCmd -notmatch '/quiet') { $uninstallCmd += ' /quiet' } - if ($uninstallCmd -notmatch '/norestart') { $uninstallCmd += ' /norestart' } - } - - Write-Host "Running uninstall command:`n$uninstallCmd" - $proc = Start-Process 'cmd.exe' -ArgumentList '/c', $uninstallCmd -Wait -PassThru - if ($proc.ExitCode -ne 0) { - Write-Host "Error: Uninstall failed (exit code $($proc.ExitCode))." - exit 1 - } - - # Double-check removal - try { - $after = (pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') - if ($after) { - Write-Host "Error: PowerShell is still present ($after) after uninstall. Aborting downgrade." - exit 1 - } - } catch { } - } - } - - # Download requested MSI - $msi = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.msi" - $url = "https://github.com/PowerShell/PowerShell/releases/download/v$($env:REQUESTED_VERSION)/$msi" - Write-Host "Downloading from: $url" - - $null = Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop - - # Install requested version - Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)]..." - $msiProcess = Start-Process msiexec.exe -ArgumentList '/i', $msi, '/quiet', '/norestart' -Wait -PassThru - if ($msiProcess.ExitCode -ne 0) { - Write-Host "Error: Installation failed (exit code $($msiProcess.ExitCode))." - exit 1 - } - - Write-Host "Installation complete. PowerShell [$($env:REQUESTED_VERSION)] is now available." - - # Add the install directory to GITHUB_PATH so subsequent `shell: pwsh` steps - # resolve to the version we just installed — even for preview builds whose - # install directory (7-preview) is not on the runner's default PATH. - $isPrerelease = $env:REQUESTED_VERSION -match '-' - $majorVersion = ($env:REQUESTED_VERSION -split '[.\-]')[0] - if ($majorVersion -match '^\d+$') { - $installDir = if ($isPrerelease) { - "$env:ProgramFiles\PowerShell\$majorVersion-preview" - } else { - "$env:ProgramFiles\PowerShell\$majorVersion" - } - if (Test-Path $installDir) { - Write-Host "Adding install directory to GITHUB_PATH: $installDir" - Add-Content -Path $env:GITHUB_PATH -Value $installDir - } else { - Write-Host "Warning: Expected install directory not found: $installDir" - } - } else { - Write-Host "Warning: Computed major version ('$majorVersion') is invalid; skipping GITHUB_PATH update." - } + run: ./scripts/windows/install.ps1 # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh new file mode 100644 index 0000000..6829b21 --- /dev/null +++ b/scripts/linux/install.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# Install-PowerShell +set -e + +echo "Requested version: [$REQUESTED_VERSION]" +echo "Prerelease: [$PRERELEASE]" + +github_api_get() { + local endpoint="$1" + if command -v gh >/dev/null 2>&1; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi + + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi + + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi + + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" +} + +# Only resolve to latest version if explicitly set to 'latest' (case-insensitive) +case "${REQUESTED_VERSION:-}" in + [Ll][Aa][Tt][Ee][Ss][Tt]) + if [[ "$PRERELEASE" == "true" ]]; then + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | + jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." + exit 1 + fi + echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" + else + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | + jq -r '.tag_name' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: Failed to resolve latest stable PowerShell release from GitHub." + exit 1 + fi + echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" + fi + ;; + "") + echo "Error: Version input is required (or use 'latest')" + exit 1 + ;; +esac + +DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) +if [[ -n "$DETECTED_VERSION" ]]; then + echo "Currently installed PowerShell version: $DETECTED_VERSION" +else + echo "PowerShell is not currently installed" +fi + +if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then + echo "PowerShell $DETECTED_VERSION already installed. Skipping." + exit 0 +fi + +# Determine Linux distribution type +ARCH=$(dpkg --print-architecture 2>/dev/null || rpm --eval '%{_arch}' 2>/dev/null || echo "x86_64") + +# Query GitHub Releases API for the actual asset URLs to handle naming +# convention differences across releases (e.g. powershell-preview_ vs powershell_). +RELEASE_JSON=$( + github_api_get "repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" +) +if [[ -z "$RELEASE_JSON" ]]; then + echo "Error: Failed to fetch release info for v${REQUESTED_VERSION} from GitHub." + exit 1 +fi + +# Determine if the requested version is a prerelease (contains a hyphen, e.g. 7.6.0-preview.6) +IS_PRERELEASE=false +if [[ "$REQUESTED_VERSION" == *-* ]]; then + IS_PRERELEASE=true +fi + +if command -v apt-get >/dev/null || command -v dpkg >/dev/null; then + # Debian/Ubuntu based + echo "Detected Debian/Ubuntu based system..." + if [[ "$IS_PRERELEASE" == "true" ]]; then + # Prerelease .deb naming varies across releases: + # Older: powershell-preview_7.4.0-preview.5-1.deb_amd64.deb + # Newer: powershell_7.6.0-preview.6-1.deb_amd64.deb + # Try powershell-preview_ first, then fall back to powershell_ + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-preview_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + if [[ -z "$URL" ]]; then + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + fi + else + # For stable versions, select the powershell package (not powershell-lts or powershell-preview) + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + fi + if [[ -z "$URL" ]]; then + echo "Error: No .deb package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." + exit 1 + fi + DEB_NAME=$(basename "$URL") + echo "Downloading from: $URL" + wget -q "$URL" -O "$DEB_NAME" + + # Remove all existing PowerShell packages to avoid dpkg conflicts + # (powershell, powershell-lts, and powershell-preview all provide /usr/bin/pwsh) + echo "Removing existing PowerShell packages to avoid conflicts..." + sudo dpkg --remove powershell powershell-lts powershell-preview 2>/dev/null || true + + echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." + sudo dpkg -i "$DEB_NAME" || sudo apt-get -f install -y +elif command -v rpm >/dev/null; then + # RHEL/Fedora/CentOS based + echo "Detected RHEL/Fedora/CentOS based system..." + if [[ "$IS_PRERELEASE" == "true" ]]; then + # Prerelease .rpm naming varies across releases: + # Older: powershell-preview-7.4.0_preview.5-1.rh.x86_64.rpm + # Newer: powershell-7.6.0_preview.6-1.rh.x86_64.rpm + # Try powershell-preview first, then fall back to powershell- + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-preview.*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + if [[ -z "$URL" ]]; then + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + fi + else + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + fi + if [[ -z "$URL" ]]; then + echo "Error: No .rpm package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." + exit 1 + fi + RPM_NAME=$(basename "$URL") + echo "Downloading from: $URL" + wget -q "$URL" -O "$RPM_NAME" + + # Remove existing PowerShell packages to avoid conflicts + echo "Removing existing PowerShell packages to avoid conflicts..." + sudo rpm -e powershell powershell-preview 2>/dev/null || true + + echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." + sudo rpm -i "$RPM_NAME" || sudo yum install -y "$RPM_NAME" +else + echo "Unsupported Linux distribution. Cannot determine package format." + exit 1 +fi + +# Determine the install directory and add to PATH before verification. +# Preview builds install to /opt/microsoft/powershell/-preview/ +# which is not on the default PATH after removing the old powershell package. +MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) +if [[ "$IS_PRERELEASE" == "true" ]]; then + INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}-preview" +else + INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}" +fi +if [[ -d "$INSTALL_DIR" ]]; then + export PATH="$INSTALL_DIR:$PATH" +fi + +# Verify installation succeeded +INSTALLED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) +if [[ "$INSTALLED_VERSION" != "$REQUESTED_VERSION" ]]; then + echo "Error: Installation verification failed. Expected $REQUESTED_VERSION but got ${INSTALLED_VERSION:-nothing}." + exit 1 +fi +echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." + +# For prerelease builds, add the install directory to GITHUB_PATH so subsequent +# `shell: pwsh` steps resolve to the version we just installed. +if [[ "$IS_PRERELEASE" == "true" && -d "$INSTALL_DIR" ]]; then + echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" + echo "$INSTALL_DIR" >> "$GITHUB_PATH" +fi diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh new file mode 100644 index 0000000..755b961 --- /dev/null +++ b/scripts/macos/install.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Install-PowerShell +set -e + +echo "Requested version: [$REQUESTED_VERSION]" +echo "Prerelease: [$PRERELEASE]" + +github_api_get() { + local endpoint="$1" + if command -v gh >/dev/null 2>&1; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi + + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi + + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi + + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" +} + +# Only resolve to latest version if explicitly set to 'latest' (case-insensitive) +case "${REQUESTED_VERSION:-}" in + [Ll][Aa][Tt][Ee][Ss][Tt]) + if [[ "$PRERELEASE" == "true" ]]; then + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | + jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." + exit 1 + fi + echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" + else + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | + jq -r '.tag_name' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: Failed to resolve latest stable PowerShell release from GitHub." + exit 1 + fi + echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" + fi + ;; + "") + echo "Error: Version input is required (or use 'latest')" + exit 1 + ;; +esac + +DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) +if [[ -n "$DETECTED_VERSION" ]]; then + echo "Currently installed PowerShell version: $DETECTED_VERSION" +else + echo "PowerShell is not currently installed" +fi + +if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then + echo "PowerShell $DETECTED_VERSION already installed. Skipping." + exit 0 +fi + +# Determine architecture and download appropriate package +ARCH=$(uname -m) +case "$ARCH" in + "arm64") PKG_NAME="powershell-${REQUESTED_VERSION}-osx-arm64.pkg" ;; + *) PKG_NAME="powershell-${REQUESTED_VERSION}-osx-x64.pkg" ;; +esac + +URL="https://github.com/PowerShell/PowerShell/releases/download/v${REQUESTED_VERSION}/${PKG_NAME}" +echo "Downloading from: $URL" +echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." + +if ! curl -sSL "$URL" -o "$PKG_NAME"; then + echo "Error: Failed to download PowerShell package" + exit 1 +fi +sudo installer -pkg "$PKG_NAME" -target / +echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." + +# For prerelease builds, add the install directory to GITHUB_PATH so subsequent +# `shell: pwsh` steps resolve to the version we just installed. +if [[ "$REQUESTED_VERSION" == *-* ]]; then + MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) + if [[ "$MAJOR_VERSION" =~ ^[0-9]+$ ]]; then + INSTALL_DIR="/usr/local/microsoft/powershell/${MAJOR_VERSION}-preview" + if [[ -d "$INSTALL_DIR" ]]; then + echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" + echo "$INSTALL_DIR" >> "$GITHUB_PATH" + fi + else + echo "Warning: Computed MAJOR_VERSION ('$MAJOR_VERSION') is invalid; skipping GITHUB_PATH update." >&2 + fi +fi diff --git a/scripts/windows/install.ps1 b/scripts/windows/install.ps1 new file mode 100644 index 0000000..bee4d00 --- /dev/null +++ b/scripts/windows/install.ps1 @@ -0,0 +1,172 @@ +# Install-PowerShell +Write-Host "Requested version: [$env:REQUESTED_VERSION]" +Write-Host "Prerelease: [$env:PRERELEASE]" + +# Resolve 'latest' -> concrete version +$req = $env:REQUESTED_VERSION +if ($req -and $req.Trim().ToLower() -eq 'latest') { + $headers = @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + if ($env:GITHUB_TOKEN) { + $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" + } + if ($env:PRERELEASE -eq 'true') { + $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers + $latestRelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 + if (-not $latestRelease) { + Write-Host 'Error: No prerelease PowerShell releases are available from GitHub.' + exit 1 + } + $latest = $latestRelease.tag_name.TrimStart('v') + Write-Host "Latest prerelease PowerShell version detected: $latest" + } else { + $latest = (Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers).tag_name.TrimStart('v') + if (-not $latest) { + Write-Host 'Error: Failed to resolve latest stable PowerShell release from GitHub.' + exit 1 + } + Write-Host "Latest stable PowerShell release detected: $latest" + } + $env:REQUESTED_VERSION = $latest +} elseif ([string]::IsNullOrWhiteSpace($req)) { + Write-Host "Error: Version input is required (or use 'latest')" + exit 1 +} + +# Detect currently installed version (if any) +$detected = $null +try { + $detected = (pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') + Write-Host "Currently installed PowerShell version: $detected" +} catch { + Write-Host 'PowerShell is not currently installed' +} + +if ($detected -eq $env:REQUESTED_VERSION) { + Write-Host "PowerShell $detected already installed. Skipping." + exit 0 +} + +# Downgrade detection +# Strip prerelease suffix for [version] comparison (e.g. '7.6.0-preview.6' -> '7.6.0') +$isDowngrade = $false +if ($detected -and $detected -ne $env:REQUESTED_VERSION) { + try { + $detectedBase = ($detected -split '-')[0] + $requestedBase = ($env:REQUESTED_VERSION -split '-')[0] + $detectedVersion = [version]$detectedBase + $requestedVersion = [version]$requestedBase + if ($detectedVersion -gt $requestedVersion) { + Write-Host "Downgrade detected: $detected -> $($env:REQUESTED_VERSION)" + $isDowngrade = $true + } elseif ($detectedVersion -eq $requestedVersion -and $detected -ne $env:REQUESTED_VERSION) { + # Same base version but different prerelease label - MSI installers cannot + # handle cross-prerelease changes in-place, so force uninstall first. + Write-Host "Prerelease version change detected (same base, different label): $detected -> $($env:REQUESTED_VERSION)" + $isDowngrade = $true + } else { + Write-Host "Upgrade detected: $detected -> $($env:REQUESTED_VERSION)" + } + } catch { + Write-Host 'Warning: Could not compare versions, proceeding with regular installation' + } +} + +# If downgrade -> fully uninstall current PowerShell 7 +if ($isDowngrade) { + Write-Host 'Uninstalling existing PowerShell version before downgrade...' + + # Search both 64-bit and 32-bit uninstall hives + $regPaths = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' + ) + + $isDetectedPreview = $detected -match '-preview|-rc' + $pwshEntries = Get-ItemProperty -Path $regPaths -ErrorAction SilentlyContinue | + Where-Object { + $_.Publisher -eq 'Microsoft Corporation' -and + $_.DisplayName -like 'PowerShell 7*' -and + $(if ($isDetectedPreview) { $_.DisplayName -like '*Preview*' } else { $_.DisplayName -notlike '*Preview*' }) -and + $_.DisplayVersion -and + ($_.DisplayVersion -match "^$([regex]::Escape(($detected -split '-')[0]))([.\-]|$)" -or $_.DisplayVersion -eq ($detected -split '-')[0]) + } + + $targetEntry = $pwshEntries | Select-Object -First 1 + if (-not $targetEntry) { + Write-Host "Warning: Could not find an uninstall entry for PowerShell $detected" + } else { + $uninstallCmd = if ($targetEntry.QuietUninstallString) { + $targetEntry.QuietUninstallString + } else { + $targetEntry.UninstallString + } + + # If the uninstall command is MSI-based and lacks /quiet, add it + if ($uninstallCmd -match 'msiexec') { + if ($uninstallCmd -notmatch '/quiet') { + $uninstallCmd += ' /quiet' + } + if ($uninstallCmd -notmatch '/norestart') { + $uninstallCmd += ' /norestart' + } + } + + Write-Host "Running uninstall command:`n$uninstallCmd" + $proc = Start-Process 'cmd.exe' -ArgumentList '/c', $uninstallCmd -Wait -PassThru + if ($proc.ExitCode -ne 0) { + Write-Host "Error: Uninstall failed (exit code $($proc.ExitCode))." + exit 1 + } + + # Double-check removal + try { + $after = (pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') + if ($after) { + Write-Host "Error: PowerShell is still present ($after) after uninstall. Aborting downgrade." + exit 1 + } + } catch { + } + } +} + +# Download requested MSI +$msi = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.msi" +$url = "https://github.com/PowerShell/PowerShell/releases/download/v$($env:REQUESTED_VERSION)/$msi" +Write-Host "Downloading from: $url" + +$null = Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop + +# Install requested version +Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)]..." +$msiProcess = Start-Process msiexec.exe -ArgumentList '/i', $msi, '/quiet', '/norestart' -Wait -PassThru +if ($msiProcess.ExitCode -ne 0) { + Write-Host "Error: Installation failed (exit code $($msiProcess.ExitCode))." + exit 1 +} + +Write-Host "Installation complete. PowerShell [$($env:REQUESTED_VERSION)] is now available." + +# Add the install directory to GITHUB_PATH so subsequent `shell: pwsh` steps +# resolve to the version we just installed - even for preview builds whose +# install directory (7-preview) is not on the runner's default PATH. +$isPrerelease = $env:REQUESTED_VERSION -match '-' +$majorVersion = ($env:REQUESTED_VERSION -split '[.\-]')[0] +if ($majorVersion -match '^\d+$') { + $installDir = if ($isPrerelease) { + "$env:ProgramFiles\PowerShell\$majorVersion-preview" + } else { + "$env:ProgramFiles\PowerShell\$majorVersion" + } + if (Test-Path $installDir) { + Write-Host "Adding install directory to GITHUB_PATH: $installDir" + Add-Content -Path $env:GITHUB_PATH -Value $installDir + } else { + Write-Host "Warning: Expected install directory not found: $installDir" + } +} else { + Write-Host "Warning: Computed major version ('$majorVersion') is invalid; skipping GITHUB_PATH update." +} From 2407dde6adca52c2ab993741b875232411e94910 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:10:52 +0200 Subject: [PATCH 06/18] Mark Linux and macOS install scripts as executable --- scripts/linux/install.sh | 0 scripts/macos/install.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/linux/install.sh mode change 100644 => 100755 scripts/macos/install.sh diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh old mode 100644 new mode 100755 diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh old mode 100644 new mode 100755 From 2506744985fc65f4eefdd7a58bd526004b617297 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:17:03 +0200 Subject: [PATCH 07/18] Move workflow verification logic to test/scripts --- .github/workflows/Action-Test.yml | 126 +---------------------- test/scripts/Verify-InstalledVersion.ps1 | 93 +++++++++++++++++ 2 files changed, 98 insertions(+), 121 deletions(-) create mode 100644 test/scripts/Verify-InstalledVersion.ps1 diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index a04b472..a19d56f 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -1,6 +1,6 @@ name: Action-Test -run-name: "Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" +run-name: 'Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}' on: workflow_dispatch: @@ -39,76 +39,8 @@ jobs: - name: Verify installed version shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - # Requested version that came from the matrix - $requested = '${{ matrix.version }}' - - # When 'prerelease' → resolve to latest prerelease - if ($requested.Trim().ToLower() -eq 'prerelease') { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 - if (-not $latestPrerelease) { - throw "No prerelease releases found for PowerShell/PowerShell." - } - $requested = $latestPrerelease.tag_name.TrimStart('v') - Write-Host "Resolved 'prerelease' → $requested" - } - # When empty / 'null' / 'latest' → resolve to latest stable release - elseif ([string]::IsNullOrWhiteSpace($requested) -or - $requested.Trim().ToLower() -in @('latest','null')) { - - $requested = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - Write-Host "Resolved 'latest' → $requested" - } - - # On Windows, always verify by launching pwsh from the known install directory. - # This avoids relying on PATH resolution, which may still point to the pre-installed - # version if the runner's environment hasn't refreshed after the MSI install. - if ($IsWindows) { - $isPrerelease = $requested -match '-' - $majorVersion = ($requested -split '[\.-]')[0] - $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } - $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" - Write-Host "Windows: verifying via subprocess at $pwshPath" - if (Test-Path $pwshPath) { - $installed = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') - } else { - Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" - $installed = ($PSVersionTable.PSVersion).ToString() - } - } else { - $installed = ($PSVersionTable.PSVersion).ToString() - } - Write-Host "Installed PowerShell version: $installed" - Write-Host "Expected PowerShell version: $requested" - - if ($installed -ne $requested) { - throw "Failed: expected $requested but got $installed" - } - - # For prerelease matrix entries, additionally assert the version string - # contains a prerelease segment so we never silently fall back to stable. - $matrixVersion = '${{ matrix.version }}' - if ($matrixVersion.Trim().ToLower() -eq 'prerelease') { - if ($installed -notmatch '-') { - throw "Prerelease validation failed: installed version '$installed' does not contain a prerelease segment." - } - Write-Host "Prerelease check passed: '$installed' contains a prerelease segment." - } + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ github.token }}' ActionTestTokenUseCases: name: 'ubuntu-latest - [Token use cases]' @@ -126,25 +58,8 @@ jobs: - name: Verify default token behavior shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Default token behavior failed: expected $expected but got $installed" - } + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' - name: Action-Test (Explicit token input) uses: ./ @@ -154,25 +69,8 @@ jobs: - name: Verify explicit token input behavior shell: pwsh - env: - GITHUB_TOKEN: ${{ github.token }} run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Explicit token behavior failed: expected $expected but got $installed" - } + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' - name: Action-Test (Anonymous mode) uses: ./ @@ -183,18 +81,4 @@ jobs: - name: Verify anonymous mode behavior shell: pwsh run: | - $expected = ( - Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' ` - -Headers @{ - 'Accept' = 'application/vnd.github+json' - 'X-GitHub-Api-Version' = '2022-11-28' - } - ).tag_name.TrimStart('v') - - $installed = ($PSVersionTable.PSVersion).ToString() - Write-Host "Installed: $installed" - Write-Host "Expected : $expected" - - if ($installed -ne $expected) { - throw "Anonymous mode failed: expected $expected but got $installed" - } + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' diff --git a/test/scripts/Verify-InstalledVersion.ps1 b/test/scripts/Verify-InstalledVersion.ps1 new file mode 100644 index 0000000..fb9a3e7 --- /dev/null +++ b/test/scripts/Verify-InstalledVersion.ps1 @@ -0,0 +1,93 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $RequestedVersion, + + [Parameter()] + [string] $GitHubToken +) + +function Get-GitHubApiHeaders { + [CmdletBinding()] + param( + [Parameter()] + [string] $Token + ) + + $headers = @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + + if (-not [string]::IsNullOrWhiteSpace($Token)) { + $headers['Authorization'] = "Bearer $Token" + } + + return $headers +} + +function Resolve-ExpectedVersion { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Version, + + [Parameter()] + [string] $Token + ) + + $resolvedVersion = $Version + $normalizedVersion = $Version.Trim().ToLower() + $headers = Get-GitHubApiHeaders -Token $Token + + if ($normalizedVersion -eq 'prerelease') { + $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers + $latestPrerelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 + + if (-not $latestPrerelease) { + throw 'No prerelease releases found for PowerShell/PowerShell.' + } + + $resolvedVersion = $latestPrerelease.tag_name.TrimStart('v') + Write-Host "Resolved 'prerelease' -> $resolvedVersion" + } elseif ([string]::IsNullOrWhiteSpace($normalizedVersion) -or $normalizedVersion -in @('latest', 'null')) { + $resolvedVersion = ( + Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers + ).tag_name.TrimStart('v') + Write-Host "Resolved 'latest' -> $resolvedVersion" + } + + return $resolvedVersion +} + +$expectedVersion = Resolve-ExpectedVersion -Version $RequestedVersion -Token $GitHubToken + +if ($IsWindows) { + # On Windows, verify via the expected install directory to avoid stale PATH resolution. + $isPrerelease = $expectedVersion -match '-' + $majorVersion = ($expectedVersion -split '[\.-]')[0] + $installDir = if ($isPrerelease) { "$majorVersion-preview" } else { $majorVersion } + $pwshPath = "$env:ProgramFiles\PowerShell\$installDir\pwsh.exe" + + Write-Host "Windows: verifying via subprocess at $pwshPath" + + if (Test-Path $pwshPath) { + $installedVersion = (& $pwshPath -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') + } else { + Write-Host "Warning: Expected pwsh not found at $pwshPath, falling back to `$PSVersionTable" + $installedVersion = ($PSVersionTable.PSVersion).ToString() + } +} else { + $installedVersion = ($PSVersionTable.PSVersion).ToString() +} + +Write-Host "Installed PowerShell version: $installedVersion" +Write-Host "Expected PowerShell version: $expectedVersion" + +if ($installedVersion -ne $expectedVersion) { + throw "Failed: expected $expectedVersion but got $installedVersion" +} + +if ($RequestedVersion.Trim().ToLower() -eq 'prerelease' -and $installedVersion -notmatch '-') { + throw "Prerelease validation failed: installed version '$installedVersion' does not contain a prerelease segment." +} From 38a2506e921e1d4027e25ec50e22dc5288cee277 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:18:23 +0200 Subject: [PATCH 08/18] Move repeated workflow env values to job-level scope --- .github/workflows/Action-Test.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index a19d56f..20ccca0 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -25,6 +25,8 @@ jobs: version: ['latest', 'prerelease', '7.4.7', '7.5.0', '7.4.0-preview.5'] runs-on: ${{ matrix.os }} name: '${{ matrix.os }} - [${{ matrix.version }}]' + env: + TestGitHubToken: ${{ github.token }} steps: - name: Checkout repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -40,11 +42,14 @@ jobs: - name: Verify installed version shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ github.token }}' + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ env.TestGitHubToken }}' ActionTestTokenUseCases: name: 'ubuntu-latest - [Token use cases]' runs-on: ubuntu-latest + env: + TestGitHubToken: ${{ github.token }} + TestLatestVersion: latest steps: - name: Checkout repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -54,31 +59,31 @@ jobs: - name: Action-Test (Default token behavior) uses: ./ with: - Version: latest + Version: ${{ env.TestLatestVersion }} - name: Verify default token behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' - name: Action-Test (Explicit token input) uses: ./ with: - Version: latest - Token: ${{ github.token }} + Version: ${{ env.TestLatestVersion }} + Token: ${{ env.TestGitHubToken }} - name: Verify explicit token input behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' -GitHubToken '${{ github.token }}' + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' - name: Action-Test (Anonymous mode) uses: ./ with: - Version: latest + Version: ${{ env.TestLatestVersion }} Token: '' - name: Verify anonymous mode behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion 'latest' + ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' From 9fc2f9e323b12ee3617e93ea1d4e603ea8c90ef2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:51:01 +0200 Subject: [PATCH 09/18] Move repeated action env values to composite runs level --- action.yml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/action.yml b/action.yml index 2218875..e0804be 100644 --- a/action.yml +++ b/action.yml @@ -38,37 +38,27 @@ inputs: runs: using: composite + env: + REQUESTED_VERSION: ${{ inputs.Version }} + PRERELEASE: ${{ inputs.Prerelease }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} steps: - name: Install PowerShell (Linux) if: runner.os == 'Linux' shell: bash working-directory: ${{ github.action_path }} - env: - REQUESTED_VERSION: ${{ inputs.Version }} - PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ inputs.Token }} - GH_TOKEN: ${{ inputs.Token }} - GH_HOST: ${{ inputs.Host }} - run: bash ./scripts/linux/install.sh # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input + run: bash ./scripts/linux/install.sh - name: Install PowerShell (macOS) if: runner.os == 'macOS' shell: bash working-directory: ${{ github.action_path }} - env: - REQUESTED_VERSION: ${{ inputs.Version }} - PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ inputs.Token }} - GH_TOKEN: ${{ inputs.Token }} - GH_HOST: ${{ inputs.Host }} - run: bash ./scripts/macos/install.sh # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input + run: bash ./scripts/macos/install.sh - name: Install PowerShell (Windows) if: runner.os == 'Windows' shell: powershell working-directory: ${{ github.action_path }} - env: - REQUESTED_VERSION: ${{ inputs.Version }} - PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ inputs.Token }} - run: ./scripts/windows/install.ps1 # zizmor: ignore[github-env] GITHUB_PATH writes use hardcoded install dirs, not user input + run: ./scripts/windows/install.ps1 From 5aa9d1f2131ef5ad5395d8df2c24108ef7e5b74f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:51:25 +0200 Subject: [PATCH 10/18] Move working-directory to defaults section in composite action --- action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index e0804be..f85d670 100644 --- a/action.yml +++ b/action.yml @@ -38,6 +38,9 @@ inputs: runs: using: composite + defaults: + run: + working-directory: ${{ github.action_path }} env: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} @@ -48,17 +51,14 @@ runs: - name: Install PowerShell (Linux) if: runner.os == 'Linux' shell: bash - working-directory: ${{ github.action_path }} run: bash ./scripts/linux/install.sh - name: Install PowerShell (macOS) if: runner.os == 'macOS' shell: bash - working-directory: ${{ github.action_path }} run: bash ./scripts/macos/install.sh - name: Install PowerShell (Windows) if: runner.os == 'Windows' shell: powershell - working-directory: ${{ github.action_path }} run: ./scripts/windows/install.ps1 From 59e35d573ccf1366fbe07185ad935877a8e8f43e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:53:37 +0200 Subject: [PATCH 11/18] Remove unsupported defaults section, restore working-directory to steps --- action.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index f85d670..7f474ad 100644 --- a/action.yml +++ b/action.yml @@ -36,11 +36,10 @@ inputs: required: false default: github.com + + runs: using: composite - defaults: - run: - working-directory: ${{ github.action_path }} env: REQUESTED_VERSION: ${{ inputs.Version }} PRERELEASE: ${{ inputs.Prerelease }} @@ -51,14 +50,17 @@ runs: - name: Install PowerShell (Linux) if: runner.os == 'Linux' shell: bash + working-directory: ${{ github.action_path }} run: bash ./scripts/linux/install.sh - name: Install PowerShell (macOS) if: runner.os == 'macOS' shell: bash + working-directory: ${{ github.action_path }} run: bash ./scripts/macos/install.sh - name: Install PowerShell (Windows) if: runner.os == 'Windows' shell: powershell + working-directory: ${{ github.action_path }} run: ./scripts/windows/install.ps1 From 838b4575d71788fd416f178fa3ccd5f8462b19bd Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:55:12 +0200 Subject: [PATCH 12/18] Move env block from runs level to individual steps --- action.yml | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/action.yml b/action.yml index 7f474ad..fb73d87 100644 --- a/action.yml +++ b/action.yml @@ -40,27 +40,39 @@ inputs: runs: using: composite - env: - REQUESTED_VERSION: ${{ inputs.Version }} - PRERELEASE: ${{ inputs.Prerelease }} - GITHUB_TOKEN: ${{ inputs.Token }} - GH_TOKEN: ${{ inputs.Token }} - GH_HOST: ${{ inputs.Host }} steps: - - name: Install PowerShell (Linux) + - name: Install PowerShell if: runner.os == 'Linux' shell: bash working-directory: ${{ github.action_path }} + env: + REQUESTED_VERSION: ${{ inputs.Version }} + PRERELEASE: ${{ inputs.Prerelease }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} run: bash ./scripts/linux/install.sh - - name: Install PowerShell (macOS) + - name: Install PowerShell if: runner.os == 'macOS' shell: bash working-directory: ${{ github.action_path }} + env: + REQUESTED_VERSION: ${{ inputs.Version }} + PRERELEASE: ${{ inputs.Prerelease }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} run: bash ./scripts/macos/install.sh - - name: Install PowerShell (Windows) + - name: Install PowerShell if: runner.os == 'Windows' shell: powershell working-directory: ${{ github.action_path }} + env: + REQUESTED_VERSION: ${{ inputs.Version }} + PRERELEASE: ${{ inputs.Prerelease }} + GITHUB_TOKEN: ${{ inputs.Token }} + GH_TOKEN: ${{ inputs.Token }} + GH_HOST: ${{ inputs.Host }} run: ./scripts/windows/install.ps1 From a5ef22450bd755dd2fa362bc0fb9852021fbb558 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 22:57:39 +0200 Subject: [PATCH 13/18] Format GitHub API headers for consistency --- .github/workflows/Action-Test.yml | 8 ++++---- {test => tests}/scripts/Verify-InstalledVersion.ps1 | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename {test => tests}/scripts/Verify-InstalledVersion.ps1 (98%) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 20ccca0..1012f67 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -42,7 +42,7 @@ jobs: - name: Verify installed version shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ env.TestGitHubToken }}' ActionTestTokenUseCases: name: 'ubuntu-latest - [Token use cases]' @@ -64,7 +64,7 @@ jobs: - name: Verify default token behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' - name: Action-Test (Explicit token input) uses: ./ @@ -75,7 +75,7 @@ jobs: - name: Verify explicit token input behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' - name: Action-Test (Anonymous mode) uses: ./ @@ -86,4 +86,4 @@ jobs: - name: Verify anonymous mode behavior shell: pwsh run: | - ./test/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' diff --git a/test/scripts/Verify-InstalledVersion.ps1 b/tests/scripts/Verify-InstalledVersion.ps1 similarity index 98% rename from test/scripts/Verify-InstalledVersion.ps1 rename to tests/scripts/Verify-InstalledVersion.ps1 index fb9a3e7..1fb227e 100644 --- a/test/scripts/Verify-InstalledVersion.ps1 +++ b/tests/scripts/Verify-InstalledVersion.ps1 @@ -15,7 +15,7 @@ function Get-GitHubApiHeaders { ) $headers = @{ - 'Accept' = 'application/vnd.github+json' + 'Accept' = 'application/vnd.github+json' 'X-GitHub-Api-Version' = '2022-11-28' } From dc09e1fe6dd1d3512d4ef93b6d14db16968ec62c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 23:14:45 +0200 Subject: [PATCH 14/18] Run token use cases in separate workflow jobs --- .github/workflows/Action-Test.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 1012f67..8102504 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -44,8 +44,8 @@ jobs: run: | ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ env.TestGitHubToken }}' - ActionTestTokenUseCases: - name: 'ubuntu-latest - [Token use cases]' + ActionTestTokenUseCaseDefault: + name: 'ubuntu-latest - [Token default behavior]' runs-on: ubuntu-latest env: TestGitHubToken: ${{ github.token }} @@ -66,6 +66,18 @@ jobs: run: | ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ActionTestTokenUseCaseExplicit: + name: 'ubuntu-latest - [Token explicit input]' + runs-on: ubuntu-latest + env: + TestGitHubToken: ${{ github.token }} + TestLatestVersion: latest + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Action-Test (Explicit token input) uses: ./ with: @@ -77,6 +89,17 @@ jobs: run: | ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ActionTestTokenUseCaseAnonymous: + name: 'ubuntu-latest - [Token anonymous mode]' + runs-on: ubuntu-latest + env: + TestLatestVersion: latest + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Action-Test (Anonymous mode) uses: ./ with: From 2d3c379510ee65f0e5e77bace48233a47ceb15b0 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 16 May 2026 23:32:43 +0200 Subject: [PATCH 15/18] Fix anonymous-mode and GHES token routing for all platforms Thread 1&2 (linux/macos): guard gh api path with [[ -n \ ]] so anonymous callers fall through to the unauthenticated curl path instead of letting gh api fail on GitHub-hosted runners where gh is always installed. Thread 3 (windows): build REST API base URL from GH_HOST so GHES environments resolve releases against the configured host instead of hard-coded api.github.com. Thread 4 (action.yml): add GH_ENTERPRISE_TOKEN to all three step env blocks; GitHub CLI requires this variable (not GH_TOKEN) when talking to a non-github.com host. --- action.yml | 3 +++ scripts/linux/install.sh | 2 +- scripts/macos/install.sh | 2 +- scripts/windows/install.ps1 | 9 +++++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index fb73d87..d678911 100644 --- a/action.yml +++ b/action.yml @@ -50,6 +50,7 @@ runs: PRERELEASE: ${{ inputs.Prerelease }} GITHUB_TOKEN: ${{ inputs.Token }} GH_TOKEN: ${{ inputs.Token }} + GH_ENTERPRISE_TOKEN: ${{ inputs.Token }} GH_HOST: ${{ inputs.Host }} run: bash ./scripts/linux/install.sh @@ -62,6 +63,7 @@ runs: PRERELEASE: ${{ inputs.Prerelease }} GITHUB_TOKEN: ${{ inputs.Token }} GH_TOKEN: ${{ inputs.Token }} + GH_ENTERPRISE_TOKEN: ${{ inputs.Token }} GH_HOST: ${{ inputs.Host }} run: bash ./scripts/macos/install.sh @@ -74,5 +76,6 @@ runs: PRERELEASE: ${{ inputs.Prerelease }} GITHUB_TOKEN: ${{ inputs.Token }} GH_TOKEN: ${{ inputs.Token }} + GH_ENTERPRISE_TOKEN: ${{ inputs.Token }} GH_HOST: ${{ inputs.Host }} run: ./scripts/windows/install.ps1 diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh index 6829b21..8dc1b63 100755 --- a/scripts/linux/install.sh +++ b/scripts/linux/install.sh @@ -7,7 +7,7 @@ echo "Prerelease: [$PRERELEASE]" github_api_get() { local endpoint="$1" - if command -v gh >/dev/null 2>&1; then + if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then local gh_args=() if [[ -n "$GH_HOST" ]]; then gh_args+=(--hostname "$GH_HOST") diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh index 755b961..8757514 100755 --- a/scripts/macos/install.sh +++ b/scripts/macos/install.sh @@ -7,7 +7,7 @@ echo "Prerelease: [$PRERELEASE]" github_api_get() { local endpoint="$1" - if command -v gh >/dev/null 2>&1; then + if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then local gh_args=() if [[ -n "$GH_HOST" ]]; then gh_args+=(--hostname "$GH_HOST") diff --git a/scripts/windows/install.ps1 b/scripts/windows/install.ps1 index bee4d00..035c257 100644 --- a/scripts/windows/install.ps1 +++ b/scripts/windows/install.ps1 @@ -12,8 +12,13 @@ if ($req -and $req.Trim().ToLower() -eq 'latest') { if ($env:GITHUB_TOKEN) { $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" } + $apiBase = if ($env:GH_HOST -and $env:GH_HOST -ne 'github.com') { + "https://$($env:GH_HOST)/api/v3" + } else { + 'https://api.github.com' + } if ($env:PRERELEASE -eq 'true') { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers + $releases = Invoke-RestMethod -Uri "$apiBase/repos/PowerShell/PowerShell/releases?per_page=100" -Headers $headers $latestRelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 if (-not $latestRelease) { Write-Host 'Error: No prerelease PowerShell releases are available from GitHub.' @@ -22,7 +27,7 @@ if ($req -and $req.Trim().ToLower() -eq 'latest') { $latest = $latestRelease.tag_name.TrimStart('v') Write-Host "Latest prerelease PowerShell version detected: $latest" } else { - $latest = (Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest' -Headers $headers).tag_name.TrimStart('v') + $latest = (Invoke-RestMethod -Uri "$apiBase/repos/PowerShell/PowerShell/releases/latest" -Headers $headers).tag_name.TrimStart('v') if (-not $latest) { Write-Host 'Error: Failed to resolve latest stable PowerShell release from GitHub.' exit 1 From 34c1617cd7975fbcd3f937a560e49a9e2ff1edf7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 17 May 2026 00:09:06 +0200 Subject: [PATCH 16/18] Fix lint findings in shell/workflow/powershell checks - format linux/macos installers with shfmt style and add SC2016 suppressions for literal pwsh command strings - remove workflow run-block template interpolation in Action-Test verification steps to satisfy zizmor template-injection audit - add script-level suppressions for intentional Write-Host/empty-catch usage in windows installer - rename Get-GitHubApiHeaders to Get-GitHubApiHeader in verify script for singular noun rule --- .github/workflows/Action-Test.yml | 16 +- scripts/linux/install.sh | 268 +++++++++++----------- scripts/macos/install.sh | 135 +++++------ scripts/windows/install.ps1 | 6 +- tests/scripts/Verify-InstalledVersion.ps1 | 8 +- 5 files changed, 225 insertions(+), 208 deletions(-) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 8102504..069a304 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -41,8 +41,10 @@ jobs: - name: Verify installed version shell: pwsh + env: + RequestedVersion: ${{ matrix.version }} run: | - ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ matrix.version }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion "$env:RequestedVersion" -GitHubToken "$env:TestGitHubToken" ActionTestTokenUseCaseDefault: name: 'ubuntu-latest - [Token default behavior]' @@ -63,8 +65,10 @@ jobs: - name: Verify default token behavior shell: pwsh + env: + RequestedVersion: ${{ env.TestLatestVersion }} run: | - ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion "$env:RequestedVersion" -GitHubToken "$env:TestGitHubToken" ActionTestTokenUseCaseExplicit: name: 'ubuntu-latest - [Token explicit input]' @@ -86,8 +90,10 @@ jobs: - name: Verify explicit token input behavior shell: pwsh + env: + RequestedVersion: ${{ env.TestLatestVersion }} run: | - ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' -GitHubToken '${{ env.TestGitHubToken }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion "$env:RequestedVersion" -GitHubToken "$env:TestGitHubToken" ActionTestTokenUseCaseAnonymous: name: 'ubuntu-latest - [Token anonymous mode]' @@ -108,5 +114,7 @@ jobs: - name: Verify anonymous mode behavior shell: pwsh + env: + RequestedVersion: ${{ env.TestLatestVersion }} run: | - ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion '${{ env.TestLatestVersion }}' + ./tests/scripts/Verify-InstalledVersion.ps1 -RequestedVersion "$env:RequestedVersion" diff --git a/scripts/linux/install.sh b/scripts/linux/install.sh index 8dc1b63..b07df88 100755 --- a/scripts/linux/install.sh +++ b/scripts/linux/install.sh @@ -6,74 +6,75 @@ echo "Requested version: [$REQUESTED_VERSION]" echo "Prerelease: [$PRERELEASE]" github_api_get() { - local endpoint="$1" - if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then - local gh_args=() - if [[ -n "$GH_HOST" ]]; then - gh_args+=(--hostname "$GH_HOST") - fi - gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" - return - fi - - local auth_header=() - if [[ -n "$GITHUB_TOKEN" ]]; then - auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") - fi - - local api_base="https://api.github.com" - if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then - api_base="https://${GH_HOST}/api/v3" - fi - - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - "${auth_header[@]}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "${api_base}/${endpoint}" + local endpoint="$1" + if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi + + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi + + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi + + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" } # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) case "${REQUESTED_VERSION:-}" in - [Ll][Aa][Tt][Ee][Ss][Tt]) - if [[ "$PRERELEASE" == "true" ]]; then - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | - jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." - exit 1 - fi - echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" - else - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases/latest' | - jq -r '.tag_name' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: Failed to resolve latest stable PowerShell release from GitHub." - exit 1 - fi - echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" - fi - ;; - "") - echo "Error: Version input is required (or use 'latest')" - exit 1 - ;; +[Ll][Aa][Tt][Ee][Ss][Tt]) + if [[ "$PRERELEASE" == "true" ]]; then + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | + jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." + exit 1 + fi + echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" + else + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | + jq -r '.tag_name' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: Failed to resolve latest stable PowerShell release from GitHub." + exit 1 + fi + echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" + fi + ;; +"") + echo "Error: Version input is required (or use 'latest')" + exit 1 + ;; esac +# shellcheck disable=SC2016 # PowerShell expression is intentionally single-quoted for pwsh -Command. DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) if [[ -n "$DETECTED_VERSION" ]]; then - echo "Currently installed PowerShell version: $DETECTED_VERSION" + echo "Currently installed PowerShell version: $DETECTED_VERSION" else - echo "PowerShell is not currently installed" + echo "PowerShell is not currently installed" fi if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then - echo "PowerShell $DETECTED_VERSION already installed. Skipping." - exit 0 + echo "PowerShell $DETECTED_VERSION already installed. Skipping." + exit 0 fi # Determine Linux distribution type @@ -82,88 +83,88 @@ ARCH=$(dpkg --print-architecture 2>/dev/null || rpm --eval '%{_arch}' 2>/dev/nul # Query GitHub Releases API for the actual asset URLs to handle naming # convention differences across releases (e.g. powershell-preview_ vs powershell_). RELEASE_JSON=$( - github_api_get "repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" + github_api_get "repos/PowerShell/PowerShell/releases/tags/v${REQUESTED_VERSION}" ) if [[ -z "$RELEASE_JSON" ]]; then - echo "Error: Failed to fetch release info for v${REQUESTED_VERSION} from GitHub." - exit 1 + echo "Error: Failed to fetch release info for v${REQUESTED_VERSION} from GitHub." + exit 1 fi # Determine if the requested version is a prerelease (contains a hyphen, e.g. 7.6.0-preview.6) IS_PRERELEASE=false if [[ "$REQUESTED_VERSION" == *-* ]]; then - IS_PRERELEASE=true + IS_PRERELEASE=true fi if command -v apt-get >/dev/null || command -v dpkg >/dev/null; then - # Debian/Ubuntu based - echo "Detected Debian/Ubuntu based system..." - if [[ "$IS_PRERELEASE" == "true" ]]; then - # Prerelease .deb naming varies across releases: - # Older: powershell-preview_7.4.0-preview.5-1.deb_amd64.deb - # Newer: powershell_7.6.0-preview.6-1.deb_amd64.deb - # Try powershell-preview_ first, then fall back to powershell_ - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-preview_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - if [[ -z "$URL" ]]; then - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - fi - else - # For stable versions, select the powershell package (not powershell-lts or powershell-preview) - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') - fi - if [[ -z "$URL" ]]; then - echo "Error: No .deb package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." - exit 1 - fi - DEB_NAME=$(basename "$URL") - echo "Downloading from: $URL" - wget -q "$URL" -O "$DEB_NAME" - - # Remove all existing PowerShell packages to avoid dpkg conflicts - # (powershell, powershell-lts, and powershell-preview all provide /usr/bin/pwsh) - echo "Removing existing PowerShell packages to avoid conflicts..." - sudo dpkg --remove powershell powershell-lts powershell-preview 2>/dev/null || true - - echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." - sudo dpkg -i "$DEB_NAME" || sudo apt-get -f install -y + # Debian/Ubuntu based + echo "Detected Debian/Ubuntu based system..." + if [[ "$IS_PRERELEASE" == "true" ]]; then + # Prerelease .deb naming varies across releases: + # Older: powershell-preview_7.4.0-preview.5-1.deb_amd64.deb + # Newer: powershell_7.6.0-preview.6-1.deb_amd64.deb + # Try powershell-preview_ first, then fall back to powershell_ + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-preview_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + if [[ -z "$URL" ]]; then + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + fi + else + # For stable versions, select the powershell package (not powershell-lts or powershell-preview) + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell_.*\\.deb_" + $arch + "\\.deb$"))] | .[0].browser_download_url // empty') + fi + if [[ -z "$URL" ]]; then + echo "Error: No .deb package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." + exit 1 + fi + DEB_NAME=$(basename "$URL") + echo "Downloading from: $URL" + wget -q "$URL" -O "$DEB_NAME" + + # Remove all existing PowerShell packages to avoid dpkg conflicts + # (powershell, powershell-lts, and powershell-preview all provide /usr/bin/pwsh) + echo "Removing existing PowerShell packages to avoid conflicts..." + sudo dpkg --remove powershell powershell-lts powershell-preview 2>/dev/null || true + + echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." + sudo dpkg -i "$DEB_NAME" || sudo apt-get -f install -y elif command -v rpm >/dev/null; then - # RHEL/Fedora/CentOS based - echo "Detected RHEL/Fedora/CentOS based system..." - if [[ "$IS_PRERELEASE" == "true" ]]; then - # Prerelease .rpm naming varies across releases: - # Older: powershell-preview-7.4.0_preview.5-1.rh.x86_64.rpm - # Newer: powershell-7.6.0_preview.6-1.rh.x86_64.rpm - # Try powershell-preview first, then fall back to powershell- - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-preview.*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - if [[ -z "$URL" ]]; then - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - fi - else - URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ - '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') - fi - if [[ -z "$URL" ]]; then - echo "Error: No .rpm package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." - exit 1 - fi - RPM_NAME=$(basename "$URL") - echo "Downloading from: $URL" - wget -q "$URL" -O "$RPM_NAME" - - # Remove existing PowerShell packages to avoid conflicts - echo "Removing existing PowerShell packages to avoid conflicts..." - sudo rpm -e powershell powershell-preview 2>/dev/null || true - - echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." - sudo rpm -i "$RPM_NAME" || sudo yum install -y "$RPM_NAME" + # RHEL/Fedora/CentOS based + echo "Detected RHEL/Fedora/CentOS based system..." + if [[ "$IS_PRERELEASE" == "true" ]]; then + # Prerelease .rpm naming varies across releases: + # Older: powershell-preview-7.4.0_preview.5-1.rh.x86_64.rpm + # Newer: powershell-7.6.0_preview.6-1.rh.x86_64.rpm + # Try powershell-preview first, then fall back to powershell- + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-preview.*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + if [[ -z "$URL" ]]; then + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + fi + else + URL=$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH" \ + '[.assets[] | select(.name | test("^powershell-[0-9].*\\.rh\\." + (if $arch == "aarch64" then $arch else "x86_64" end) + "\\.rpm$"))] | .[0].browser_download_url // empty') + fi + if [[ -z "$URL" ]]; then + echo "Error: No .rpm package found for architecture '$ARCH' in release v${REQUESTED_VERSION}." + exit 1 + fi + RPM_NAME=$(basename "$URL") + echo "Downloading from: $URL" + wget -q "$URL" -O "$RPM_NAME" + + # Remove existing PowerShell packages to avoid conflicts + echo "Removing existing PowerShell packages to avoid conflicts..." + sudo rpm -e powershell powershell-preview 2>/dev/null || true + + echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." + sudo rpm -i "$RPM_NAME" || sudo yum install -y "$RPM_NAME" else - echo "Unsupported Linux distribution. Cannot determine package format." - exit 1 + echo "Unsupported Linux distribution. Cannot determine package format." + exit 1 fi # Determine the install directory and add to PATH before verification. @@ -171,25 +172,26 @@ fi # which is not on the default PATH after removing the old powershell package. MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) if [[ "$IS_PRERELEASE" == "true" ]]; then - INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}-preview" + INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}-preview" else - INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}" + INSTALL_DIR="/opt/microsoft/powershell/${MAJOR_VERSION}" fi if [[ -d "$INSTALL_DIR" ]]; then - export PATH="$INSTALL_DIR:$PATH" + export PATH="$INSTALL_DIR:$PATH" fi # Verify installation succeeded +# shellcheck disable=SC2016 # PowerShell expression is intentionally single-quoted for pwsh -Command. INSTALLED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) if [[ "$INSTALLED_VERSION" != "$REQUESTED_VERSION" ]]; then - echo "Error: Installation verification failed. Expected $REQUESTED_VERSION but got ${INSTALLED_VERSION:-nothing}." - exit 1 + echo "Error: Installation verification failed. Expected $REQUESTED_VERSION but got ${INSTALLED_VERSION:-nothing}." + exit 1 fi echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." # For prerelease builds, add the install directory to GITHUB_PATH so subsequent # `shell: pwsh` steps resolve to the version we just installed. if [[ "$IS_PRERELEASE" == "true" && -d "$INSTALL_DIR" ]]; then - echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" - echo "$INSTALL_DIR" >> "$GITHUB_PATH" + echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" + echo "$INSTALL_DIR" >>"$GITHUB_PATH" fi diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh index 8757514..f1820f2 100755 --- a/scripts/macos/install.sh +++ b/scripts/macos/install.sh @@ -6,81 +6,82 @@ echo "Requested version: [$REQUESTED_VERSION]" echo "Prerelease: [$PRERELEASE]" github_api_get() { - local endpoint="$1" - if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then - local gh_args=() - if [[ -n "$GH_HOST" ]]; then - gh_args+=(--hostname "$GH_HOST") - fi - gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" - return - fi + local endpoint="$1" + if command -v gh >/dev/null 2>&1 && [[ -n "$GH_TOKEN" ]]; then + local gh_args=() + if [[ -n "$GH_HOST" ]]; then + gh_args+=(--hostname "$GH_HOST") + fi + gh api "${gh_args[@]}" -H "X-GitHub-Api-Version: 2022-11-28" "$endpoint" + return + fi - local auth_header=() - if [[ -n "$GITHUB_TOKEN" ]]; then - auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") - fi + local auth_header=() + if [[ -n "$GITHUB_TOKEN" ]]; then + auth_header=(-H "Authorization: Bearer $GITHUB_TOKEN") + fi - local api_base="https://api.github.com" - if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then - api_base="https://${GH_HOST}/api/v3" - fi + local api_base="https://api.github.com" + if [[ -n "$GH_HOST" && "$GH_HOST" != "github.com" ]]; then + api_base="https://${GH_HOST}/api/v3" + fi - curl -s -f \ - -H "Accept: application/vnd.github+json" \ - "${auth_header[@]}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "${api_base}/${endpoint}" + curl -s -f \ + -H "Accept: application/vnd.github+json" \ + "${auth_header[@]}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_base}/${endpoint}" } # Only resolve to latest version if explicitly set to 'latest' (case-insensitive) case "${REQUESTED_VERSION:-}" in - [Ll][Aa][Tt][Ee][Ss][Tt]) - if [[ "$PRERELEASE" == "true" ]]; then - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | - jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." - exit 1 - fi - echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" - else - REQUESTED_VERSION=$( - github_api_get 'repos/PowerShell/PowerShell/releases/latest' | - jq -r '.tag_name' | sed 's/^v//' - ) - if [[ -z "$REQUESTED_VERSION" ]]; then - echo "Error: Failed to resolve latest stable PowerShell release from GitHub." - exit 1 - fi - echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" - fi - ;; - "") - echo "Error: Version input is required (or use 'latest')" - exit 1 - ;; +[Ll][Aa][Tt][Ee][Ss][Tt]) + if [[ "$PRERELEASE" == "true" ]]; then + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases?per_page=100' | + jq -r '[.[] | select(.prerelease == true)] | (.[0].tag_name // empty)' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: No prerelease PowerShell releases found when resolving latest prerelease." + exit 1 + fi + echo "Latest prerelease PowerShell version detected: $REQUESTED_VERSION" + else + REQUESTED_VERSION=$( + github_api_get 'repos/PowerShell/PowerShell/releases/latest' | + jq -r '.tag_name' | sed 's/^v//' + ) + if [[ -z "$REQUESTED_VERSION" ]]; then + echo "Error: Failed to resolve latest stable PowerShell release from GitHub." + exit 1 + fi + echo "Latest stable PowerShell release detected: $REQUESTED_VERSION" + fi + ;; +"") + echo "Error: Version input is required (or use 'latest')" + exit 1 + ;; esac +# shellcheck disable=SC2016 # PowerShell expression is intentionally single-quoted for pwsh -Command. DETECTED_VERSION=$(pwsh -NoLogo -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || true) if [[ -n "$DETECTED_VERSION" ]]; then - echo "Currently installed PowerShell version: $DETECTED_VERSION" + echo "Currently installed PowerShell version: $DETECTED_VERSION" else - echo "PowerShell is not currently installed" + echo "PowerShell is not currently installed" fi if [[ "$DETECTED_VERSION" == "$REQUESTED_VERSION" ]]; then - echo "PowerShell $DETECTED_VERSION already installed. Skipping." - exit 0 + echo "PowerShell $DETECTED_VERSION already installed. Skipping." + exit 0 fi # Determine architecture and download appropriate package ARCH=$(uname -m) case "$ARCH" in - "arm64") PKG_NAME="powershell-${REQUESTED_VERSION}-osx-arm64.pkg" ;; - *) PKG_NAME="powershell-${REQUESTED_VERSION}-osx-x64.pkg" ;; +"arm64") PKG_NAME="powershell-${REQUESTED_VERSION}-osx-arm64.pkg" ;; +*) PKG_NAME="powershell-${REQUESTED_VERSION}-osx-x64.pkg" ;; esac URL="https://github.com/PowerShell/PowerShell/releases/download/v${REQUESTED_VERSION}/${PKG_NAME}" @@ -88,8 +89,8 @@ echo "Downloading from: $URL" echo "Starting installation of PowerShell [$REQUESTED_VERSION]..." if ! curl -sSL "$URL" -o "$PKG_NAME"; then - echo "Error: Failed to download PowerShell package" - exit 1 + echo "Error: Failed to download PowerShell package" + exit 1 fi sudo installer -pkg "$PKG_NAME" -target / echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." @@ -97,14 +98,14 @@ echo "Installation complete. PowerShell [$REQUESTED_VERSION] is now available." # For prerelease builds, add the install directory to GITHUB_PATH so subsequent # `shell: pwsh` steps resolve to the version we just installed. if [[ "$REQUESTED_VERSION" == *-* ]]; then - MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) - if [[ "$MAJOR_VERSION" =~ ^[0-9]+$ ]]; then - INSTALL_DIR="/usr/local/microsoft/powershell/${MAJOR_VERSION}-preview" - if [[ -d "$INSTALL_DIR" ]]; then - echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" - echo "$INSTALL_DIR" >> "$GITHUB_PATH" - fi - else - echo "Warning: Computed MAJOR_VERSION ('$MAJOR_VERSION') is invalid; skipping GITHUB_PATH update." >&2 - fi + MAJOR_VERSION=$(echo "$REQUESTED_VERSION" | cut -d'.' -f1) + if [[ "$MAJOR_VERSION" =~ ^[0-9]+$ ]]; then + INSTALL_DIR="/usr/local/microsoft/powershell/${MAJOR_VERSION}-preview" + if [[ -d "$INSTALL_DIR" ]]; then + echo "Adding install directory to GITHUB_PATH: $INSTALL_DIR" + echo "$INSTALL_DIR" >>"$GITHUB_PATH" + fi + else + echo "Warning: Computed MAJOR_VERSION ('$MAJOR_VERSION') is invalid; skipping GITHUB_PATH update." >&2 + fi fi diff --git a/scripts/windows/install.ps1 b/scripts/windows/install.ps1 index 035c257..cc39b1f 100644 --- a/scripts/windows/install.ps1 +++ b/scripts/windows/install.ps1 @@ -1,4 +1,8 @@ -# Install-PowerShell +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] +param() + +# Install-PowerShell Write-Host "Requested version: [$env:REQUESTED_VERSION]" Write-Host "Prerelease: [$env:PRERELEASE]" diff --git a/tests/scripts/Verify-InstalledVersion.ps1 b/tests/scripts/Verify-InstalledVersion.ps1 index 1fb227e..32dc161 100644 --- a/tests/scripts/Verify-InstalledVersion.ps1 +++ b/tests/scripts/Verify-InstalledVersion.ps1 @@ -1,4 +1,6 @@ -[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + +[CmdletBinding()] param( [Parameter(Mandatory)] [string] $RequestedVersion, @@ -7,7 +9,7 @@ param( [string] $GitHubToken ) -function Get-GitHubApiHeaders { +function Get-GitHubApiHeader { [CmdletBinding()] param( [Parameter()] @@ -38,7 +40,7 @@ function Resolve-ExpectedVersion { $resolvedVersion = $Version $normalizedVersion = $Version.Trim().ToLower() - $headers = Get-GitHubApiHeaders -Token $Token + $headers = Get-GitHubApiHeader -Token $Token if ($normalizedVersion -eq 'prerelease') { $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases?per_page=100' -Headers $headers From b13a335f561a0427b0a09aac9ca5f0eb5c3785dc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 17 May 2026 00:20:57 +0200 Subject: [PATCH 17/18] Fix remaining lint and flaky prerelease download - add comment help/output metadata in Verify-InstalledVersion helper functions so super-linter PowerShell checks pass - add bounded retry loop around Windows MSI Invoke-WebRequest to reduce transient prerelease job failures --- scripts/windows/install.ps1 | 20 +++++++++++++++++++- tests/scripts/Verify-InstalledVersion.ps1 | 10 ++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/windows/install.ps1 b/scripts/windows/install.ps1 index cc39b1f..8527c8b 100644 --- a/scripts/windows/install.ps1 +++ b/scripts/windows/install.ps1 @@ -147,7 +147,25 @@ $msi = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.msi" $url = "https://github.com/PowerShell/PowerShell/releases/download/v$($env:REQUESTED_VERSION)/$msi" Write-Host "Downloading from: $url" -$null = Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop +$downloadSucceeded = $false +$maxAttempts = 3 +for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + try { + $null = Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop + $downloadSucceeded = $true + break + } catch { + if ($attempt -eq $maxAttempts) { + throw + } + Write-Host "Warning: Download attempt $attempt failed; retrying..." + } +} + +if (-not $downloadSucceeded) { + Write-Host 'Error: Failed to download PowerShell package after retry attempts.' + exit 1 +} # Install requested version Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)]..." diff --git a/tests/scripts/Verify-InstalledVersion.ps1 b/tests/scripts/Verify-InstalledVersion.ps1 index 32dc161..7d7afe6 100644 --- a/tests/scripts/Verify-InstalledVersion.ps1 +++ b/tests/scripts/Verify-InstalledVersion.ps1 @@ -9,8 +9,13 @@ param( [string] $GitHubToken ) +<# +.SYNOPSIS +Builds GitHub API request headers for optional authenticated requests. +#> function Get-GitHubApiHeader { [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter()] [string] $Token @@ -28,8 +33,13 @@ function Get-GitHubApiHeader { return $headers } +<# +.SYNOPSIS +Resolves symbolic input values (latest/prerelease) to a concrete PowerShell version. +#> function Resolve-ExpectedVersion { [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory)] [string] $Version, From 0be829caafb3d32ca282f2d49e706375ab09f35d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 07:39:45 +0000 Subject: [PATCH 18/18] fix(windows): fall back to ZIP install when MSI is not available for a release Agent-Logs-Url: https://github.com/PSModule/Install-PowerShell/sessions/5033cf58-ed4c-4cc6-9396-0ed9bec18663 Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> --- scripts/windows/install.ps1 | 106 +++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 37 deletions(-) diff --git a/scripts/windows/install.ps1 b/scripts/windows/install.ps1 index 8527c8b..d493d21 100644 --- a/scripts/windows/install.ps1 +++ b/scripts/windows/install.ps1 @@ -6,21 +6,23 @@ param() Write-Host "Requested version: [$env:REQUESTED_VERSION]" Write-Host "Prerelease: [$env:PRERELEASE]" +# GitHub API headers used throughout the script +$headers = @{ + 'Accept' = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' +} +if ($env:GITHUB_TOKEN) { + $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" +} +$apiBase = if ($env:GH_HOST -and $env:GH_HOST -ne 'github.com') { + "https://$($env:GH_HOST)/api/v3" +} else { + 'https://api.github.com' +} + # Resolve 'latest' -> concrete version $req = $env:REQUESTED_VERSION if ($req -and $req.Trim().ToLower() -eq 'latest') { - $headers = @{ - 'Accept' = 'application/vnd.github+json' - 'X-GitHub-Api-Version' = '2022-11-28' - } - if ($env:GITHUB_TOKEN) { - $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" - } - $apiBase = if ($env:GH_HOST -and $env:GH_HOST -ne 'github.com') { - "https://$($env:GH_HOST)/api/v3" - } else { - 'https://api.github.com' - } if ($env:PRERELEASE -eq 'true') { $releases = Invoke-RestMethod -Uri "$apiBase/repos/PowerShell/PowerShell/releases?per_page=100" -Headers $headers $latestRelease = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 @@ -93,7 +95,7 @@ if ($isDowngrade) { 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) - $isDetectedPreview = $detected -match '-preview|-rc' + $isDetectedPreview = $detected -match '-' $pwshEntries = Get-ItemProperty -Path $regPaths -ErrorAction SilentlyContinue | Where-Object { $_.Publisher -eq 'Microsoft Corporation' -and @@ -142,16 +144,37 @@ if ($isDowngrade) { } } -# Download requested MSI -$msi = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.msi" -$url = "https://github.com/PowerShell/PowerShell/releases/download/v$($env:REQUESTED_VERSION)/$msi" +# Determine which package type is available for this release (MSI preferred, ZIP as fallback) +$msiName = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.msi" +$zipName = "PowerShell-$($env:REQUESTED_VERSION)-win-x64.zip" +$baseUrl = "https://github.com/PowerShell/PowerShell/releases/download/v$($env:REQUESTED_VERSION)" +$useMsi = $true + +try { + $releaseAssets = (Invoke-RestMethod -Uri "$apiBase/repos/PowerShell/PowerShell/releases/tags/v$($env:REQUESTED_VERSION)" -Headers $headers).assets + $assetNames = $releaseAssets | Select-Object -ExpandProperty name + if ($assetNames -notcontains $msiName) { + if ($assetNames -contains $zipName) { + $useMsi = $false + Write-Host "Note: No MSI package found for this release; using ZIP instead." + } else { + Write-Host "Error: No suitable Windows x64 package (MSI or ZIP) found for PowerShell $($env:REQUESTED_VERSION)." + exit 1 + } + } +} catch { + Write-Host "Warning: Could not query release assets; assuming MSI package is available." +} + +$pkg = if ($useMsi) { $msiName } else { $zipName } +$url = "$baseUrl/$pkg" Write-Host "Downloading from: $url" $downloadSucceeded = $false $maxAttempts = 3 for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { try { - $null = Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop + $null = Invoke-WebRequest -Uri $url -OutFile $pkg -UseBasicParsing -ErrorAction Stop $downloadSucceeded = $true break } catch { @@ -167,33 +190,42 @@ if (-not $downloadSucceeded) { exit 1 } -# Install requested version -Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)]..." -$msiProcess = Start-Process msiexec.exe -ArgumentList '/i', $msi, '/quiet', '/norestart' -Wait -PassThru -if ($msiProcess.ExitCode -ne 0) { - Write-Host "Error: Installation failed (exit code $($msiProcess.ExitCode))." +# Compute the install directory (used for both MSI and ZIP installs, and for GITHUB_PATH) +$isPrerelease = $env:REQUESTED_VERSION -match '-' +$majorVersion = ($env:REQUESTED_VERSION -split '[.\-]')[0] +if ($majorVersion -notmatch '^\d+$') { + Write-Host "Warning: Computed major version ('$majorVersion') is invalid; skipping installation." exit 1 } +$installDir = if ($isPrerelease) { + "$env:ProgramFiles\PowerShell\$majorVersion-preview" +} else { + "$env:ProgramFiles\PowerShell\$majorVersion" +} + +if ($useMsi) { + # Install via MSI + Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)] from MSI..." + $msiProcess = Start-Process msiexec.exe -ArgumentList '/i', $pkg, '/quiet', '/norestart' -Wait -PassThru + if ($msiProcess.ExitCode -ne 0) { + Write-Host "Error: Installation failed (exit code $($msiProcess.ExitCode))." + exit 1 + } +} else { + # Install via ZIP extraction + Write-Host "Starting installation of PowerShell [$($env:REQUESTED_VERSION)] from ZIP..." + $null = New-Item -ItemType Directory -Force -Path $installDir + Expand-Archive -Path $pkg -DestinationPath $installDir -Force +} Write-Host "Installation complete. PowerShell [$($env:REQUESTED_VERSION)] is now available." # Add the install directory to GITHUB_PATH so subsequent `shell: pwsh` steps # resolve to the version we just installed - even for preview builds whose # install directory (7-preview) is not on the runner's default PATH. -$isPrerelease = $env:REQUESTED_VERSION -match '-' -$majorVersion = ($env:REQUESTED_VERSION -split '[.\-]')[0] -if ($majorVersion -match '^\d+$') { - $installDir = if ($isPrerelease) { - "$env:ProgramFiles\PowerShell\$majorVersion-preview" - } else { - "$env:ProgramFiles\PowerShell\$majorVersion" - } - if (Test-Path $installDir) { - Write-Host "Adding install directory to GITHUB_PATH: $installDir" - Add-Content -Path $env:GITHUB_PATH -Value $installDir - } else { - Write-Host "Warning: Expected install directory not found: $installDir" - } +if (Test-Path $installDir) { + Write-Host "Adding install directory to GITHUB_PATH: $installDir" + Add-Content -Path $env:GITHUB_PATH -Value $installDir } else { - Write-Host "Warning: Computed major version ('$majorVersion') is invalid; skipping GITHUB_PATH update." + Write-Host "Warning: Expected install directory not found: $installDir" }