Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972dd103cb | ||
|
|
12fff9f6ad |
No files matched your search
@@ -122,6 +122,9 @@ jobs:
|
|||||||
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
|
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
|
||||||
ls -la "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
|
- name: Publish SoulStream Setup
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -129,4 +132,20 @@ jobs:
|
|||||||
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
|
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
|
||||||
TAG: nightly
|
TAG: nightly
|
||||||
VERSION: ${{ steps.stamp.outputs.version }}
|
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
|
||||||
@@ -105,6 +105,10 @@ jobs:
|
|||||||
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
|
cp target/release/SoulStream-Setup.exe "SoulStream-Setup-$VERSION.exe"
|
||||||
ls -la "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
|
- name: Publish SoulStream Setup
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -112,4 +116,19 @@ jobs:
|
|||||||
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
|
TARGET: ${{ vars.RELEASES_OWNER || github.repository_owner }}/${{ vars.RELEASES_REPO || github.event.repository.name }}
|
||||||
TAG: v${{ steps.version.outputs.version }}
|
TAG: v${{ steps.version.outputs.version }}
|
||||||
VERSION: ${{ 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
@@ -160,7 +160,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audio-formats",
|
"audio-formats",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -429,7 +429,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "audio-formats"
|
name = "audio-formats"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -1722,7 +1722,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freenet-adapter"
|
name = "freenet-adapter"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"freenet-stdlib",
|
"freenet-stdlib",
|
||||||
@@ -2896,7 +2896,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "library"
|
name = "library"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audio-formats",
|
"audio-formats",
|
||||||
"log",
|
"log",
|
||||||
@@ -3034,7 +3034,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "metadata"
|
name = "metadata"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
@@ -3701,7 +3701,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "persona-state"
|
name = "persona-state"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"blake3",
|
"blake3",
|
||||||
@@ -3799,7 +3799,7 @@ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "player"
|
name = "player"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audio-formats",
|
"audio-formats",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -4960,7 +4960,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soulseek-client"
|
name = "soulseek-client"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audio-formats",
|
"audio-formats",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4975,7 +4975,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soulseek-proto"
|
name = "soulseek-proto"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -4985,7 +4985,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soulstream-setup"
|
name = "soulstream-setup"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -5392,7 +5392,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tagger"
|
name = "tagger"
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lofty",
|
"lofty",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.8.1-alpha"
|
version = "1.8.2-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.2"
|
rust-version = "1.77.2"
|
||||||
authors = ["Trifecta Solutions"]
|
authors = ["Trifecta Solutions"]
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "soulstream",
|
"name": "soulstream",
|
||||||
"version": "1.8.1-alpha",
|
"version": "1.8.2-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "soulstream",
|
"name": "soulstream",
|
||||||
"version": "1.8.1-alpha",
|
"version": "1.8.2-alpha",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2"
|
"@tauri-apps/api": "^2"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "soulstream",
|
"name": "soulstream",
|
||||||
"version": "1.8.1-alpha",
|
"version": "1.8.2-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "SoulStream — music player, downloader and streamer for the Soulseek network",
|
"description": "SoulStream — music player, downloader and streamer for the Soulseek network",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ rust-version.workspace = true
|
|||||||
name = "SoulStream-Setup"
|
name = "SoulStream-Setup"
|
||||||
path = "src/main.rs"
|
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]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -4,9 +4,21 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn main() {
|
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 manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let conf = manifest.join("../../src-tauri/tauri.conf.json");
|
let conf = manifest.join("../../src-tauri/tauri.conf.json");
|
||||||
println!("cargo:rerun-if-changed={}", conf.display());
|
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 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 json: serde_json::Value = serde_json::from_str(&text).expect("parse tauri.conf.json");
|
||||||
let pubkey = json["plugins"]["updater"]["pubkey"]
|
let pubkey = json["plugins"]["updater"]["pubkey"]
|
||||||
|
|||||||
+69
-10
@@ -26,6 +26,9 @@ use serde::Serialize;
|
|||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
const REPO: &str = "KeelrLeek/SoulStream";
|
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 APP_DATA_DIRNAME: &str = "com.soulstream.app";
|
||||||
const KEYRING_SERVICE: &str = "com.soulstream.app";
|
const KEYRING_SERVICE: &str = "com.soulstream.app";
|
||||||
const KEYRING_TOKEN_USER: &str = "updater-token";
|
const KEYRING_TOKEN_USER: &str = "updater-token";
|
||||||
@@ -163,6 +166,44 @@ struct Release {
|
|||||||
sig_url: Option<String>,
|
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> {
|
fn client() -> Result<reqwest::Client, String> {
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(60))
|
.timeout(Duration::from_secs(60))
|
||||||
@@ -171,23 +212,37 @@ fn client() -> Result<reqwest::Client, String> {
|
|||||||
.map_err(|e| e.to_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()?
|
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)
|
.bearer_auth(token)
|
||||||
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
||||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.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() {
|
match resp.status().as_u16() {
|
||||||
200 => {}
|
200 => {}
|
||||||
401 => return Err("That key was refused.".into()),
|
401 => return Err(Failure::refused("That key was refused.")),
|
||||||
403 => return Err("That key cannot read the SoulStream repository.".into()),
|
403 => return Err(Failure::refused("That key cannot read the SoulStream repository.")),
|
||||||
404 => return Err("No release found — or that key cannot see the repository.".into()),
|
// A private repository answers 404 to a key that cannot see it.
|
||||||
s => return Err(format!("GitHub answered {s}.")),
|
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 tag = v["tag_name"].as_str().unwrap_or("");
|
||||||
let assets = v["assets"].as_array().cloned().unwrap_or_default();
|
let assets = v["assets"].as_array().cloned().unwrap_or_default();
|
||||||
let find = |suffix: &str| {
|
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) };
|
let Some(t) = saved_key() else { return Ok(None) };
|
||||||
match fetch_release(&t).await {
|
match fetch_release(&t).await {
|
||||||
Ok(r) => Ok(Some(r)),
|
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() {
|
if let Some(entry) = keyring_entry() {
|
||||||
let _ = entry.delete_credential();
|
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
|
quit
|
||||||
])
|
])
|
||||||
.run({
|
.run({
|
||||||
|
#[cfg_attr(not(debug_assertions), allow(unused_mut))]
|
||||||
let mut context = tauri::generate_context!();
|
let mut context = tauri::generate_context!();
|
||||||
// Driving the window from a probe, debug builds only.
|
// Driving the window from a probe, debug builds only.
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "SoulStream Setup",
|
"productName": "SoulStream Setup",
|
||||||
"version": "1.8.1-alpha",
|
"version": "1.8.2-alpha",
|
||||||
"identifier": "com.soulstream.setup",
|
"identifier": "com.soulstream.setup",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npx vite --config vite.setup.config.js",
|
"beforeDevCommand": "npx vite --config vite.setup.config.js",
|
||||||
|
|||||||
+55
-16
@@ -11,8 +11,12 @@
|
|||||||
//! private release cannot be read without a token. With a read-only token
|
//! private release cannot be read without a token. With a read-only token
|
||||||
//! saved in the Credential Manager (Settings → Updates → Access token), a
|
//! saved in the Credential Manager (Settings → Updates → Access token), a
|
||||||
//! GitHub source is resolved through the API instead of the browser URLs:
|
//! GitHub source is resolved through the API instead of the browser URLs:
|
||||||
//! `GET /repos/{o}/{r}/releases/latest` (or `/tags/{tag}`) finds the
|
//! `GET /repos/{o}/{r}/releases/tags/{tag}` for a pinned tag, and for
|
||||||
//! `latest.json` asset, the plugin fetches THAT asset URL with
|
//! `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
|
//! `Authorization: Bearer` + `Accept: application/octet-stream`, and the
|
||||||
//! manifest's own download URLs are API asset URLs too — the workflows
|
//! manifest's own download URLs are API asset URLs too — the workflows
|
||||||
//! rewrite them after publishing — so the installer downloads with the same
|
//! 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";
|
"https://github.com/KeelrLeek/SoulStream/releases/latest/download/latest.json";
|
||||||
pub const DEFAULT_NIGHTLY: &str =
|
pub const DEFAULT_NIGHTLY: &str =
|
||||||
"https://github.com/KeelrLeek/SoulStream/releases/download/nightly/latest.json";
|
"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.
|
/// First automatic check after launch, then the interval between checks.
|
||||||
const FIRST_CHECK: Duration = Duration::from_secs(25);
|
const FIRST_CHECK: Duration = Duration::from_secs(25);
|
||||||
@@ -199,22 +205,15 @@ fn announce(app: &AppHandle) {
|
|||||||
let _ = app.emit("updater:status", status(app));
|
let _ = app.emit("updater:status", status(app));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask the GitHub API where a private release keeps its `latest.json`.
|
/// One authenticated GitHub API GET, its failures already in words.
|
||||||
async fn resolve_github_asset(
|
async fn github_json(
|
||||||
src: &GitHubSource,
|
client: &reqwest::Client,
|
||||||
|
url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
user_agent: &str,
|
user_agent: &str,
|
||||||
) -> Result<Url, String> {
|
) -> Result<serde_json::Value, 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())?;
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&api)
|
.get(url)
|
||||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
||||||
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
||||||
.header(reqwest::header::USER_AGENT, user_agent)
|
.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()),
|
404 => return Err("no release found — or the token cannot see the repository".into()),
|
||||||
s => return Err(format!("GitHub answered {s}")),
|
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 assets = body["assets"].as_array().cloned().unwrap_or_default();
|
||||||
let asset = assets
|
let asset = assets
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "SoulStream",
|
"productName": "SoulStream",
|
||||||
"version": "1.8.1-alpha",
|
"version": "1.8.2-alpha",
|
||||||
"identifier": "com.soulstream.app",
|
"identifier": "com.soulstream.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
Reference in new issue
Block a user