Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
143 changes: 143 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# GitHub Actions: lint + unit tests + security scans.
# Does not build or publish artifacts. Image scanning is done via
# trivy fs on the project tree (covers deps, secrets, and Dockerfile
# misconfig) so CI does not need a docker-in-docker setup.

name: CI

on:
pull_request:
push:
branches: [main]
release:
types: [published]

env:
UV_LINK_MODE: copy
# Version is git-derived (hatch-vcs). CI's clone is shallow/tagless, which
# makes setuptools_scm raise, so pin a placeholder for the build -- CI only
# lints/tests and never publishes. #169
SETUPTOOLS_SCM_PRETEND_VERSION: "0.0.0"

jobs:
lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
steps:
- uses: actions/checkout@v4
- run: uv sync --frozen --all-extras
- run: uv run ruff check app/ tests/
- run: uv run ruff format --check app/ tests/
- run: bash -n run.sh

test:
runs-on: ubuntu-latest
container:
image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
steps:
- uses: actions/checkout@v4
- run: apt-get update && apt-get install -y --no-install-recommends ffmpeg
- run: uv sync --frozen --all-extras
- run: uv run pytest tests/ -q

js-syntax:
runs-on: ubuntu-latest
container:
image: node:20-alpine
steps:
- uses: actions/checkout@v4
- run: for f in static/js/*.js; do node --check "$f"; done

sast-bandit:
runs-on: ubuntu-latest
container:
image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
steps:
- uses: actions/checkout@v4
- run: uv tool install bandit
- run: uv tool run bandit -r app/ -ll # fail on medium+ severity

deps-audit:
runs-on: ubuntu-latest
container:
image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
steps:
- uses: actions/checkout@v4
- run: uv tool install pip-audit
- run: uv pip compile pyproject.toml -o /tmp/requirements.txt
# Ignored CVEs (review when upgrading torch or demucs):
#
# torch 2.6.0 -- pinned to <2.7 because torchaudio 2.7+ removed its
# built-in audio writer and now requires torchcodec, which has ABI
# issues that break demucs 4.0.1's torchaudio.save() path. All torch
# CVEs below are in ops that StemDeck does not invoke; risk on a
# local-only, single-user app is negligible. Re-evaluate once demucs
# supports torch 2.7+ without torchcodec.
#
# joblib PYSEC-2024-277 -- no fix version available as of 2026-05-21
# (1.5.3 is latest). joblib is a transitive dep via demucs/librosa;
# StemDeck does not directly invoke joblib serialization. Drop once
# a patched release is available.
- run: |
uv tool run pip-audit -r /tmp/requirements.txt --strict \
--ignore-vuln CVE-2025-2953 \
--ignore-vuln CVE-2025-3730 \
--ignore-vuln PYSEC-2025-189 \
--ignore-vuln PYSEC-2025-190 \
--ignore-vuln PYSEC-2025-192 \
--ignore-vuln PYSEC-2025-193 \
--ignore-vuln PYSEC-2025-194 \
--ignore-vuln PYSEC-2025-195 \
--ignore-vuln PYSEC-2025-196 \
--ignore-vuln PYSEC-2025-197 \
--ignore-vuln PYSEC-2025-198 \
--ignore-vuln PYSEC-2025-199 \
--ignore-vuln PYSEC-2025-200 \
--ignore-vuln PYSEC-2025-201 \
--ignore-vuln PYSEC-2025-202 \
--ignore-vuln PYSEC-2025-203 \
--ignore-vuln PYSEC-2025-204 \
--ignore-vuln PYSEC-2025-205 \
--ignore-vuln PYSEC-2025-206 \
--ignore-vuln PYSEC-2025-207 \
--ignore-vuln PYSEC-2025-208 \
--ignore-vuln PYSEC-2025-209 \
--ignore-vuln PYSEC-2025-210 \
--ignore-vuln PYSEC-2026-139 \
--ignore-vuln PYSEC-2024-277 \
--ignore-vuln CVE-2025-2148 \
--ignore-vuln CVE-2025-2149 \
--ignore-vuln CVE-2025-2998 \
--ignore-vuln CVE-2025-2999 \
--ignore-vuln CVE-2025-3000 \
--ignore-vuln CVE-2025-3001

trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Scans the source tree for: known CVEs in deps, leaked secrets,
# and Dockerfile / compose misconfigurations. Skips .venv (it can
# be left over from earlier steps in the shared workspace; trivy
# 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
with:
scan-type: fs
scan-ref: .
scanners: vuln,secret,misconfig
severity: HIGH,CRITICAL
exit-code: '1'
ignore-unfixed: true
trivyignores: .trivyignore
skip-dirs: .venv,jobs
# Dedicated Dockerfile + compose static analysis (Trivy's IaC linter).
- name: trivy config
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: build/
severity: HIGH,CRITICAL
exit-code: '1'
132 changes: 132 additions & 0 deletions .github/workflows/macos-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: macOS Release

on:
push:
tags:
- 'v*'
release:
types: [published]

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-upload:
# Runner must be darwin/arm64 with Rosetta 2, Xcode CLT, Homebrew, rustup, and zstd.
runs-on: [self-hosted, osx, arm64]
timeout-minutes: 120
permissions:
contents: write
defaults:
run:
shell: bash
steps:
- name: clean workspace
run: rm -rf .build dist

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: write version files
run: |
if [ -z "${GITHUB_REF_NAME:-}" ]; then
echo "GITHUB_REF_NAME is not set" >&2
exit 1
fi
VERSION="${GITHUB_REF_NAME#v}"
printf '{ "version": "%s" }\n' "$VERSION" > static/version.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" desktop/src-tauri/Cargo.toml
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" desktop/src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" desktop/package.json
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "Wrote version $VERSION to all version files"

- name: build macOS arm64
env:
SSL_CERT_FILE: /etc/ssl/cert.pem
REQUESTS_CA_BUNDLE: /etc/ssl/cert.pem
run: |
command -v zstd
command -v uv >/dev/null 2>&1 || brew install uv
uv python install cpython-3.12-macos-aarch64-none
ARM64_PYTHON="$(uv python find cpython-3.12-macos-aarch64-none)"
scripts/macos/make-iconset.sh
ARCH=arm64 VERSION="$VERSION" PYTHON_BIN="$ARM64_PYTHON" scripts/macos/make-runtime-pack.sh
ARCH=arm64 VERSION="$VERSION" scripts/macos/make-app.sh
ARCH=arm64 VERSION="$VERSION" scripts/macos/make-dmg.sh

- name: build macOS x64
env:
SSL_CERT_FILE: /etc/ssl/cert.pem
REQUESTS_CA_BUNDLE: /etc/ssl/cert.pem
run: |
command -v zstd
if ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
echo "ERROR: Rosetta 2 is required to build the x64 runtime on this agent." >&2
exit 1
fi
rustup default stable
rustup target add x86_64-apple-darwin
command -v uv >/dev/null 2>&1 || brew install uv
uv python install cpython-3.12-macos-x86_64-none
X64_PYTHON="$(uv python find cpython-3.12-macos-x86_64-none)"
ARCH=x64 VERSION="$VERSION" PYTHON_BIN="$X64_PYTHON" scripts/macos/make-runtime-pack.sh
ARCH=x64 VERSION="$VERSION" scripts/macos/make-app.sh
ARCH=x64 VERSION="$VERSION" scripts/macos/make-dmg.sh

- name: inspect artifacts
run: |
test -f .build/macos-dist/StemDeck-macOS-arm64.dmg
test -f .build/StemDeck-runtime-macOS-arm64.tar.zst
test -f .build/macos-dist/SHA256SUMS-macOS-arm64.txt
test -f .build/macos-dist/StemDeck-macOS-x64.dmg
test -f .build/StemDeck-runtime-macOS-x64.tar.zst
test -f .build/macos-dist/SHA256SUMS-macOS-x64.txt
cat .build/macos-dist/SHA256SUMS-macOS-arm64.txt
cat .build/macos-dist/SHA256SUMS-macOS-x64.txt
du -sh .build/macos-dist/StemDeck-macOS-arm64.dmg .build/StemDeck-runtime-macOS-arm64.tar.zst
du -sh .build/macos-dist/StemDeck-macOS-x64.dmg .build/StemDeck-runtime-macOS-x64.tar.zst
if find desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app \
\( -iname '*python*' -o -iname '*torch*' -o -iname '*ffmpeg*' -o -iname '*ffprobe*' \) |
grep -q .; then
echo "arm64 StemDeck.app contains runtime binaries that should stay outside the DMG." >&2
exit 1
fi
if find desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/StemDeck.app \
\( -iname '*python*' -o -iname '*torch*' -o -iname '*ffmpeg*' -o -iname '*ffprobe*' \) |
grep -q .; then
echo "x64 StemDeck.app contains runtime binaries that should stay outside the DMG." >&2
exit 1
fi
for arch in arm64 x64; do
mountpoint="$(mktemp -d /tmp/stemdeck-dmg.XXXXXX)"
hdiutil attach ".build/macos-dist/StemDeck-macOS-$arch.dmg" -readonly -nobrowse -mountpoint "$mountpoint"
trap 'hdiutil detach "$mountpoint" >/dev/null 2>&1 || true; rmdir "$mountpoint" >/dev/null 2>&1 || true' EXIT
test -d "$mountpoint/StemDeck.app"
test -L "$mountpoint/Applications"
test -f "$mountpoint/README-macOS.txt"
test -f "$mountpoint/THIRD_PARTY_NOTICES.txt"
hdiutil detach "$mountpoint"
rmdir "$mountpoint"
trap - EXIT
done

- name: upload artifacts
if: github.event_name == 'release'
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: |
.build/macos-dist/StemDeck-macOS-arm64.dmg
.build/StemDeck-runtime-macOS-arm64.tar.zst
.build/macos-dist/SHA256SUMS-macOS-arm64.txt
.build/macos-dist/StemDeck-macOS-x64.dmg
.build/StemDeck-runtime-macOS-x64.tar.zst
.build/macos-dist/SHA256SUMS-macOS-x64.txt
append_body: true
body: |
### Artifact build

- macOS arm64 and x64 DMGs and runtime packs were built and inspected on a macOS GitHub Actions runner before upload.
107 changes: 107 additions & 0 deletions .github/workflows/windows-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Windows Release

on:
push:
tags:
- 'v*'
release:
types: [published]

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-upload:
# Runner must be windows/x64 with PowerShell, Docker, and rustup.
runs-on: [self-hosted, windows, x64]
timeout-minutes: 90
permissions:
contents: write
defaults:
run:
shell: pwsh
steps:
- name: clean workspace
run: |
Remove-Item -Recurse -Force .build, dist -ErrorAction SilentlyContinue

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: write version files
run: |
$tag = $env:GITHUB_REF_NAME
if (-not $tag) { throw "GITHUB_REF_NAME is not set" }
$version = $tag -replace '^v', ''
$json = "{`"version`": `"$version`"}"
Set-Content -Path "static/version.json" -Value $json -Encoding UTF8
(Get-Content "desktop/src-tauri/Cargo.toml") -replace '^version = ".*"', "version = `"$version`"" |
Set-Content "desktop/src-tauri/Cargo.toml"
(Get-Content "desktop/src-tauri/tauri.conf.json") -replace '"version": "[^"]*"', "`"version`": `"$version`"" |
Set-Content "desktop/src-tauri/tauri.conf.json"
(Get-Content "pyproject.toml") -replace '^version = ".*"', "version = `"$version`"" |
Set-Content "pyproject.toml"
(Get-Content "desktop/package.json") -replace '"version": "[^"]*"', "`"version`": `"$version`"" |
Set-Content "desktop/package.json"
Write-Host "Wrote version $version to all version files"

- name: build Windows NVIDIA
run: |
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/windows/make-portable.ps1 `
-PackageName StemDeck-Windows-x64.NVIDIA `
-PackageVersion "$env:GITHUB_REF_NAME" `
-StripVenv

- name: build Windows CPU
run: |
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/windows/make-portable.ps1 `
-PackageName StemDeck-Windows-x64 `
-PackageVersion "$env:GITHUB_REF_NAME" `
-CpuOnly `
-StripVenv

- name: scan artifacts
run: |
Write-Host "Preparing ClamAV scan for Windows release artifacts..."
Write-Host "Artifacts staged in: $PWD\dist"
Get-ChildItem -Path "dist" -File |
Sort-Object Name |
Select-Object Name,
@{Name="SizeMB"; Expression={ [math]::Round($_.Length / 1MB, 2) }} |
Format-Table -AutoSize

Write-Host "SHA256 checksums:"
Get-ChildItem -Path "dist" -Filter "*.zip" -File |
Sort-Object Name |
ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 $_.FullName
Write-Host " $($hash.Hash) $($_.Name)"
}

Write-Host "Pulling latest ClamAV scanner image..."
docker pull clamav/clamav:latest

Write-Host "Running ClamAV scan over dist/..."
docker run --rm -v "${PWD}/dist:/scan:ro" clamav/clamav:latest `
clamscan --recursive --infected --bell /scan
if ($LASTEXITCODE -ne 0) {
throw "ClamAV scan failed or reported infected files. Exit code: $LASTEXITCODE"
}
Write-Host "ClamAV scan completed successfully. No infected files reported."

- name: upload artifacts
if: github.event_name == 'release'
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: |
dist/StemDeck-Windows-x64.NVIDIA.zip
dist/StemDeck-Windows-x64.NVIDIA.zip.sha256
dist/StemDeck-Windows-x64.zip
dist/StemDeck-Windows-x64.zip.sha256
append_body: true
body: |
### Artifact scan

- Windows portable packages were scanned with ClamAV in CI before upload.
Loading
Loading