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
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:
@@ -3,17 +3,89 @@ name: Build Sidecars
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
paths: ['python/**']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
bump-sidecar-version:
|
||||||
|
name: Bump sidecar version and tag
|
||||||
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.bump.outputs.version }}
|
||||||
|
tag: ${{ steps.bump.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config user.name "Gitea Actions"
|
||||||
|
git config user.email "actions@gitea.local"
|
||||||
|
|
||||||
|
- name: Bump sidecar patch version
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
# Read current version from python/pyproject.toml
|
||||||
|
CURRENT=$(grep '^version = ' python/pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||||
|
echo "Current sidecar version: ${CURRENT}"
|
||||||
|
|
||||||
|
# Increment patch number
|
||||||
|
MAJOR=$(echo "${CURRENT}" | cut -d. -f1)
|
||||||
|
MINOR=$(echo "${CURRENT}" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "${CURRENT}" | cut -d. -f3)
|
||||||
|
NEW_PATCH=$((PATCH + 1))
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||||
|
echo "New sidecar version: ${NEW_VERSION}"
|
||||||
|
|
||||||
|
# Update python/pyproject.toml
|
||||||
|
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" python/pyproject.toml
|
||||||
|
|
||||||
|
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Commit and tag
|
||||||
|
env:
|
||||||
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
TAG="${{ steps.bump.outputs.tag }}"
|
||||||
|
git add python/pyproject.toml
|
||||||
|
git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]"
|
||||||
|
git tag "${TAG}"
|
||||||
|
|
||||||
|
# Push using token for authentication
|
||||||
|
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
||||||
|
git push "${REMOTE_URL}" HEAD:main
|
||||||
|
git push "${REMOTE_URL}" "${TAG}"
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
env:
|
||||||
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
|
run: |
|
||||||
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
TAG="${{ steps.bump.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
RELEASE_NAME="Sidecar v${VERSION}"
|
||||||
|
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated sidecar build.\", \"draft\": false, \"prerelease\": false}" \
|
||||||
|
"${REPO_API}/releases"
|
||||||
|
echo "Created release: ${RELEASE_NAME}"
|
||||||
|
|
||||||
build-sidecar-linux:
|
build-sidecar-linux:
|
||||||
name: Build Sidecar (Linux)
|
name: Build Sidecar (Linux)
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
needs: bump-sidecar-version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.bump-sidecar-version.outputs.tag }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
run: |
|
run: |
|
||||||
@@ -48,23 +120,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd python/dist/voice-to-notes-sidecar && zip -r ../../../sidecar-linux-x86_64-cpu.zip .
|
cd python/dist/voice-to-notes-sidecar && zip -r ../../../sidecar-linux-x86_64-cpu.zip .
|
||||||
|
|
||||||
- name: Wait for release and upload
|
- name: Upload to sidecar release
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y jq
|
sudo apt-get install -y jq
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
TAG="${{ needs.bump-sidecar-version.outputs.tag }}"
|
||||||
|
|
||||||
# Fetch the latest release tag (retry up to 30 times with 10s delay)
|
# Find the sidecar release by tag (retry up to 30 times with 10s delay)
|
||||||
echo "Waiting for latest release to be available..."
|
echo "Waiting for sidecar release ${TAG} to be available..."
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
|
RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
"${REPO_API}/releases/latest")
|
"${REPO_API}/releases/tags/${TAG}")
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
|
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
|
||||||
TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name // empty')
|
|
||||||
|
|
||||||
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ] && [ -n "${TAG}" ] && [ "${TAG}" != "null" ]; then
|
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then
|
||||||
echo "Found release: ${TAG} (ID: ${RELEASE_ID})"
|
echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -73,7 +145,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
|
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
|
||||||
echo "ERROR: Failed to find latest release after 30 attempts."
|
echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -99,12 +171,14 @@ jobs:
|
|||||||
|
|
||||||
build-sidecar-windows:
|
build-sidecar-windows:
|
||||||
name: Build Sidecar (Windows)
|
name: Build Sidecar (Windows)
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
needs: bump-sidecar-version
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.bump-sidecar-version.outputs.tag }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -153,27 +227,26 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
7z a -tzip -mx=5 sidecar-windows-x86_64-cpu.zip .\python\dist\voice-to-notes-sidecar\*
|
7z a -tzip -mx=5 sidecar-windows-x86_64-cpu.zip .\python\dist\voice-to-notes-sidecar\*
|
||||||
|
|
||||||
- name: Wait for release and upload
|
- name: Upload to sidecar release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
$REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
$REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||||
$Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" }
|
$Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" }
|
||||||
|
$TAG = "${{ needs.bump-sidecar-version.outputs.tag }}"
|
||||||
|
|
||||||
# Fetch the latest release (retry up to 30 times with 10s delay)
|
# Find the sidecar release by tag (retry up to 30 times with 10s delay)
|
||||||
Write-Host "Waiting for latest release to be available..."
|
Write-Host "Waiting for sidecar release ${TAG} to be available..."
|
||||||
$RELEASE_ID = $null
|
$RELEASE_ID = $null
|
||||||
$TAG = $null
|
|
||||||
|
|
||||||
for ($i = 1; $i -le 30; $i++) {
|
for ($i = 1; $i -le 30; $i++) {
|
||||||
try {
|
try {
|
||||||
$release = Invoke-RestMethod -Uri "${REPO_API}/releases/latest" -Headers $Headers -ErrorAction Stop
|
$release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop
|
||||||
$RELEASE_ID = $release.id
|
$RELEASE_ID = $release.id
|
||||||
$TAG = $release.tag_name
|
|
||||||
|
|
||||||
if ($RELEASE_ID -and $TAG) {
|
if ($RELEASE_ID) {
|
||||||
Write-Host "Found release: ${TAG} (ID: ${RELEASE_ID})"
|
Write-Host "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -185,7 +258,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $RELEASE_ID) {
|
if (-not $RELEASE_ID) {
|
||||||
Write-Host "ERROR: Failed to find latest release after 30 attempts."
|
Write-Host "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +292,14 @@ jobs:
|
|||||||
|
|
||||||
build-sidecar-macos:
|
build-sidecar-macos:
|
||||||
name: Build Sidecar (macOS)
|
name: Build Sidecar (macOS)
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
needs: bump-sidecar-version
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.bump-sidecar-version.outputs.tag }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
run: |
|
run: |
|
||||||
@@ -249,24 +324,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd python/dist/voice-to-notes-sidecar && zip -r ../../../sidecar-macos-aarch64-cpu.zip .
|
cd python/dist/voice-to-notes-sidecar && zip -r ../../../sidecar-macos-aarch64-cpu.zip .
|
||||||
|
|
||||||
- name: Wait for release and upload
|
- name: Upload to sidecar release
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
which jq || brew install jq
|
which jq || brew install jq
|
||||||
|
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
TAG="${{ needs.bump-sidecar-version.outputs.tag }}"
|
||||||
|
|
||||||
# Fetch the latest release tag (retry up to 30 times with 10s delay)
|
# Find the sidecar release by tag (retry up to 30 times with 10s delay)
|
||||||
echo "Waiting for latest release to be available..."
|
echo "Waiting for sidecar release ${TAG} to be available..."
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
|
RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
"${REPO_API}/releases/latest")
|
"${REPO_API}/releases/tags/${TAG}")
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
|
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
|
||||||
TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name // empty')
|
|
||||||
|
|
||||||
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ] && [ -n "${TAG}" ] && [ "${TAG}" != "null" ]; then
|
if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then
|
||||||
echo "Found release: ${TAG} (ID: ${RELEASE_ID})"
|
echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -275,7 +350,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
|
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
|
||||||
echo "ERROR: Failed to find latest release after 30 attempts."
|
echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ jobs:
|
|||||||
# Update src-tauri/Cargo.toml (match version = "x.y.z" in [package] section)
|
# Update src-tauri/Cargo.toml (match version = "x.y.z" in [package] section)
|
||||||
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" src-tauri/Cargo.toml
|
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" src-tauri/Cargo.toml
|
||||||
|
|
||||||
# Update python/pyproject.toml
|
|
||||||
sed -i "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" python/pyproject.toml
|
|
||||||
|
|
||||||
echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -58,7 +55,7 @@ jobs:
|
|||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
||||||
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml python/pyproject.toml
|
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml
|
||||||
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
|
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
|
||||||
git tag "v${NEW_VERSION}"
|
git tag "v${NEW_VERSION}"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "voice-to-notes"
|
name = "voice-to-notes"
|
||||||
version = "0.2.12"
|
version = "1.0.0"
|
||||||
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
|
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -20,19 +20,81 @@ pub struct UpdateInfo {
|
|||||||
pub latest_version: String,
|
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]
|
#[tauri::command]
|
||||||
pub fn check_sidecar() -> bool {
|
pub fn check_sidecar() -> bool {
|
||||||
let data_dir = match DATA_DIR.get() {
|
let data_dir = match DATA_DIR.get() {
|
||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let version = match read_local_sidecar_version() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
let binary_name = if cfg!(target_os = "windows") {
|
let binary_name = if cfg!(target_os = "windows") {
|
||||||
"voice-to-notes-sidecar.exe"
|
"voice-to-notes-sidecar.exe"
|
||||||
} else {
|
} else {
|
||||||
"voice-to-notes-sidecar"
|
"voice-to-notes-sidecar"
|
||||||
};
|
};
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
|
||||||
let extract_dir = data_dir.join(format!("sidecar-{}", version));
|
let extract_dir = data_dir.join(format!("sidecar-{}", version));
|
||||||
extract_dir.join(binary_name).exists()
|
extract_dir.join(binary_name).exists()
|
||||||
}
|
}
|
||||||
@@ -61,44 +123,35 @@ fn platform_arch() -> &'static str {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), String> {
|
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 data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
let os = platform_os();
|
let os = platform_os();
|
||||||
let arch = platform_arch();
|
let arch = platform_arch();
|
||||||
let asset_name = format!("sidecar-{}-{}-{}.zip", os, arch, variant);
|
let asset_name = format!("sidecar-{}-{}-{}.zip", os, arch, variant);
|
||||||
|
|
||||||
// Fetch release info from Gitea API to get the download URL
|
// Fetch the latest sidecar release from Gitea API
|
||||||
let release_url = format!("{}/releases/tags/v{}", REPO_API, version);
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let release_resp = client
|
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
|
||||||
.get(&release_url)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to fetch release info: {}", e))?;
|
|
||||||
|
|
||||||
if !release_resp.status().is_success() {
|
let tag = sidecar_release["tag_name"]
|
||||||
return Err(format!(
|
.as_str()
|
||||||
"Failed to fetch release info: HTTP {}",
|
.ok_or("No tag_name in sidecar release")?;
|
||||||
release_resp.status()
|
let sidecar_version = version_from_sidecar_tag(tag).to_string();
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let release_json = release_resp
|
|
||||||
.json::<serde_json::Value>()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to parse release JSON: {}", e))?;
|
|
||||||
|
|
||||||
// Find the matching asset
|
// Find the matching asset
|
||||||
let assets = release_json["assets"]
|
let assets = sidecar_release["assets"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.ok_or("No assets found in release")?;
|
.ok_or("No assets found in sidecar release")?;
|
||||||
|
|
||||||
let download_url = assets
|
let download_url = assets
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a["name"].as_str() == Some(&asset_name))
|
.find(|a| a["name"].as_str() == Some(&asset_name))
|
||||||
.and_then(|a| a["browser_download_url"].as_str())
|
.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();
|
.to_string();
|
||||||
|
|
||||||
// Stream download with progress events
|
// 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))?;
|
.map_err(|e| format!("Failed to create zip file: {}", e))?;
|
||||||
|
|
||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = stream.next().await {
|
||||||
let chunk: bytes::Bytes =
|
let chunk: bytes::Bytes = chunk.map_err(|e| format!("Download stream error: {}", e))?;
|
||||||
chunk.map_err(|e| format!("Download stream error: {}", e))?;
|
|
||||||
file.write_all(&chunk)
|
file.write_all(&chunk)
|
||||||
.map_err(|e| format!("Failed to write chunk: {}", e))?;
|
.map_err(|e| format!("Failed to write chunk: {}", e))?;
|
||||||
downloaded += chunk.len() as u64;
|
downloaded += chunk.len() as u64;
|
||||||
@@ -142,7 +194,7 @@ pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract the downloaded zip
|
// 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)?;
|
SidecarManager::extract_zip(&zip_path, &extract_dir)?;
|
||||||
|
|
||||||
// Make the binary executable on Unix
|
// 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
|
// Clean up the zip file and old sidecar versions
|
||||||
let _ = std::fs::remove_file(&zip_path);
|
let _ = std::fs::remove_file(&zip_path);
|
||||||
SidecarManager::cleanup_old_sidecars(data_dir, version);
|
SidecarManager::cleanup_old_sidecars(data_dir, &sidecar_version);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -172,36 +227,19 @@ pub async fn check_sidecar_update() -> Result<Option<UpdateInfo>, String> {
|
|||||||
return Ok(None);
|
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
|
// Fetch latest sidecar release from Gitea API
|
||||||
let latest_url = format!("{}/releases/latest", REPO_API);
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp = client
|
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
|
||||||
.get(&latest_url)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to fetch latest release: {}", e))?;
|
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
let latest_tag = sidecar_release["tag_name"]
|
||||||
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"]
|
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or("No tag_name in release")?;
|
.ok_or("No tag_name in sidecar release")?;
|
||||||
|
let latest_version = version_from_sidecar_tag(latest_tag);
|
||||||
// Strip leading 'v' if present
|
|
||||||
let latest_version = latest_tag.strip_prefix('v').unwrap_or(latest_tag);
|
|
||||||
|
|
||||||
if latest_version != current_version {
|
if latest_version != current_version {
|
||||||
Ok(Some(UpdateInfo {
|
Ok(Some(UpdateInfo {
|
||||||
|
|||||||
@@ -56,10 +56,33 @@ impl SidecarManager {
|
|||||||
cfg!(debug_assertions) || std::env::var("VOICE_TO_NOTES_DEV").is_ok()
|
cfg!(debug_assertions) || std::env::var("VOICE_TO_NOTES_DEV").is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the locally installed sidecar version from `sidecar-version.txt`.
|
||||||
|
fn read_sidecar_version() -> Result<String, 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::read_to_string(&version_file)
|
||||||
|
.map_err(|_| {
|
||||||
|
"Sidecar not installed: sidecar-version.txt not found. Please download the sidecar."
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.map(|v| v.trim().to_string())
|
||||||
|
.and_then(|v| {
|
||||||
|
if v.is_empty() {
|
||||||
|
Err(
|
||||||
|
"Sidecar version file is empty. Please re-download the sidecar."
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve the frozen sidecar binary path (production mode).
|
/// Resolve the frozen sidecar binary path (production mode).
|
||||||
///
|
///
|
||||||
/// First checks if the sidecar is already extracted to the app data directory.
|
/// Reads the installed sidecar version from `sidecar-version.txt` and
|
||||||
/// If not, looks for `sidecar.zip` in the Tauri resource directory and extracts it.
|
/// looks for the binary in the corresponding `sidecar-{version}` directory.
|
||||||
|
/// If the version file doesn't exist, the sidecar hasn't been downloaded yet.
|
||||||
fn resolve_sidecar_path() -> Result<PathBuf, String> {
|
fn resolve_sidecar_path() -> Result<PathBuf, String> {
|
||||||
let binary_name = if cfg!(target_os = "windows") {
|
let binary_name = if cfg!(target_os = "windows") {
|
||||||
"voice-to-notes-sidecar.exe"
|
"voice-to-notes-sidecar.exe"
|
||||||
@@ -67,16 +90,15 @@ impl SidecarManager {
|
|||||||
"voice-to-notes-sidecar"
|
"voice-to-notes-sidecar"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Versioned extraction directory prevents stale sidecar after app updates
|
|
||||||
let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
|
let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
|
||||||
let current_version = env!("CARGO_PKG_VERSION");
|
let current_version = Self::read_sidecar_version()?;
|
||||||
let extract_dir = data_dir.join(format!("sidecar-{}", current_version));
|
let extract_dir = data_dir.join(format!("sidecar-{}", current_version));
|
||||||
|
|
||||||
let binary_path = extract_dir.join(binary_name);
|
let binary_path = extract_dir.join(binary_name);
|
||||||
|
|
||||||
// Already extracted — use it directly
|
// Already extracted — use it directly
|
||||||
if binary_path.exists() {
|
if binary_path.exists() {
|
||||||
Self::cleanup_old_sidecars(data_dir, current_version);
|
Self::cleanup_old_sidecars(data_dir, ¤t_version);
|
||||||
return Ok(binary_path);
|
return Ok(binary_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +124,7 @@ impl SidecarManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::cleanup_old_sidecars(data_dir, current_version);
|
Self::cleanup_old_sidecars(data_dir, ¤t_version);
|
||||||
Ok(binary_path)
|
Ok(binary_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user