Author SHA1 Message Date
KeelrLeek 972dd103cb release: 1.8.2-alpha
release / windows (push) Canceled after 0s
2026-09-10 07:19:18 -05:00
KeelrLeek 12fff9f6ad setup, updater: the installer window was a dev build with nothing inside it, and latest never matched a prerelease
Tauri decides dev-vs-production from the `custom-protocol` feature, not from
--release, and both workflows build the setup exe with a plain `cargo build`.
So cfg(dev) stayed on, the frontend was never embedded, and the window a
tester opened loaded devUrl: "localhost refused to connect" on a blank
800x580. The feature is a default of the crate now, with a build.rs guard
that fails a release build without it, and build.rs tracks setup/dist so a
changed window cannot be left out of the binary it is supposed to be in.

Behind that, the next wall: /releases/latest answers only with full
releases, and every release here is an alpha, so it 404s. The setup app read
that as a refused key and the app's Stable channel read it as a token that
cannot see the repository — both have been broken since the first alpha.
Each now lists /releases and takes the newest that is neither a draft nor
the rolling nightly, which the list really does put first.

And check_saved_key threw the saved key away on any failure at all, a
dropped connection included. The entry it deletes is the app's own updater
token, so only a refusal clears it now.

The release notes name the file to hand a tester: six assets on the page and
nothing saying which, while the bare NSIS installer is the biggest of them
and installs with no key gate and no signature check.
2026-09-10 07:19:15 -05:00
12 changed files with 204 additions and 45 deletions

No files matched your search

+20 -1
View File
@@ -122,6 +122,9 @@ jobs:
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
ls -la "SoulStream-Setup-$VERSION.exe"
# Which of the files on the page a tester runs, said on the page: the
# bare NSIS installer reads like the download and installs with no key
# gate and no signature check.
- name: Publish SoulStream Setup
shell: bash
env:
@@ -129,4 +132,20 @@ jobs:
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
TAG: nightly
VERSION: ${{ steps.stamp.outputs.version }}
run: gh release upload "$TAG" "SoulStream-Setup-$VERSION.exe" --repo "$TARGET" --clobber
SHA: ${{ github.sha }}
run: |
gh release upload "$TAG" "SoulStream-Setup-$VERSION.exe" --repo "$TARGET" --clobber
{
echo "### Download \`SoulStream-Setup-$VERSION.exe\`"
echo
echo "It asks for your alpha key, fetches this build with it, checks the signature and installs — no admin prompt."
echo
echo "The other files are the ones Setup uses:"
echo
echo "- \`SoulStream_${VERSION}_x64-setup.exe\` — the bare installer Setup downloads. Running it yourself skips the key and the signature check."
echo "- \`SoulStream_${VERSION}_x64-setup.exe.sig\` — its signature."
echo "- \`latest.json\` — the manifest the app's Nightly channel reads."
echo
echo "Built from main at $SHA. Nightlies can break."
} > notes.md
gh release edit "$TAG" --repo "$TARGET" --notes-file notes.md
+20 -1
View File
@@ -105,6 +105,10 @@ jobs:
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
ls -la "SoulStream-Setup-$VERSION.exe"
# Six files on the page and nothing saying which one a tester runs: the
# NSIS installer is the biggest and reads like the download, and running
# it puts the app on with no key gate and no signature check. The notes
# go on last, once the file they point at is actually up.
- name: Publish SoulStream Setup
shell: bash
env:
@@ -112,4 +116,19 @@ jobs:
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
TAG: v${{ steps.version.outputs.version }}
VERSION: ${{ steps.version.outputs.version }}
run: gh release upload "$TAG" "SoulStream-Setup-$VERSION.exe" --repo "$TARGET" --clobber
run: |
gh release upload "$TAG" "SoulStream-Setup-$VERSION.exe" --repo "$TARGET" --clobber
{
echo "### Download \`SoulStream-Setup-$VERSION.exe\`"
echo
echo "It asks for your alpha key, fetches this release with it, checks the signature and installs — no admin prompt."
echo
echo "The other files are the ones Setup uses:"
echo
echo "- \`SoulStream_${VERSION}_x64-setup.exe\` — the bare installer Setup downloads. Running it yourself skips the key and the signature check."
echo "- \`SoulStream_${VERSION}_x64-setup.exe.sig\` — its signature."
echo "- \`latest.json\` — the manifest the in-app updater reads."
echo
echo "See the commits since the last release."
} > notes.md
gh release edit "$TAG" --repo "$TARGET" --notes-file notes.md
Generated
+11 -11
View File
@@ -160,7 +160,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "app"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"audio-formats",
"base64 0.22.1",
@@ -429,7 +429,7 @@ dependencies = [
[[package]]
name = "audio-formats"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
[[package]]
name = "autocfg"
@@ -1722,7 +1722,7 @@ dependencies = [
[[package]]
name = "freenet-adapter"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"bs58",
"freenet-stdlib",
@@ -2896,7 +2896,7 @@ dependencies = [
[[package]]
name = "library"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"audio-formats",
"log",
@@ -3034,7 +3034,7 @@ dependencies = [
[[package]]
name = "metadata"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"futures",
"httpdate",
@@ -3701,7 +3701,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "persona-state"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"bincode",
"blake3",
@@ -3799,7 +3799,7 @@ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "player"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"audio-formats",
"env_logger",
@@ -4960,7 +4960,7 @@ dependencies = [
[[package]]
name = "soulseek-client"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"audio-formats",
"bytes",
@@ -4975,7 +4975,7 @@ dependencies = [
[[package]]
name = "soulseek-proto"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"bytes",
"flate2",
@@ -4985,7 +4985,7 @@ dependencies = [
[[package]]
name = "soulstream-setup"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"base64 0.21.7",
"dirs 5.0.1",
@@ -5392,7 +5392,7 @@ dependencies = [
[[package]]
name = "tagger"
version = "1.8.1-alpha"
version = "1.8.2-alpha"
dependencies = [
"lofty",
"log",
+1 -1
View File
@@ -15,7 +15,7 @@ members = [
]
[workspace.package]
version = "1.8.1-alpha"
version = "1.8.2-alpha"
edition = "2021"
rust-version = "1.77.2"
authors = ["Trifecta Solutions"]
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "soulstream",
"version": "1.8.1-alpha",
"version": "1.8.2-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "soulstream",
"version": "1.8.1-alpha",
"version": "1.8.2-alpha",
"license": "ISC",
"dependencies": {
"@tauri-apps/api": "^2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "soulstream",
"version": "1.8.1-alpha",
"version": "1.8.2-alpha",
"type": "module",
"description": "SoulStream — music player, downloader and streamer for the Soulseek network",
"scripts": {
+11
View File
@@ -11,6 +11,17 @@ rust-version.workspace = true
name = "SoulStream-Setup"
path = "src/main.rs"
# Tauri serves the frontend it embedded at compile time only with this on;
# without it the window loads `devUrl` instead and a tester gets a blank
# window saying "localhost refused to connect". The workflows build this
# crate with plain `cargo build` — no Tauri CLI to turn it on for them — so
# it is a DEFAULT feature here: every build that does not say otherwise is a
# shippable one. `--no-default-features` is the build that talks to the vite
# dev server on 1425.
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
serde_json.workspace = true
+12
View File
@@ -4,9 +4,21 @@
use std::path::{Path, PathBuf};
fn main() {
// The frontend is embedded only when tauri's `custom-protocol` feature is
// on; without it the window points at devUrl and the exe we hand a tester
// shows "localhost refused to connect". It is a default feature, so this
// only fires when something turned the defaults off — and it fires at
// build time rather than on the tester's machine.
if tauri_build::is_dev() && std::env::var("PROFILE").as_deref() == Ok("release") {
panic!("SoulStream Setup: a release build needs the `custom-protocol` feature (on by default); without it the window loads devUrl and is blank");
}
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let conf = manifest.join("../../src-tauri/tauri.conf.json");
println!("cargo:rerun-if-changed={}", conf.display());
// tauri-build tracks the config, not the frontend the exe embeds: without
// this, a changed Setup.svelte rebuilds `setup/dist` and cargo hands back
// yesterday's binary, still carrying yesterday's window.
println!("cargo:rerun-if-changed={}", manifest.join("../dist").display());
let text = std::fs::read_to_string(&conf).expect("read src-tauri/tauri.conf.json");
let json: serde_json::Value = serde_json::from_str(&text).expect("parse tauri.conf.json");
let pubkey = json["plugins"]["updater"]["pubkey"]
+69 -10
View File
@@ -26,6 +26,9 @@ use serde::Serialize;
use tauri::{AppHandle, Emitter};
const REPO: &str = "KeelrLeek/SoulStream";
/// The rolling prerelease `.github/workflows/nightly.yml` replaces daily —
/// never the one Setup installs.
const NIGHTLY_TAG: &str = "nightly";
const APP_DATA_DIRNAME: &str = "com.soulstream.app";
const KEYRING_SERVICE: &str = "com.soulstream.app";
const KEYRING_TOKEN_USER: &str = "updater-token";
@@ -163,6 +166,44 @@ struct Release {
sig_url: Option<String>,
}
/// Why a release lookup failed, and whether it was the KEY that GitHub
/// refused. Only a refusal is a reason to throw the saved key away — a
/// laptop between wifi networks is not, and the entry thrown away is the
/// one the app reads for its own updates.
struct Failure {
message: String,
key_refused: bool,
}
impl Failure {
fn plain(message: impl Into<String>) -> Self {
Self { message: message.into(), key_refused: false }
}
fn refused(message: impl Into<String>) -> Self {
Self { message: message.into(), key_refused: true }
}
}
impl From<Failure> for String {
fn from(f: Failure) -> Self {
f.message
}
}
/// A bare message is never a refusal — a release missing its installer says
/// nothing about the key that read it.
impl From<&str> for Failure {
fn from(m: &str) -> Self {
Failure::plain(m)
}
}
impl From<String> for Failure {
fn from(m: String) -> Self {
Failure::plain(m)
}
}
fn client() -> Result<reqwest::Client, String> {
reqwest::Client::builder()
.timeout(Duration::from_secs(60))
@@ -171,23 +212,37 @@ fn client() -> Result<reqwest::Client, String> {
.map_err(|e| e.to_string())
}
async fn fetch_release(token: &str) -> Result<Release, String> {
async fn fetch_release(token: &str) -> Result<Release, Failure> {
// Not `/releases/latest`: that endpoint answers with the newest FULL
// release and every SoulStream release so far is a prerelease, so it
// 404s and a perfectly good key reads back as a bad one. The list comes
// newest first — take the first that is neither a draft nor the rolling
// nightly, which is not what a tester is here to install.
let resp = client()?
.get(format!("https://api.github.com/repos/{REPO}/releases/latest"))
.get(format!("https://api.github.com/repos/{REPO}/releases?per_page=30"))
.bearer_auth(token)
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.send()
.await
.map_err(|e| format!("Could not reach GitHub — is this computer online? ({e})"))?;
.map_err(|e| Failure::plain(format!("Could not reach GitHub — is this computer online? ({e})")))?;
match resp.status().as_u16() {
200 => {}
401 => return Err("That key was refused.".into()),
403 => return Err("That key cannot read the SoulStream repository.".into()),
404 => return Err("No release found — or that key cannot see the repository.".into()),
s => return Err(format!("GitHub answered {s}.")),
401 => return Err(Failure::refused("That key was refused.")),
403 => return Err(Failure::refused("That key cannot read the SoulStream repository.")),
// A private repository answers 404 to a key that cannot see it.
404 => return Err(Failure::refused("No release found — or that key cannot see the repository.")),
s => return Err(Failure::plain(format!("GitHub answered {s}."))),
}
let v: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?;
let list: Vec<serde_json::Value> =
resp.json().await.map_err(|e| Failure::plain(e.to_string()))?;
// The key worked; there is simply nothing published yet. Not its fault.
let v = list
.into_iter()
.find(|r| {
r["draft"].as_bool() != Some(true) && r["tag_name"].as_str() != Some(NIGHTLY_TAG)
})
.ok_or_else(|| Failure::plain("That key works, but the repository has no release to install yet."))?;
let tag = v["tag_name"].as_str().unwrap_or("");
let assets = v["assets"].as_array().cloned().unwrap_or_default();
let find = |suffix: &str| {
@@ -339,12 +394,15 @@ async fn check_saved_key() -> Result<Option<Release>, String> {
let Some(t) = saved_key() else { return Ok(None) };
match fetch_release(&t).await {
Ok(r) => Ok(Some(r)),
Err(e) => {
Err(e) if e.key_refused => {
// The same Credential Manager entry the app reads for its own
// updates, so it goes only when GitHub actually turned it down.
if let Some(entry) = keyring_entry() {
let _ = entry.delete_credential();
}
Err(format!("The key saved on this computer no longer works — {e}"))
Err(format!("The key saved on this computer no longer works — {}", e.message))
}
Err(e) => Err(e.message),
}
}
@@ -471,6 +529,7 @@ fn main() {
quit
])
.run({
#[cfg_attr(not(debug_assertions), allow(unused_mut))]
let mut context = tauri::generate_context!();
// Driving the window from a probe, debug builds only.
#[cfg(debug_assertions)]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "SoulStream Setup",
"version": "1.8.1-alpha",
"version": "1.8.2-alpha",
"identifier": "com.soulstream.setup",
"build": {
"beforeDevCommand": "npx vite --config vite.setup.config.js",
+55 -16
View File
@@ -11,8 +11,12 @@
//! private release cannot be read without a token. With a read-only token
//! saved in the Credential Manager (Settings → Updates → Access token), a
//! GitHub source is resolved through the API instead of the browser URLs:
//! `GET /repos/{o}/{r}/releases/latest` (or `/tags/{tag}`) finds the
//! `latest.json` asset, the plugin fetches THAT asset URL with
//! `GET /repos/{o}/{r}/releases/tags/{tag}` for a pinned tag, and for
//! `latest` a `GET /repos/{o}/{r}/releases` whose first entry that is
//! neither a draft nor the rolling `nightly` wins. NOT `/releases/latest`:
//! that answers only with full releases, and every release here is an
//! alpha, so it 404s. Either way the `latest.json` asset is found, the
//! plugin fetches THAT asset URL with
//! `Authorization: Bearer` + `Accept: application/octet-stream`, and the
//! manifest's own download URLs are API asset URLs too — the workflows
//! rewrite them after publishing — so the installer downloads with the same
@@ -37,6 +41,8 @@ pub const DEFAULT_STABLE: &str =
"https://github.com/KeelrLeek/SoulStream/releases/latest/download/latest.json";
pub const DEFAULT_NIGHTLY: &str =
"https://github.com/KeelrLeek/SoulStream/releases/download/nightly/latest.json";
/// The tag `nightly.yml` rewrites daily. Never what `latest` should resolve to.
const NIGHTLY_TAG: &str = "nightly";
/// First automatic check after launch, then the interval between checks.
const FIRST_CHECK: Duration = Duration::from_secs(25);
@@ -199,22 +205,15 @@ fn announce(app: &AppHandle) {
let _ = app.emit("updater:status", status(app));
}
/// Ask the GitHub API where a private release keeps its `latest.json`.
async fn resolve_github_asset(
src: &GitHubSource,
/// One authenticated GitHub API GET, its failures already in words.
async fn github_json(
client: &reqwest::Client,
url: &str,
token: &str,
user_agent: &str,
) -> Result<Url, String> {
let api = format!(
"https://api.github.com/repos/{}/{}/releases/{}",
src.owner, src.repo, src.release
);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
) -> Result<serde_json::Value, String> {
let resp = client
.get(&api)
.get(url)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
.header(reqwest::header::USER_AGENT, user_agent)
@@ -229,7 +228,47 @@ async fn resolve_github_asset(
404 => return Err("no release found — or the token cannot see the repository".into()),
s => return Err(format!("GitHub answered {s}")),
}
let body: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?;
resp.json().await.map_err(|e| e.to_string())
}
/// Ask the GitHub API where a private release keeps its `latest.json`.
async fn resolve_github_asset(
src: &GitHubSource,
token: &str,
user_agent: &str,
) -> Result<Url, String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
let repo = format!("https://api.github.com/repos/{}/{}", src.owner, src.repo);
let body = if src.release == "latest" {
// `/releases/latest` answers with the newest FULL release, and every
// SoulStream release so far is a prerelease — so it 404s and Stable
// reports the token cannot see the repository. The list comes newest
// first: take the first that is neither a draft nor the rolling
// nightly, which the Nightly channel asks for by tag instead.
let list = github_json(&client, &format!("{repo}/releases?per_page=30"), token, user_agent)
.await?;
list.as_array()
.and_then(|rs| {
rs.iter()
.find(|r| {
r["draft"].as_bool() != Some(true)
&& r["tag_name"].as_str() != Some(NIGHTLY_TAG)
})
.cloned()
})
.ok_or("no release found — or the token cannot see the repository")?
} else {
github_json(
&client,
&format!("{repo}/releases/{}", src.release),
token,
user_agent,
)
.await?
};
let assets = body["assets"].as_array().cloned().unwrap_or_default();
let asset = assets
.iter()
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "SoulStream",
"version": "1.8.1-alpha",
"version": "1.8.2-alpha",
"identifier": "com.soulstream.app",
"build": {
"beforeDevCommand": "npm run dev",