main
34 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b9437115ca |
Only offer an update once the release has been promoted (#429)
Two halves of the same bug. The upload steps passed no `prerelease` to softprops/action-gh-release, which defaults it to false and writes it back. Attaching assets therefore promoted the release it was attaching them to. v0.13.0 and v0.14.0 were both published as pre-releases and both ended up marked as the latest release within the hour, which also fired `released` and pushed :latest to GHCR. Each workflow now carries the release's own flag. The updater took the newest non-draft release, pre-releases included. That was deliberate when it was written, because /releases/latest hides pre-releases and every release was one, so tracking stable meant nobody would ever be notified. With the flag preserved, a pre-release is now genuinely a pre-release, and offering it would push unverified builds to everyone. It now takes the newest release that is neither draft nor pre-release, so a release reaches users only once it has been promoted. The e2e stub grew an unpromoted pre-release ahead of the stable one, and the spec pins which of the two the card names. Co-authored-by: Thales <> |
||
|
|
d0e6d01971 |
chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#426)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
9b655d5a07 |
Windows: leaner portable package and opt-in in-app updater (#421) (#423)
* feat(windows): leaner portable package and opt-in in-app updater (#421) Issue #421 asked for Python embedded in a single EXE so updating would not mean copying ~20k loose files over an existing install. A onefile EXE is not viable for this stack (onefile modes re-extract the whole multi-GB payload on every launch, and torch/onnxruntime fight frozen-import hooks), so this addresses the root cause instead: ship less, and stop making users hand-copy a full zip for a release that only changed app code. Leaner package (make-portable.ps1): - Stripping is now unconditional. The -StripVenv opt-in gate was a silent regression risk: nothing stopped a future workflow edit from shipping the unstripped venv with no error. - Also strips stdlib base/Lib/test and per-package test/tests dirs. - Deliberately does NOT strip .dist-info/RECORD. pip needs it to replace a package, and install_cuda_torch pip-installs into this venv on every NVIDIA machine at first run; removing it yields "Failed to uninstall ... missing RECORD file". - Adds a post-strip import check so an over-aggressive strip fails the build rather than a release. Updater (main.rs, catalog.js): - New commands installed_runtime_id, download_app_update, apply_app_update. - Opt-in: the check on launch is unchanged, but download and apply are each an explicit click. It never auto-applies and never interrupts a running job. - Replaces StemDeck.exe and backend/ only. python/ is never touched, because an NVIDIA install rewrites it with CUDA torch at first run and torchDeviceSettled skips ensure_torch_device once the device is cuda, so swapping the directory would silently drop that machine to CPU with no recovery. - The runtime id (uv.lock + interpreter major.minor) is a compatibility gate, not a download trigger: if a release changed the Python dependency set the updater stands down and points at the full download. Only 19 of the last 200 commits touch uv.lock, so the fast path covers most releases. - apply_app_update stages and validates everything before any destructive rename, stops the backend synchronously first (the existing stop_backend returns before the process dies, which would have made every update fail on Windows), and retries renames past transient AV/indexer handles. - Known gap, documented in code: the two exe renames are not atomic. A hard crash in that window leaves StemDeck.exe.old needing a manual rename. Closing it needs a bootstrap launcher that is never itself replaced. CI publishes -app.zip, its .sha256 and -runtime-version.json alongside the unchanged full zips. Fresh installs are unaffected. i18n: the 5 new strings are translated into all 7 language tables, not just English. t() falls back to English silently, so an English-only key looks correct in testing and ships untranslated to six locales. Verified: Windows and Linux (WSL) both compile clean with no new clippy warnings, 39 Rust tests pass on both, JS suites pass, ruff clean. Two new unit tests pin the JSON contract between the PowerShell writer and the Rust reader. Not yet verified: no end-to-end run against a real release. * fix(updater): make the in-app update actually work, verified end to end (#421) Built both packages on a real Windows box and drove the whole flow. Four bugs that only surfaced by running it, none of which static checks could see. 1. Stale version after updating. app_version() read installed dist metadata, which lives in python/ -- the directory the updater deliberately never replaces. A self-updated install kept reporting the old version and would re-offer an update it had already applied, forever. It now prefers the app layer's static/version.json, which moves with backend/. Gitignored, so Docker and source checkouts still fall through to the hatch-vcs metadata. Proven: after a real update, python/ dist-info says 0.13.0 while /api/health reports 0.13.1. 2. The page CSP blocked the whole feature. The UI is served over http by the Python backend, so its connect-src applies: api.github.com is allowed, github.com and objects.githubusercontent.com are not, and that is where release assets live. Fetching the checksum and runtime id from JS was refused, so the pill would simply never appear. Those two reads moved into Rust (check_app_update), whose HTTP client is not bound by the page CSP, so the policy from #171 stays exactly as tight as it was. 3. plugin:event|listen refused by the Tauri ACL. App-defined commands are not ACL-gated but plugin commands are, and the capability does not cover the remote http origin the UI is served from. The progress bar is now indeterminate instead of granting a remote origin event permissions to put a percentage on a 5 MB download. 4. The post-strip import check re-bloated the package. Running Python regenerated 1,912 files / 39 MB of __pycache__ that the strip had just removed, cancelling nearly all of it: the net saving was 180 files. Swept once after the last interpreter run, and backend/ no longer ships a developer's local __pycache__ either. Also: the *.old sweep now runs on every launch rather than only on a version change. apply_app_update relaunches then exits, so on the first launch of the new build Windows still holds StemDeck.exe.old open, the delete fails silently, and gated on a change that already happened it would never retry. Observed for real: 15.7 MB stranded. Verified swept on the next launch. UI: "Update now" is an accent pill BESIDE Download, not a replacement, so the zip stays one click away and is the escape hatch if an update fails. Measured against the published v0.13.0 package: 18,143 -> 16,056 files (-2,087, -11.5%) and 883 -> 850 MB. The real win for #421 is the update path itself: 5 MB and 123 files instead of 284 MB and 16,056. Verified on this machine: a real 6-stem Demucs separation through the stripped package; the full notify -> Update now -> download -> restart -> relaunch cycle, after which user data (job, 7 stems, 130 MB of models), portable.txt, cpu-only and python/ were all untouched; and the safety gate correctly declining, with no download attempted, when the release's runtime id differs. Not covered: the NVIDIA package was not built, though the risk that motivated the gate is structurally gone now that python/ is never swapped. * fix: address code-quality review on the version-source change (#421) Both findings from the automated review were fair. Narrow the bare `except Exception: pass` in app_version() to (OSError, ValueError, AttributeError). That is bandit B110, which this repo's own security conventions call out. The three cover every real failure here -- absent or unreadable file, invalid JSON or bad encoding, and valid JSON that is not an object so has no .get -- while letting an actual bug in the function surface instead of silently degrading the reported version. Bandit now reports no issues for the file. Use one import style in test_health_api.py so app.main is no longer imported both as `import app.main as main` and `from app.main import app` in the same module. Also added a "[]" case: JSON that parses but is not an object, which is the AttributeError branch the narrowed except now names explicitly. * feat(updater): extend the in-app update to Linux (#421) Linux ships the same shape as Windows -- executable, backend/ and python/ side by side -- so the updater generalises rather than needing a second design. The platform-specific parts are now three small seams: the archive format, the executable name, and one new gate. Rust: - widen the updater's cfg gates from `windows` to `any(windows, linux)`, and replace extract_zip_archive with extract_update_archive, which uses zip on Windows and the existing extract_tar_archive on Linux - APP_EXE_NAME so the swap and the leftover sweep stop hardcoding StemDeck.exe - stop_backend_and_wait now sends SIGTERM and waits before escalating on unix, matching what stop_backend already does on window close - new app_root_is_writable gate: packaging/linux/install.sh offers a global install into /opt/stemdeck, which is root-owned while the app runs as the user. check_app_update declines up front rather than failing part way through a swap. Windows portable installs are user-writable by construction, but the probe is cheap and honest on both. tar rather than zip on Linux is deliberate: it preserves the executable bit. A zip would land StemDeck without +x and the relaunch after an update would fail with a permission error. Packaging (scripts/linux/make-portable.sh): - write python/runtime-version.json using the same uv.lock + interpreter major.minor formula as the Windows script, so the compatibility gate behaves identically on both - bring the strip to parity: stdlib test/, per-package test/tests, a post-strip import check, and a final __pycache__ sweep after the last interpreter run - PUBLISH_UPDATER_ASSETS=1 emits the slim app-layer tarball, its checksum and the runtime marker; wired into the CPU build in linux-release.yml since StemDeck and backend/ are identical between both variants Frontend: updaterAssetNames() maps the platform to its asset names, and the wiring is gated on that rather than on os === "windows". macOS is deliberately still excluded, and the comments now say why rather than just that it is: backend_dir() resolves the backend inside the downloaded runtime pack rather than the .app, so its app layer is a different thing and the existing runtime-pack updater already covers most of it. Verified: both platforms compile clean with no new clippy warnings, 42 tests on Windows and 43 on Linux (the extra one is the read-only-root gate, which is meaningless on Windows). The app-layer archive was round-tripped on Linux to confirm it contains exactly StemDeck + backend/, that python/ does not leak into it, that the executable bit survives, and that replacing a running binary works. Not yet run end to end against a real Linux release. * fix(updater): see pre-releases, and compile the Rust in CI (#421) Two gaps that would each have undermined the update flow on release day. The update check polled /releases/latest, which GitHub defines as the most recent NON-PRERELEASE, non-draft release. Ship a version with the pre-release box ticked and it becomes invisible: no notification, no update button, on any platform, with nothing in the logs to explain it. StemDeck has always published even its alphas as normal releases (v0.8.0-alpha.17 has prerelease=false), which is the only reason this has not bitten yet -- it was a trap waiting on someone ticking a box. Now polls the releases list and takes the newest non-draft, so it is correct either way. Drafts stay excluded: they are already invisible unauthenticated, and a maintainer should not be offered a release whose assets do not exist yet. windows-check.yml and macos-check.yml now also run on pull requests that touch desktop/src-tauri/**, not workflow_dispatch only. This PR added roughly 600 lines of mostly cfg-gated Rust across two commits and every CI check passed without compiling a single line of it; the comment at the top of windows-check.yml notes that exact gap already shipped a broken Windows build in v0.11.1's first release attempt. Scoped by path so the self-hosted runners see no extra load from the majority of PRs, which never go near src-tauri. This also gets the macOS branch compiled for the first time. Local verification covered Windows and Linux, so the cfg(not(any(windows, linux))) arm of the three updater commands has never been near a compiler. * test(e2e): match the releases-list shape the app now polls (#421) The update-check stub returned a single release object, which was right for /releases/latest. The app now polls the releases list so a pre-release is still seen, so the fixture has to return an array or checkForUpdate bails and the release card never appears. Caught by frontend-e2e on the previous commit, which is the suite doing exactly its job: the only assertion that covers this path is report-failure.spec.mjs:98, and it went red immediately. * feat(i18n): add French, and make the runtime id line-ending independent French is a complete table, not a partial one: 435 keys, the same set German and Portuguese carry (English's 443 minus the ten Polish-only .few/.many forms and the bare upload.skippedFiles, plus singular forms for the three playlist.skip.* families). French takes the one/other buckets, so plural() needs no change. Verified with the checks from .claude/rules/i18n.md: the drift check reports clean, and separately there are zero {placeholder} mismatches and zero HTML tag mismatches against English. The 27 strings identical to English are genuinely identical in French (Piano, Solo, Transport, Position, LUFS, Standard, Port, the brand names, CUDA (NVIDIA), MPS (Apple Silicon)). Separately: the runtime id was being computed from the raw bytes of uv.lock, so a Windows checkout with core.autocrlf=true hashed CRLF and Linux hashed LF, and the same lockfile produced two different ids -- caught by building the Linux package and seeing py3.12-d74d6ef80c5e9d1f where Windows had produced py3.12-dbda45e38e1044cf. Each platform stayed self-consistent so the gate still worked, but the id would shift spuriously if a runner's autocrlf ever changed, silently declining app-only updates that were in fact compatible. Both scripts now hash the content with newlines normalised; PowerShell, bash and a reference Python implementation all agree on d74d6ef80c5e9d1f. * chore: pin Unraid template to 0.14.0 Per .claude/rules/unraid-template-version.md this is an explicit decision each time, not a default. Confirmed for this release. The 0.14.0 GHCR image is published by docker-publish.yml when the release is created, so the tag exists shortly after this lands. --------- Co-authored-by: Thales <> |
||
|
|
bf561a6c81 |
Lead/backing vocal split, stems relocation fixes, eager model pre-download (#406)
* Add on-demand lead/backing vocal split, fix stems relocation bugs, and eager model pre-download (#275, #403) Lead/backing vocal split: - New on-demand POST /api/jobs/{id}/vocal-split endpoint, running UVR-MDX-NET Karaoke 2 (audio-separator) as a second pass over Demucs's vocals.wav - Desktop and mobile UI toggle to request the split, auto-chained once the base separation finishes, for both foreground and background jobs - Mixer shows Lead Vocals / Backing Vocals lanes in place of Vocals once split Stems relocation fixes (#403): - user-data.json (library metadata) now lives inside the jobs folder so it follows a Settings relocation instead of staying behind in Documents - The relocation endpoint's settings persist step was silently swallowing write failures and reporting false success; it now reports persisted: false and the Settings UI shows a clear warning instead Desktop setup wizard: - Demucs, beat-this, and the karaoke model now download eagerly during first-boot setup instead of lazily on first use Also: - Credit audio-separator / Ultimate Vocal Remover in the README per its license's attribution requirement, plus a license audit in docs/models.md - Add models/ to .gitignore * ci: install build-essential so diffq (audio-separator's dependency) can compile diffq has no prebuilt wheel for Python 3.11+ on Linux, its last release only ever shipped cp310 wheels, so uv sync must compile it from source, which needs gcc. Docker and the Linux desktop release build already install build-essential for the same reason; the plain lint/test CI container never needed it before audio-separator (#275) pulled diffq in. * chore: pin Unraid template to 0.12.0 This PR ships as v0.12.0, per the user's decision given it introduces the new lead/backing vocal split feature. --------- Co-authored-by: Thales <> |
||
|
|
405fec28a1 |
Revert "ci: sign the Windows release executable with SignPath (#394)"
This reverts commit
|
||
|
|
7eba6340ef |
ci: sign the Windows release executable with SignPath (#394)
SignPath's OSS tier requires every job leading up to a signing request to run on a GitHub-hosted agent, but the Windows release runs entirely on the self-hosted runner. Split the workflow instead of moving the whole build: - New `sign-exe` job on `windows-latest` builds only StemDeck.exe, uploads it as a workflow artifact, submits it to SignPath, and republishes the signed binary as an artifact. Version files are stamped before the build because SignPath restricts Foundation projects on PE product name and version. - `build-and-upload` now depends on it, downloads the signed executable, and packages both variants around it via a new `-PrebuiltExe` flag on make-portable.ps1. That also removes the redundant second Rust build the CPU package used to trigger. - make-portable.ps1 rejects an unsigned prebuilt binary before it reaches the zip, and the scan step reports the Authenticode status of what was packaged. - workflow_dispatch entry point plus a release-only guard on the upload step so the integration can be exercised against a test-signing policy without cutting a tag. Requires repo secret SIGNPATH_API_TOKEN and repo variable SIGNPATH_ORGANIZATION_ID. Adds the code signing policy and attribution required by the SignPath Foundation terms. |
||
|
|
1954bfda8c |
windows-check.yml: use powershell, not bash
bash on this self-hosted Windows runner mangles the auto-generated temp script's path (backslashes stripped), the same thing windows-release.yml's own steps already worked around by using powershell. Copied macos-check.yml's shell: bash default without adjusting for the platform - caught by this workflow's own first real run (exit 1 on cargo fmt --check, before it even reached the code). |
||
|
|
5f5fead50c |
Add an on-demand Windows Rust check on the self-hosted runner
Companion to macos-check.yml. Justified twice over in one session: first by finding pre-existing macOS-only clippy issues nothing had ever caught, then for real when download_file silently lost its #[cfg(unix)] gate and shipped a broken Windows build in v0.11.1's first release attempt - undetected by ci.yml (100% ubuntu-latest) or macos-check.yml (macOS also satisfies unix, so it never exercised the Windows-only code path either). Same shape as macos-check.yml: build/clippy/test only, workflow_dispatch only, never touches packaging or uploads. |
||
|
|
2bf03d05b1 |
macos-check.yml: suppress 3 pre-existing macOS-only lint findings
This workflow's first run found unused_variables/dead_code in ensure_torch_device, classify_cuda_install_error, and child_output_with_timeout -- none touched by this branch. Nothing has ever type-checked the macOS target before this workflow existed, so these predate it rather than being caused by it. Suppressed the same way as the 3 pre-existing Linux-side clippy lints, so a real regression isn't lost in known noise. |
||
|
|
84108250cf |
Add an on-demand macOS Rust check on the self-hosted runner
ci.yml (PR-triggered) is 100% ubuntu-latest, so code behind #[cfg(target_os = "macos")] has never had a real compiler pass before merging - that cfg-gated code is stripped before semantic analysis even starts on a non-matching target, not just skipped at test time. macos-release.yml already has a real macOS runner, but only fires on release: published and does the full signed/packaged build. This is deliberately narrow: build/clippy/test only, workflow_dispatch only (not on every PR/push, so the runner's load and cost don't change from today), never touches signing, packaging, or uploads. |
||
|
|
1e1cf0bbce |
test(frontend): add browser tests, starting with the export menu (#365)
Implements #339. CI ran one check on static/js -- a syntax parse -- so any behavioural regression shipped unnoticed until a user hit it. #335 is the case in point: "Export All Stems" became permanently unclickable after a single export, for every track, until the app restarted. It shipped in alpha 15 and a user found it. Setup is Playwright against the real backend. tests/e2e/serve.sh seeds a throwaway jobs directory with one finished track and execs uvicorn against it, with every data path redirected, so a run cannot read or touch a developer's library. Only the separation pipeline is skipped; the endpoints, the registry and the Range requests for stems are real. Two details in the fixtures are load-bearing, both learned by getting them wrong first: - The sidebar renders from the library store, not /api/jobs. A job on disk but absent from the store is invisible in the UI, and a test that clicks nothing passes for the wrong reason. - stubTauri installs a controllable window.__TAURI__ so the desktop code path runs. This is the point of the exercise: #335 was invisible in a browser, because there the synthetic <a>.click() closes the chip panel before the busy state is applied and the bug hides. The stub also leaves save_audio_file pending until the test settles it, so the busy state machine is driven rather than raced. Nine tests cover all four defects from #335 and #337: rows re-enabled after an export in both host modes, a second export still working, the busy state waiting on the save rather than a timer, failures surfacing and recovering, and export errors not offering a "Try again" that sends the user to the URL import field. Verified by reintroducing each defect and watching the suite fail: clearing only the visible rows on reset fails 3 tests (the panel is closed by then, so a visibility-filtered clear clears nothing), the fixed-timer reset fails 1, and the retry button fails 1. MP4 format switching is only partly covered. The video format is hidden unless the track has one, and a video fixture is its own piece of work, so what is here pins that MP4 is not offered for audio-only tracks. Beat grid and transport coverage remain open on #339. Closes #339 |
||
|
|
5565b216ba |
feat(linux): add an optional installer for desktop integration (#364)
Implements #342. StemDeck stays portable: extract the tarball, run ./StemDeck, and none of this is required. install.sh is there for people who would rather launch from their applications menu. It installs the package it sits in and never downloads anything, so the version and the CPU/NVIDIA variant come from the package itself (backend/static/version.json and the cpu-only marker) and cannot drift from the build being installed. That also removes any need to verify a second download. Design notes, mostly things the reference installer in #342 got wrong: - Install is atomic. The new copy goes to <target>.new and is verified before the old one is moved aside, so a failure partway leaves the working install untouched. Removing the old copy first is what made a failed upgrade in that fork leave the machine with no StemDeck, no launcher and no manifest recording where it had been. - A failed copy cleans up its own staging directory rather than leaving a package-sized partial on disk. - Exec is quoted, so an install path containing a space still launches. - Version comparison is semver-aware. sort -V ranks 0.8.0-alpha.17 above 0.8.0, which would tell every pre-release user they were current the day a stable release shipped. - Reading a missing manifest key yields empty rather than killing the script, which under set -euo pipefail is what a grep|head|cut pipeline does. - Global installs put the launcher in /usr/share/applications and the icon in /usr/share/pixmaps, so other users on the machine can see it. - Installing from inside the destination is refused rather than moving the running script out from under bash. - Non-x86_64 machines get a clear refusal instead of a binary that cannot run. User data is never touched. Stems live in ~/Documents/StemDeck and the runtime, models and logs in $XDG_DATA_HOME/stemdeck, both outside the install directory. Legacy data/ from pre-migration builds is carried across an upgrade, and uninstall refuses to delete it, leaving the folder and saying why. tests/linux/test_install_sh.sh runs the real installer against a synthetic package in a throwaway HOME: 52 checks covering install, upgrade, the failed-upgrade case, uninstall, corrupt manifests, paths with spaces, legacy data, self-install, arch refusal and the semver table. CI runs it on Linux with shellcheck and desktop-file-validate. Closes #361 |
||
|
|
8d670e3b69 |
fix(player): read the whole WAV header, and say so when a track cannot load (#362)
* fix(player): read past the first 1 KB when locating the WAV data chunk The chunked engine parses WAV containers itself and asked only for bytes 0-1023 when looking for the `data` chunk. RIFF is a linked list, so anything the writer puts in front of `data` -- a LIST/INFO block, a JUNK chunk padded for sector alignment -- pushes it out of that window. The parser returned null, the engine reported a duration of 0, and playback was disabled. The track rendered normally and the header showed its real length, so it looked like a GPU or renderer fault rather than a parse failure (#343). Walk the chunk table properly and widen the request when it runs past what was fetched, capped at 1 MB and 5 attempts. The parser reports "need more bytes" separately from "not a WAV", since only the caller knows whether more bytes can be had. Two further container cases fixed along the way: - WAVE_FORMAT_EXTENSIBLE carries the real format code in its SubFormat GUID. Without reading it, a float32 file parsed cleanly and then decoded to silence. - A `data` size of 0 or 0xffffffff, written by encoders that stream to a non-seekable target and never patch the length, gave a duration of 0 or 24347 seconds respectively. Clamp to the real length reported in Content-Range. Adds tests/js/wav-header.test.mjs, which drives the real engine against synthetic layouts through a Range-honouring fetch stub. Against the pre-fix engine it reports 25/38, with JUNK 4096, LIST 2 KB, JUNK 300 KB and both unpatched data sizes failing. CI runs it alongside node --check. Closes #358 * fix(player): tell the user when a track's audio fails to load A track whose stems could not be loaded left the studio looking normal and said nothing. The only trace was a console warning, which in a release desktop build has no reachable devtools, so the failure was invisible to the user and undiagnosable from a bug report. Working out why #343 could not play took a screenshot and a round trip for a hexdump. Both engines now record why ready() resolved false and expose it via getLoadError(), separating a stem that could not be fetched from one that could not be parsed or decoded -- those send the user somewhere completely different. The player puts that message in the error box above the track header. Playback errors reuse the import error box, so they are tagged: the player retracts its own message when another track loads, without wiping an import failure the user has not read yet. Nothing cleared that box on track switch before. The player also now retries with the full-decode engine when the chunked one cannot read a container, under the same RAM ceiling the missing-peaks swap uses. The browser's own decoder handles layouts the hand-rolled parser may not, so this turns "playback disabled" into "playback works" for the whole class of container problems behind #343. Closes #359 * fix(player): reject sample formats the chunked engine cannot decode _pcmToAudioBuffer only handles 16-bit PCM and 32-bit float, but the header parser accepted any depth. A 24-bit or 32-bit-integer file therefore measured correctly, reported ready, and then decoded to nothing on every chunk. That is worse than failing outright. An all-empty chunk result is treated as a transient network failure and evicted from the cache, so the scheduler retries it on the next animation frame, forever, with the playhead pinned at zero and no message on screen. Measured against a synthetic 24-bit file with playback running: 82 range requests in 700 ms (~117/sec) versus 3 for a healthy file. Reject those formats at parse time instead. The engine then reports a readable reason and the player hands the file to the full-decode engine, whose decoder handles 24-bit and integer PCM -- so these files now play instead of hanging. Verified end to end with a real ffmpeg-produced 24-bit stem: the chunked engine declines it, the fallback picks it up, the transport advances, and playback issues no further range requests. Also covers WAVE_FORMAT_EXTENSIBLE float32, which is only accepted because the real format code is read out of the SubFormat GUID; without that it reads as 0xfffe and is now correctly rejected rather than silently decoding to nothing. |
||
|
|
de4199500b |
chore(deps): bump docker/login-action from 4.5.1 to 4.6.0 (#329)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e1a5ae3889 |
chore(deps): bump docker/login-action from 4.4.0 to 4.5.1 (#327)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.1. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.5.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
a3ccc8c439 |
chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 (#328)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v7...v7.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
2e2ad8dd4f |
chore(deps): bump softprops/action-gh-release from 3.0.1 to 3.0.2 (#323)
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / js-syntax (push) Has been cancelled
CI / sast-bandit (push) Has been cancelled
CI / deps-audit (push) Has been cancelled
CI / trivy (push) Has been cancelled
Docker Publish / build-and-push (push) Has been cancelled
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/718ea10b132b3b2eba29c1007bb80653f286566b...3d0d9888cb7fd7b750713d6e236d1fcb99157228) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
abc09e4894 |
fix(ci): docker-publish never fires on prereleased -- restore published (#266)
Publishing v0.8.0-alpha.7 (a draft prerelease -> published) never triggered Docker Publish: CI/Linux/Windows/macOS releases all still listen for `published` and fired correctly, but docker-publish.yml (changed in #259) only listened for [prereleased, released] -- and `prereleased` did not fire for this publish. Result: ghcr.io/stemdeckapp/stemdeck:0.8.0-alpha.7 was never pushed, while the Unraid CA template already points at that (nonexistent) tag. Fix: trigger on [published, released] -- published is the reliable trigger every other release workflow already relies on; released is kept for promoting an already-published prerelease to Latest via the release-label edit. The :latest condition now also covers a plain non-prerelease `published` (not just `released`), so a first-time stable release still gets :latest. Also add a workflow_dispatch `version` input, so a specific tag can be (re)pushed by hand to recover from a missed trigger without re-touching the release (which would needlessly re-run the OS build workflows). |
||
|
|
a857f60fbf |
feat(player): stream stems on desktop via the 5s-chunk engine (#261) (#264)
* feat(player): stream stems on desktop via the 5s-chunk engine (#261) Desktop previously decoded every stem in full before playback (~420 MB / slow preload). The Range-based chunked engine (chunkedAudioEngine.js) already streams glitch-free on mobile: 5s HTTP-Range windows scheduled on AudioBufferSourceNodes, first audio after ~1 chunk, ~28 MB RAM, no length cap. This promotes it to the desktop player as the default, closing its three feature stubs so there's no regression vs the full-decode engine: - chunkedAudioEngine: implement setLoop (scheduler jumps to loop.start on crossing loop.end, and caps lookahead at loop.end); add a per-stem AnalyserNode (gain -> analyser -> master) + getAnalyser for live VU. - player.js: engineMode() selects chunked by default; "fulldecode" and "0" (legacy <audio>) remain opt-in via the stemdeck.audioEngine flag. The RAM cap now only gates the full-decode engine. On the chunked path, drive lane mini-waves + the energy baseline from peaks.json and VU meters from the engine's live analysers (overview waveforms already come from peaks.json). - mixer.js: renderRealMiniWaveFromPeaks (peaks-based lane mini-wave). The original "buffering issue" was the N-<audio>-element multitrack path (HTTP/1.1 6-connection-cap underruns on Safari/WKWebView); the chunked engine avoids it by construction. Full-decode stays as a fallback. * fix(player): harden chunked streaming (loop cache pinning, retry, peaks fallback) Self-review of the chunked-engine promotion found four gaps: - Loop-start chunk was evicted as playback advanced, so every loop pass paid a refetch gap. Pin it against both eviction sites while a loop is active, and warm it as the playhead approaches loop.end (deduped by the chunk cache). - A transient all-stems fetch failure cached an empty chunk forever, leaving playback permanently silent past that point. Drop empty results so the scheduler retries. - Loop jumps scheduled with the cold-start 50 ms lead. Cached (sync) starts now use 10 ms, making loop wraps near-seamless; the async path keeps 50 ms. - Legacy jobs without peaks.json lost all waveform visuals on the streaming path. The chunked branch now falls back to the full-decode engine in that case (honoring the backend's documented "degrades to client-side decode" contract), unless the track exceeds the decode RAM cap - then it keeps streaming audio with placeholder waveforms. * ci(deps-audit): ignore torch PYSEC-2026-2286 (torch.load ACE, no adoptable fix) pip-audit newly flags torch 2.6.0 for PYSEC-2026-2286 (torch.load weights_only deserialization -> arbitrary code execution, HIGH; fixed in 2.10.0). The exploit requires an attacker-controlled .pth checkpoint. StemDeck never calls torch.load on untrusted input: demucs loads only its official model weights from the trusted torch-hub source, and users submit audio, not checkpoints. torch is pinned <2.7 (torchaudio 2.7+ dropped the writer demucs needs), so 2.10.0 is not adoptable yet. Documented alongside the existing ignored torch advisories. |
||
|
|
a67b186a24 |
ci(docker): push :latest when a prerelease is promoted to Latest (#259)
Listen to release `prereleased` and `released` actions instead of `published`: - prereleased (prerelease published) -> version tag only (e.g. 0.8.0-alpha.6) - released (stable publish, or a prerelease promoted to a full/Latest release) -> version tag + :latest - push to main / manual dispatch -> :edge (unchanged) Using these two actions instead of `published` also avoids a double run on a stable publish (which fires both published and released). |
||
|
|
1b0d689eef |
fix(ci): make manual/edge image version PEP 440-valid (#254)
git describe --long yields <tag>-<N>-g<sha>, which hatchling rejects as an invalid version (SETUPTOOLS_SCM_PRETEND_VERSION). Rewrite the trailing -N-gSHA into a +N.gSHA local segment so the docker build succeeds. |
||
|
|
ce86e8ad57 |
feat(unraid): publish container to GHCR and add Community Applications app (#253)
* feat(unraid): publish container to GHCR and add Community Applications template - add docker-publish workflow: build build/Dockerfile and push ghcr.io/stemdeckapp/stemdeck on release + manual dispatch (linux/amd64) - add templates/stemdeck.xml: Unraid Docker template (port 8000, /app/jobs + /cache volumes, persistent library default, optional NVIDIA runtime vars) - add ca_profile.xml at repo root for the CA submission scan - document the GHCR image and Unraid install in README The published image keeps the default Linux x86_64 (CUDA) torch wheel, so a single image runs on CPU by default and uses the GPU when started with --runtime=nvidia; _detect_device() auto-selects CUDA. * ci(unraid): derive manual-dispatch version from git instead of 0.0.0 Drop the workflow_dispatch version input and compute it with git describe (hatch-vcs style) so manual builds carry a real dev version. Fetch full history + tags on checkout so git describe resolves. * ci(unraid): publish a rolling :edge image on merge to main Add a push trigger on main so every merge builds and pushes ghcr.io/stemdeckapp/stemdeck:edge. :edge never moves :latest, which stays reserved for stable releases. * chore(unraid): point template at :edge until a stable release exists * docs(unraid): document edge/latest/version image tags and use :edge in the run example * chore: default run.sh PORT to 8000 to match the container/Unraid port * chore: default advertised port to 8000 across backend and desktop Align DEFAULT_PORT (app/core/settings.py) and the desktop launcher's configured_port() fallback (desktop/src-tauri/src/main.rs) from 8080 to 8000 so every path -- container, run.sh, and desktop -- shares one default. Update the settings comment and the port-default test accordingly. |
||
|
|
7a15a395e1 | ci: remove append_body from OS release workflows - artifact sections now static | ||
|
|
8d72f0d684 |
chore(deps): bump actions/checkout from 6 to 7 (#214)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c95b42ae48 |
chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#215)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/b4309332981a82ec1c5618f44dd2e27cc8bfbfda...718ea10b132b3b2eba29c1007bb80653f286566b) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
a45badf3b3 |
fix(ci): target the macOS runner's default macOS/ARM64 labels (#227)
The self-hosted runner advertises self-hosted/macOS/ARM64 (GitHub's default macOS labels), but the workflow required a custom 'osx' label that the runner no longer carries, so every macOS Release since alpha.15 sat queued with no matching runner. Match the default labels, consistent with the windows/linux release workflows. Co-authored-by: Thales <> |
||
|
|
ab832e3342 |
fix(ci): skip apt when deps present; the runner has no passwordless sudo (#224)
Root cause of the Linux apt hang: the wsl2 self-hosted runner does not have passwordless sudo, so 'sudo apt-get' blocks forever at the password prompt. The previous 'sudo timeout ... apt-get' guard did nothing because sudo prompts BEFORE the inner timeout can start. Since the build deps are already installed on the persistent runner, check each package with dpkg (no sudo) and skip apt entirely when all are present. Only touch apt if something is genuinely missing, using 'sudo -n' so it fails fast with a clear message instead of hanging at a prompt. Co-authored-by: Thales <> |
||
|
|
8210e7f22d |
fix(ci): make Linux apt step best-effort and hard-capped (#223)
The Linux release job hung for 30+ min on 'install build dependencies': apt-get stalled on the persistent wsl2 runner (likely a dpkg lock held by unattended-upgrades). The packages are already installed on that runner from prior runs, so the install itself is a no-op -- the hang is in apt-get update / lock acquisition. Bound the apt commands with and DPkg::Lock::Timeout so a stuck lock cannot hang the release, set DEBIAN_FRONTEND=noninteractive to avoid debconf prompts, treat apt as best-effort, then verify webkit2gtk-4.1 is actually present (fail loudly only if genuinely missing). Co-authored-by: Thales <> |
||
|
|
44336a55d5 |
fix(ci): source release tag from github.ref_name on all self-hosted runners (#221)
* fix(ci): source release tag from github.ref_name on Windows The Windows release job failed at 'write version files' with 'GITHUB_REF_NAME is not set' on the org-level self-hosted win runner, while the Linux runner saw the variable fine. $env:GITHUB_REF_NAME is only injected by Actions runner >= 2.290, so an older self-hosted runner leaves it empty. Source the tag from the github.ref_name context (evaluated by Actions before the step runs) via a job-level REF_NAME env var instead, so the build no longer depends on the runner version. Replaces all three $env:GITHUB_REF_NAME usages (version step + both build steps). * fix(ci): source release tag from github.ref_name on Linux and macOS too The Linux release job failed at 'write version files' with the same empty-GITHUB_REF_NAME cause as Windows: the wsl2 self-hosted runner is also older than 2.290. macOS uses the same pattern and the same class of runner, so fix all three release workflows consistently to read github.ref_name from context via a REF_NAME env var. --------- Co-authored-by: Thales <> |
||
|
|
8131900d0a |
feat: Linux portable builds (CPU + NVIDIA) and release workflow (#220)
* feat: add CPU-only Linux portable build and release workflow
Adds a Linux .tar.gz portable package mirroring the existing Windows/macOS
build paths. Bundles a python-build-standalone runtime (CPU torch + demucs)
plus the Tauri binary so users extract and run ./StemDeck.
- scripts/linux/make-portable.sh: stages PBS Python, force-installs CPU-only
torch, builds the Tauri binary, and produces StemDeck-Linux-x64.tar.gz with
the backend/app + python/ layout find_repo_root resolves at runtime.
- .github/workflows/linux-release.yml: builds on hosted ubuntu-latest on
release publish; installs Tauri v2 apt deps + uv, ClamAV-scans, uploads.
- packaging/linux/{README-LINUX,THIRD_PARTY_NOTICES}.txt: extract-and-run
instructions noting ffmpeg + WebKitGTK are system (apt) prerequisites.
FFmpeg is not bundled: the Linux shell expects ffmpeg on PATH. NVIDIA/CUDA
and AppImage variants are intentionally deferred to later phases.
* fix: don't set PYTHONHOME on Linux (breaks PBS stdlib resolution)
The Linux backend failed to start with 'ModuleNotFoundError: No module
named encodings'. PYTHONHOME was being set to python/bin instead of the
prefix python/, so CPython looked for its stdlib under python/bin/lib and
could not boot.
Linux bundles python-build-standalone exactly like macOS, which detects
its own prefix by walking up from bin/ and must NOT have PYTHONHOME set.
The two PYTHONHOME sites were gated #[cfg(not(target_os = "macos"))],
wrongly including Linux alongside Windows. Only Windows -- whose portable
venv keeps the stdlib under base/Lib -- needs PYTHONHOME, so gate both
sites (start_backend and python_stdlib_ok) to #[cfg(windows)].
This also fixes the latent inconsistency where probe_runtime reported
Python ready (python_stdlib_ok set PYTHONHOME=python, the correct prefix)
while start_backend set PYTHONHOME=python/bin and failed.
* feat: add NVIDIA/CUDA Linux portable variant
Adds a second Linux package, StemDeck-Linux-x64.NVIDIA.tar.gz, with
CUDA-enabled torch baked in (mirrors the Windows NVIDIA variant).
- make-portable.sh: CPU_ONLY toggle (default 1). CPU_ONLY=0 keeps the
project's default torch wheel, which on Linux x86_64 is the CUDA build,
and omits the cpu-only marker so the desktop shell detects the GPU and
uses CUDA at runtime. No app-side changes needed -- the CUDA detection/
install path in main.rs is already cfg(not(macos)) and covers Linux.
- linux-release.yml: builds both variants in one job. CPU first (full Tauri
build), then NVIDIA with SKIP_TAURI_BUILD=1 reusing the same binary. Adds
a free-disk-space step (CUDA bundle is several GB) and drops each
uncompressed stage after taring to stay within the hosted runner's disk.
- README-LINUX.txt: documents both variants and the NVIDIA driver
prerequisite (nvidia-smi must work; CUDA runtime is bundled, no toolkit
install needed; falls back to CPU when no GPU).
* ci: run Linux release on self-hosted linux/x64 runner
Targets the org's self-hosted wsl2 runner ([self-hosted, linux, x64])
instead of hosted ubuntu-latest, matching the Windows/macOS release
jobs. Drops the free-disk-space step: it was a hosted-runner workaround
and would needlessly rm system directories on a persistent self-hosted
box (WSL2's virtual disk has ample room for the CUDA bundle).
* ci: add workflow_dispatch test build for Linux release
Lets you run the full two-variant build + ClamAV scan on the self-hosted
runner without publishing a release, to validate the runner toolchain and
the CUDA build. Resolves the version from a manual input (default 0.0.0,
must be valid PEP 440) instead of the branch ref, and skips the upload
step on non-release events.
---------
Co-authored-by: Thales <>
|
||
|
|
66425fe108 | fix: use powershell shell on Windows runner (pwsh not available) (#212) | ||
|
|
d6280a3efb | ci: trigger release workflows only on release published (#208) | ||
|
|
bca5a8961b |
chore(deps): bump actions/checkout from 4 to 6 (#206)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
3baaa0fa07 |
ci: migrate from Woodpecker to GitHub Actions (#205)
* ci: add GitHub Actions CI workflow (replaces Woodpecker ci.yml) Parallel jobs: lint, test, js-syntax, sast-bandit, deps-audit, trivy. Trivy now uses aquasecurity/trivy-action instead of the container image. * fix: upgrade yt-dlp, starlette, python-multipart; ignore new torch CVEs - yt-dlp 2026.3.17 -> 2026.6.9 (fixes CVE-2026-50023, CVE-2026-50574, GHSA-69qj-pvh9-c5wg) - starlette 1.0.0 -> 1.3.1 (fixes CVE-2026-48818, CVE-2026-54283) - python-multipart 0.0.27 -> 0.0.32 (fixes CVE-2026-53539) - Add CVE-2025-2148/2149/2998/2999/3000/3001 to deps-audit ignore list: torch is pinned below 2.7 due to torchaudio/demucs compat; these CVEs are in ops StemDeck does not invoke. * ci: add macOS and Windows release workflows + Dependabot config - macos-release.yml: builds arm64 and x64 DMGs on self-hosted macOS runner, inspects artifacts, uploads to GitHub release via softprops/action-gh-release - windows-release.yml: builds NVIDIA and CPU portable ZIPs on self-hosted Windows runner, scans with ClamAV, uploads to GitHub release - dependabot.yml: weekly action SHA bumps for all workflows - Both release workflows: permissions locked to read-only at workflow level, contents:write only on the job; concurrency guard; 120/90 min timeouts; workspace cleanup; all actions pinned to SHA * ci: use osx runner label for macOS release workflow |