From b0d4fc798b0790e66cc6c089e43b39b476d44563 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Thu, 1 Oct 2026 19:46:54 -0700 Subject: [PATCH 1/2] feat(installer): add opt-in -Version dev-build and -ZipPath to install.ps1 -Version dev-build installs the development pre-release. It failed before because the asset name (UTV-dev-windows-x64.zip) does not contain the tag. -ZipPath installs from a zip on disk, such as a CI build artifact, including the zip-in-a-zip the GitHub web page serves for artifacts. The default path (no parameters) and explicit release versions resolve to the same download as before. A new workflow runs the installer on a Windows runner in each mode whenever scripts/install.ps1 changes. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Michael Oliver --- .github/workflows/installer-test.yml | 60 +++++++++++++++++++++++++ scripts/install.ps1 | 67 +++++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/installer-test.yml diff --git a/.github/workflows/installer-test.yml b/.github/workflows/installer-test.yml new file mode 100644 index 00000000..af3b8a17 --- /dev/null +++ b/.github/workflows/installer-test.yml @@ -0,0 +1,60 @@ +name: Installer Test (Windows) + +# Runs scripts/install.ps1 for real on a clean Windows runner whenever the installer changes, so a +# change cannot break the default install path unnoticed (see #73). + +on: + pull_request: + paths: + - "scripts/install.ps1" + - ".github/workflows/installer-test.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + install: + name: Install (${{ matrix.mode }}) + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + mode: [default, dev-build, zip-path, nested-zip] + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Run installer + shell: powershell + run: | + $ErrorActionPreference = "Stop" + $dir = Join-Path $env:RUNNER_TEMP "utv-install" + # -SkipDeps: the 390 MB dependency installer is not what is under test here. + $common = @{ InstallDir = $dir; SkipDeps = $true; NoShortcuts = $true } + $devUrl = "https://github.com/OpenUTV/utv/releases/download/dev-build/UTV-dev-windows-x64.zip" + + switch ("${{ matrix.mode }}") { + "default" { .\scripts\install.ps1 @common } + "dev-build" { .\scripts\install.ps1 @common -Version dev-build } + "zip-path" { + $zip = Join-Path $env:RUNNER_TEMP "utv.zip" + Invoke-WebRequest -Uri $devUrl -OutFile $zip -UseBasicParsing + .\scripts\install.ps1 @common -ZipPath $zip + if (-not (Test-Path $zip)) { throw "-ZipPath deleted the user's zip" } + } + "nested-zip" { + # What the GitHub web page gives for a CI artifact: a zip containing the UTV zip. + $inner = Join-Path $env:RUNNER_TEMP "inner\UTV-branch-windows-x64.zip" + New-Item -ItemType Directory -Force -Path (Split-Path $inner) | Out-Null + Invoke-WebRequest -Uri $devUrl -OutFile $inner -UseBasicParsing + $outer = Join-Path $env:RUNNER_TEMP "artifact.zip" + Compress-Archive -Path $inner -DestinationPath $outer -CompressionLevel NoCompression + .\scripts\install.ps1 @common -ZipPath $outer + } + } + + foreach ($f in @("bin\utv.exe", "bin\utvio.exe")) { + if (-not (Test-Path (Join-Path $dir $f))) { throw "Installer did not produce $f" } + } + Write-Host "Installed files present in $dir" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 3c19383d..f3387932 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -5,15 +5,27 @@ Installs OpenUTV to Program Files (or AppData for non-admin users), automatically verifies and installs OpenUTVDeps runtime dependencies, configures system PATH, and creates Start Menu and Desktop shortcuts. +.PARAMETER Version + "latest" (default) installs the latest release. A release tag such as "2026.9" installs that + release. "dev-build" installs the newest development pre-release built from main. +.PARAMETER ZipPath + Install from a UTV Windows zip already on disk (for example a CI build artifact) instead of + downloading a release. .EXAMPLE irm https://openutv.com/install.ps1 | iex irm https://raw.githubusercontent.com/OpenUTV/utv/main/scripts/install.ps1 | iex +.EXAMPLE + # Development pre-release (parameters can't be passed through "irm | iex") + & ([scriptblock]::Create((irm https://openutv.com/install.ps1))) -Version dev-build +.EXAMPLE + .\install.ps1 -ZipPath .\UTV-windows-x64.zip #> [CmdletBinding()] param( [string]$Version = "latest", [string]$DepsVersion = "26.5", [string]$InstallDir = "", + [string]$ZipPath = "", [switch]$SkipDeps = $false, [switch]$NoShortcuts = $false, [switch]$Uninstall = $false @@ -164,8 +176,39 @@ if ($depsFound) { } # Step 2: Resolve UTV Release Asset -Write-Host "`n--- Downloading OpenUTV ($Version) ---" -ForegroundColor Cyan -if ($Version -eq "latest") { +# +# The default (no parameters) installs the latest release. The opt-in paths below, -ZipPath and +# -Version dev-build, must not change what the default does. +$zipUrl = $null +$localZip = $null +if ($ZipPath) { + if (-not (Test-Path -LiteralPath $ZipPath -PathType Leaf)) { + Write-Error "-ZipPath: file not found: $ZipPath" + return + } + $localZip = (Resolve-Path -LiteralPath $ZipPath).Path + if ($Version -eq "latest") { $Version = "local" } + Write-Host "`n--- Installing OpenUTV from $localZip ---" -ForegroundColor Cyan +} else { + Write-Host "`n--- Downloading OpenUTV ($Version) ---" -ForegroundColor Cyan +} + +if (-not $localZip -and $Version -eq "dev-build") { + # The development pre-release keeps one moving tag, and its asset name does not contain the tag. + $apiUrl = "https://api.github.com/repos/$RepoOwner/$RepoName/releases/tags/dev-build" + try { + $releaseData = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + $asset = $releaseData.assets | Where-Object { $_.name -match "UTV-.*-windows-x64\.zip" } | Select-Object -First 1 + if ($asset) { + $zipUrl = $asset.browser_download_url + } + } catch { + Write-Warning "Could not query GitHub API for the dev-build release. Falling back to the default asset URL." + } + if (-not $zipUrl) { + $zipUrl = "https://github.com/$RepoOwner/$RepoName/releases/download/dev-build/UTV-dev-windows-x64.zip" + } +} elseif (-not $localZip -and $Version -eq "latest") { $apiUrl = "https://api.github.com/repos/$RepoOwner/$RepoName/releases/latest" try { $releaseData = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing @@ -179,16 +222,21 @@ if ($Version -eq "latest") { } } -if (-not $zipUrl) { +if (-not $localZip -and -not $zipUrl) { if ($Version -eq "latest") { $Version = "2026.9" } $zipUrl = "https://github.com/$RepoOwner/$RepoName/releases/download/$Version/UTV-$Version-windows-x64.zip" } Write-Host "Release Version: $Version" -ForegroundColor White -Write-Host "Download URL: $zipUrl" -ForegroundColor Gray $tempZip = Join-Path $env:TEMP "OpenUTV-$Version-windows-x64.zip" -Invoke-WebRequest -Uri $zipUrl -OutFile $tempZip -UseBasicParsing +if ($localZip) { + # Work on a copy: the temporary zip is deleted after extraction. + Copy-Item -LiteralPath $localZip -Destination $tempZip -Force +} else { + Write-Host "Download URL: $zipUrl" -ForegroundColor Gray + Invoke-WebRequest -Uri $zipUrl -OutFile $tempZip -UseBasicParsing +} # Step 3: Extract & Install Application Files Write-Host "`n--- Installing OpenUTV into $InstallDir ---" -ForegroundColor Cyan @@ -197,6 +245,15 @@ New-Item -ItemType Directory -Force -Path $tempExtract | Out-Null Expand-Archive -Path $tempZip -DestinationPath $tempExtract -Force Remove-Item -Force $tempZip -ErrorAction SilentlyContinue +# A CI artifact downloaded from the GitHub web page is a zip that contains the UTV zip. +if ($localZip -and -not (Test-Path (Join-Path $tempExtract "utv-windows-x64"))) { + $innerZips = @(Get-ChildItem -Path $tempExtract -Filter "*.zip" -File) + if ($innerZips.Count -eq 1) { + Expand-Archive -Path $innerZips[0].FullName -DestinationPath $tempExtract -Force + Remove-Item -Force $innerZips[0].FullName -ErrorAction SilentlyContinue + } +} + $sourceDir = Join-Path $tempExtract "utv-windows-x64" if (-not (Test-Path $sourceDir)) { $sourceDir = $tempExtract From b89882301d3c25dc44261f55028e968374925841 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Thu, 1 Oct 2026 19:53:19 -0700 Subject: [PATCH 2/2] ci: only rebuild branches when the branch build workflow itself changes Adding or editing an unrelated workflow (e.g. the installer test) triggered a full three-platform build. Also end the installer test step with exit 0: robocopy's non-zero success code was failing it after a successful install. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Michael Oliver --- .github/workflows/branch-build.yml | 2 +- .github/workflows/installer-test.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/branch-build.yml b/.github/workflows/branch-build.yml index 98f5c8c8..618b4b4e 100644 --- a/.github/workflows/branch-build.yml +++ b/.github/workflows/branch-build.yml @@ -10,7 +10,7 @@ on: - 'CMakeLists.txt' - 'build.sh' - '.github/actions/build-windows/**' - - '.github/workflows/**' + - '.github/workflows/branch-build.yml' - 'requirements.txt' permissions: diff --git a/.github/workflows/installer-test.yml b/.github/workflows/installer-test.yml index af3b8a17..bbf20b65 100644 --- a/.github/workflows/installer-test.yml +++ b/.github/workflows/installer-test.yml @@ -58,3 +58,5 @@ jobs: if (-not (Test-Path (Join-Path $dir $f))) { throw "Installer did not produce $f" } } Write-Host "Installed files present in $dir" + # The installer copies with robocopy, whose success codes are non-zero and would fail the step. + exit 0