diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1496cde..08d74051 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -178,7 +178,7 @@ jobs: # would scan its bundled extractor files and flag false-positive # secrets that ship inside third-party packages like yt-dlp). - name: trivy fs - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: fs scan-ref: . @@ -190,7 +190,7 @@ jobs: skip-dirs: .venv,jobs # Dedicated Dockerfile + compose static analysis (Trivy's IaC linter). - name: trivy config - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: config scan-ref: build/ diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 5aa06590..fade993b 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -139,6 +139,24 @@ jobs: clamscan --recursive --infected --bell /scan echo "ClamAV scan completed successfully. No infected files reported." + # macos-release.yml asserts its assets exist before uploading; this did + # not. action-gh-release defaults fail_on_unmatched_files to false, so a + # missing updater asset published a release that looked fine and went + # green, and the in-app updater then 404'd for every installed user. + - name: verify every asset exists + if: github.event_name == 'release' + run: | + for f in \ + dist/StemDeck-Linux-x64.tar.gz \ + dist/StemDeck-Linux-x64.tar.gz.sha256 \ + dist/StemDeck-Linux-x64.NVIDIA.tar.gz \ + dist/StemDeck-Linux-x64.NVIDIA.tar.gz.sha256 \ + dist/StemDeck-Linux-x64-app.tar.gz \ + dist/StemDeck-Linux-x64-app.tar.gz.sha256 \ + dist/StemDeck-Linux-x64-runtime-version.json; do + test -f "$f" || { echo "missing release asset: $f" >&2; exit 1; } + done + - name: upload artifacts # Only attach to a real release; a manual test build has nothing to upload to. if: github.event_name == 'release' @@ -150,6 +168,7 @@ jobs: # pushing :latest to GHCR, and makes the in-app updater offer a build # that was never verified. prerelease: ${{ github.event.release.prerelease }} + fail_on_unmatched_files: true files: | dist/StemDeck-Linux-x64.tar.gz dist/StemDeck-Linux-x64.tar.gz.sha256 diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 70883061..6103a65b 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -92,6 +92,25 @@ jobs: } Write-Host "ClamAV scan completed successfully. No infected files reported." + # See the same guard in linux-release.yml: action-gh-release silently + # tolerates missing files, so an absent updater asset shipped a green + # release the in-app updater could not use. + - name: verify every asset exists + shell: powershell + run: | + $required = @( + "dist/StemDeck-Windows-x64.NVIDIA.zip", + "dist/StemDeck-Windows-x64.NVIDIA.zip.sha256", + "dist/StemDeck-Windows-x64.zip", + "dist/StemDeck-Windows-x64.zip.sha256", + "dist/StemDeck-Windows-x64-app.zip", + "dist/StemDeck-Windows-x64-app.zip.sha256", + "dist/StemDeck-Windows-x64-runtime-version.json" + ) + foreach ($f in $required) { + if (-not (Test-Path $f)) { Write-Error "missing release asset: $f"; exit 1 } + } + - name: upload artifacts uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: @@ -101,6 +120,7 @@ jobs: # pushing :latest to GHCR, and makes the in-app updater offer a build # that was never verified. prerelease: ${{ github.event.release.prerelease }} + fail_on_unmatched_files: true files: | dist/StemDeck-Windows-x64.NVIDIA.zip dist/StemDeck-Windows-x64.NVIDIA.zip.sha256 diff --git a/build/Dockerfile b/build/Dockerfile index 5405f55c..cf0c38db 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -29,13 +29,23 @@ RUN apt-get update \ # deno -- yt-dlp uses it as the JS runtime for YouTube format # extraction. Staged here so the runner doesn't need curl/unzip. +# Pinned and checksummed, not "latest". Deno is the JS runtime yt-dlp feeds +# YouTube's challenge payload to, so it executes against untrusted input -- +# and an unverified binary fetched at build time is a supply-chain hole +# whether or not it is small. The desktop packaging scripts already pin and +# verify QuickJS for exactly this reason; this was the one path that did not. +# Bump DENO_VERSION and both hashes together. ARG TARGETARCH +ARG DENO_VERSION=v2.9.6 +ARG DENO_SHA256_AMD64=394f07f4da2bebe6ce6f1e7ce0fa16429b29b08c35e3fac3fe25972676dff4b2 +ARG DENO_SHA256_ARM64=9a46afc6c392c7cd2ff71a31558935545b46408d0e87f7a86908c712721c046e RUN case "${TARGETARCH:-$(dpkg --print-architecture)}" in \ - amd64) DENO_ARCH=x86_64-unknown-linux-gnu ;; \ - arm64) DENO_ARCH=aarch64-unknown-linux-gnu ;; \ + amd64) DENO_ARCH=x86_64-unknown-linux-gnu; DENO_SHA256="${DENO_SHA256_AMD64}" ;; \ + arm64) DENO_ARCH=aarch64-unknown-linux-gnu; DENO_SHA256="${DENO_SHA256_ARM64}" ;; \ *) echo "Unsupported arch: ${TARGETARCH}" && exit 1 ;; \ esac \ - && curl -fsSL -o /tmp/deno.zip "https://github.com/denoland/deno/releases/latest/download/deno-${DENO_ARCH}.zip" \ + && curl -fsSL -o /tmp/deno.zip "https://github.com/denoland/deno/releases/download/${DENO_VERSION}/deno-${DENO_ARCH}.zip" \ + && echo "${DENO_SHA256} /tmp/deno.zip" | sha256sum -c - \ && unzip /tmp/deno.zip -d /usr/local/bin \ && rm /tmp/deno.zip \ && /usr/local/bin/deno --version diff --git a/scripts/windows/make-portable.ps1 b/scripts/windows/make-portable.ps1 index 4288810c..759fd2cf 100644 --- a/scripts/windows/make-portable.ps1 +++ b/scripts/windows/make-portable.ps1 @@ -9,7 +9,19 @@ param( ) $ErrorActionPreference = "Stop" +# PowerShell 7+ only. CI invokes this script with `powershell` (Windows +# PowerShell 5.1), where this variable does nothing and $ErrorActionPreference +# does not cover native commands either -- so a failed pip install was ignored +# and the build carried on. Kept for a pwsh run; Assert-LastExitCode below is +# what actually enforces it on 5.1 (#517). $PSNativeCommandErrorActionPreference = "Stop" + +function Assert-LastExitCode { + param([Parameter(Mandatory)][string]$What) + if ($LASTEXITCODE -ne 0) { + throw "$What failed with exit code $LASTEXITCODE" + } +} Set-StrictMode -Version Latest if ($env:OS -ne "Windows_NT") { @@ -231,8 +243,10 @@ if (Get-Command "py" -ErrorAction SilentlyContinue) { } else { & python -m venv $PythonDir } +Assert-LastExitCode "creating the virtualenv" & $PythonExe -m pip install --upgrade pip +Assert-LastExitCode "pip self-upgrade" # The project version is git-derived (hatch-vcs). Pin it from $PackageVersion so # the install doesn't depend on git tags in the build checkout (#169). @@ -240,6 +254,7 @@ if ($PackageVersion) { $env:SETUPTOOLS_SCM_PRETEND_VERSION = ($PackageVersion -replace '^v', '') } & $PythonExe -m pip install "$Root" +Assert-LastExitCode "installing the StemDeck package" if ($CpuOnly) { # Force the slim CPU-only wheel. On Windows the default PyPI torch wheel is @@ -249,6 +264,10 @@ if ($CpuOnly) { & $PythonExe -m pip install torch==2.6.0+cpu torchaudio==2.6.0+cpu ` --index-url https://download.pytorch.org/whl/cpu ` --force-reinstall --no-deps + # Unchecked, a transient network failure here left whatever torch was already + # resolved in place and the zip labelled CPU shipped a non-CPU torch. The + # import checks later still pass, because torch imports fine either way. + Assert-LastExitCode "installing the CPU-only torch wheel" } # Do NOT bundle CUDA torch into the NVIDIA (non-CpuOnly) package. It ships base # torch and the desktop app installs the CUDA build on first run via