Decouple sidecar versioning from app versioning
Some checks failed
Build Sidecars / Bump sidecar version and tag (push) Successful in 3s
Release / Bump version and tag (push) Failing after 3s
Release / Build App (Linux) (push) Has been skipped
Release / Build App (Windows) (push) Has been skipped
Release / Build App (macOS) (push) Has been skipped
Build Sidecars / Build Sidecar (macOS) (push) Successful in 5m28s
Build Sidecars / Build Sidecar (Linux) (push) Successful in 13m54s
Build Sidecars / Build Sidecar (Windows) (push) Successful in 37m38s

Sidecar now has its own version (1.0.0) and release lifecycle:
- Sidecar tags: sidecar-v1.0.0, sidecar-v1.0.1, etc.
- App tags: v0.2.x (unchanged)
- Sidecar workflow triggers only on python/** changes or manual dispatch
- App release no longer bumps python/pyproject.toml

Sidecar version tracked via sidecar-version.txt in app data dir:
- resolve_sidecar_path() reads version from file instead of CARGO_PKG_VERSION
- download_sidecar() fetches latest sidecar-v* release from Gitea API
- check_sidecar_update() compares local vs remote sidecar versions
- Version file written after successful download

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-22 07:57:51 -07:00
parent 9652290a06
commit 45247ae66e
5 changed files with 226 additions and 94 deletions

View File

@@ -20,19 +20,81 @@ pub struct UpdateInfo {
pub latest_version: String,
}
/// Check if the sidecar binary exists for the current version.
/// Read the locally installed sidecar version from `sidecar-version.txt`.
/// Returns `None` if the file doesn't exist or can't be read.
fn read_local_sidecar_version() -> Option<String> {
let data_dir = DATA_DIR.get()?;
let version_file = data_dir.join("sidecar-version.txt");
std::fs::read_to_string(version_file)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
/// Write the sidecar version to `sidecar-version.txt` after a successful download.
fn write_local_sidecar_version(version: &str) -> Result<(), String> {
let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
let version_file = data_dir.join("sidecar-version.txt");
std::fs::write(&version_file, version)
.map_err(|e| format!("Failed to write sidecar version file: {}", e))
}
/// Fetch releases from the Gitea API and find the latest sidecar release
/// (one whose tag_name starts with "sidecar-v").
async fn fetch_latest_sidecar_release(
client: &reqwest::Client,
) -> Result<serde_json::Value, String> {
let releases_url = format!("{}/releases?limit=20", REPO_API);
let resp = client
.get(&releases_url)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch releases: {}", e))?;
if !resp.status().is_success() {
return Err(format!("Failed to fetch releases: HTTP {}", resp.status()));
}
let releases = resp
.json::<Vec<serde_json::Value>>()
.await
.map_err(|e| format!("Failed to parse releases JSON: {}", e))?;
releases
.into_iter()
.find(|r| {
r["tag_name"]
.as_str()
.map_or(false, |t| t.starts_with("sidecar-v"))
})
.ok_or_else(|| "No sidecar release found".to_string())
}
/// Extract the version string from a sidecar tag name (e.g. "sidecar-v1.0.1" -> "1.0.1").
fn version_from_sidecar_tag(tag: &str) -> &str {
tag.strip_prefix("sidecar-v").unwrap_or(tag)
}
/// Check if the sidecar binary exists for the currently installed version.
#[tauri::command]
pub fn check_sidecar() -> bool {
let data_dir = match DATA_DIR.get() {
Some(d) => d,
None => return false,
};
let version = match read_local_sidecar_version() {
Some(v) => v,
None => return false,
};
let binary_name = if cfg!(target_os = "windows") {
"voice-to-notes-sidecar.exe"
} else {
"voice-to-notes-sidecar"
};
let version = env!("CARGO_PKG_VERSION");
let extract_dir = data_dir.join(format!("sidecar-{}", version));
extract_dir.join(binary_name).exists()
}
@@ -61,44 +123,35 @@ fn platform_arch() -> &'static str {
#[tauri::command]
pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), String> {
let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
let version = env!("CARGO_PKG_VERSION");
let os = platform_os();
let arch = platform_arch();
let asset_name = format!("sidecar-{}-{}-{}.zip", os, arch, variant);
// Fetch release info from Gitea API to get the download URL
let release_url = format!("{}/releases/tags/v{}", REPO_API, version);
// Fetch the latest sidecar release from Gitea API
let client = reqwest::Client::new();
let release_resp = client
.get(&release_url)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch release info: {}", e))?;
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
if !release_resp.status().is_success() {
return Err(format!(
"Failed to fetch release info: HTTP {}",
release_resp.status()
));
}
let release_json = release_resp
.json::<serde_json::Value>()
.await
.map_err(|e| format!("Failed to parse release JSON: {}", e))?;
let tag = sidecar_release["tag_name"]
.as_str()
.ok_or("No tag_name in sidecar release")?;
let sidecar_version = version_from_sidecar_tag(tag).to_string();
// Find the matching asset
let assets = release_json["assets"]
let assets = sidecar_release["assets"]
.as_array()
.ok_or("No assets found in release")?;
.ok_or("No assets found in sidecar release")?;
let download_url = assets
.iter()
.find(|a| a["name"].as_str() == Some(&asset_name))
.and_then(|a| a["browser_download_url"].as_str())
.ok_or_else(|| format!("Asset '{}' not found in release v{}", asset_name, version))?
.ok_or_else(|| {
format!(
"Asset '{}' not found in sidecar release {}",
asset_name, tag
)
})?
.to_string();
// Stream download with progress events
@@ -121,8 +174,7 @@ pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), Str
.map_err(|e| format!("Failed to create zip file: {}", e))?;
while let Some(chunk) = stream.next().await {
let chunk: bytes::Bytes =
chunk.map_err(|e| format!("Download stream error: {}", e))?;
let chunk: bytes::Bytes = chunk.map_err(|e| format!("Download stream error: {}", e))?;
file.write_all(&chunk)
.map_err(|e| format!("Failed to write chunk: {}", e))?;
downloaded += chunk.len() as u64;
@@ -142,7 +194,7 @@ pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), Str
}
// Extract the downloaded zip
let extract_dir = data_dir.join(format!("sidecar-{}", version));
let extract_dir = data_dir.join(format!("sidecar-{}", sidecar_version));
SidecarManager::extract_zip(&zip_path, &extract_dir)?;
// Make the binary executable on Unix
@@ -157,9 +209,12 @@ pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), Str
}
}
// Write the sidecar version file
write_local_sidecar_version(&sidecar_version)?;
// Clean up the zip file and old sidecar versions
let _ = std::fs::remove_file(&zip_path);
SidecarManager::cleanup_old_sidecars(data_dir, version);
SidecarManager::cleanup_old_sidecars(data_dir, &sidecar_version);
Ok(())
}
@@ -172,36 +227,19 @@ pub async fn check_sidecar_update() -> Result<Option<UpdateInfo>, String> {
return Ok(None);
}
let current_version = env!("CARGO_PKG_VERSION").to_string();
let current_version = match read_local_sidecar_version() {
Some(v) => v,
None => return Ok(None),
};
// Fetch latest release from Gitea API
let latest_url = format!("{}/releases/latest", REPO_API);
// Fetch latest sidecar release from Gitea API
let client = reqwest::Client::new();
let resp = client
.get(&latest_url)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to fetch latest release: {}", e))?;
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
if !resp.status().is_success() {
return Err(format!(
"Failed to fetch latest release: HTTP {}",
resp.status()
));
}
let release_json = resp
.json::<serde_json::Value>()
.await
.map_err(|e| format!("Failed to parse release JSON: {}", e))?;
let latest_tag = release_json["tag_name"]
let latest_tag = sidecar_release["tag_name"]
.as_str()
.ok_or("No tag_name in release")?;
// Strip leading 'v' if present
let latest_version = latest_tag.strip_prefix('v').unwrap_or(latest_tag);
.ok_or("No tag_name in sidecar release")?;
let latest_version = version_from_sidecar_tag(latest_tag);
if latest_version != current_version {
Ok(Some(UpdateInfo {