diff --git a/.gitea/workflows/build-app-linux.yml b/.gitea/workflows/build-app-linux.yml new file mode 100644 index 0000000..97d6e1a --- /dev/null +++ b/.gitea/workflows/build-app-linux.yml @@ -0,0 +1,106 @@ +name: Build App (Linux) + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. v1.4.5)' + required: false + +jobs: + build-linux: + name: Build App (Linux) + runs-on: ubuntu-latest + env: + NODE_VERSION: "20" + steps: + - name: Determine tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | head -1 | sed 's|.*refs/tags/||') + fi + echo "Building for tag: ${TAG}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust stable + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils rpm + + - name: Install npm dependencies + run: npm ci + + - name: Build Tauri app + run: npm run tauri build + + - name: Upload to release + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + sudo apt-get install -y jq + REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + TAG="${{ steps.tag.outputs.tag }}" + echo "Release tag: ${TAG}" + + echo "Waiting for release ${TAG} to be available..." + RELEASE_ID="" + for i in $(seq 1 30); do + RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/tags/${TAG}") + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then + echo "Found release: ${TAG} (ID: ${RELEASE_ID})" + break + fi + + echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + sleep 10 + done + + if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then + echo "ERROR: Failed to find release for tag ${TAG} after 30 attempts." + exit 1 + fi + + find src-tauri/target/release/bundle -type f \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) | while IFS= read -r file; do + filename=$(basename "$file") + encoded_name=$(echo "$filename" | sed 's/ /%20/g') + echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." + + ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") + if [ -n "${ASSET_ID}" ]; then + curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${BUILD_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + -T "$file" \ + "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") + echo "Upload response: HTTP ${HTTP_CODE}" + done diff --git a/.gitea/workflows/build-app-macos.yml b/.gitea/workflows/build-app-macos.yml new file mode 100644 index 0000000..b43a662 --- /dev/null +++ b/.gitea/workflows/build-app-macos.yml @@ -0,0 +1,104 @@ +name: Build App (macOS) + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. v1.4.5)' + required: false + +jobs: + build-macos: + name: Build App (macOS) + runs-on: macos-latest + env: + NODE_VERSION: "20" + steps: + - name: Determine tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' | head -1 | sed 's|.*refs/tags/||') + fi + echo "Building for tag: ${TAG}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust stable + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install system dependencies + run: brew install --quiet create-dmg || true + + - name: Install npm dependencies + run: npm ci + + - name: Build Tauri app + run: npm run tauri build + + - name: Upload to release + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + which jq || brew install jq + REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + TAG="${{ steps.tag.outputs.tag }}" + echo "Release tag: ${TAG}" + + echo "Waiting for release ${TAG} to be available..." + RELEASE_ID="" + for i in $(seq 1 30); do + RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/tags/${TAG}") + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then + echo "Found release: ${TAG} (ID: ${RELEASE_ID})" + break + fi + + echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + sleep 10 + done + + if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then + echo "ERROR: Failed to find release for tag ${TAG} after 30 attempts." + exit 1 + fi + + find src-tauri/target/release/bundle -type f -name "*.dmg" | while IFS= read -r file; do + filename=$(basename "$file") + encoded_name=$(echo "$filename" | sed 's/ /%20/g') + echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." + + ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") + if [ -n "${ASSET_ID}" ]; then + curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${BUILD_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + -T "$file" \ + "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") + echo "Upload response: HTTP ${HTTP_CODE}" + done diff --git a/.gitea/workflows/build-app-windows.yml b/.gitea/workflows/build-app-windows.yml new file mode 100644 index 0000000..cbcf323 --- /dev/null +++ b/.gitea/workflows/build-app-windows.yml @@ -0,0 +1,126 @@ +name: Build App (Windows) + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. v1.4.5)' + required: false + +jobs: + build-windows: + name: Build App (Windows) + runs-on: windows-latest + env: + NODE_VERSION: "20" + steps: + - name: Determine tag + id: tag + shell: powershell + run: | + $inputTag = "${{ github.event.inputs.tag }}" + $ref = "${{ github.ref }}" + $refName = "${{ github.ref_name }}" + + if ($inputTag) { + $TAG = $inputTag + } elseif ($ref -like "refs/tags/*") { + $TAG = $refName + } else { + $tags = git ls-remote --tags --sort=-v:refname origin 'refs/tags/v*' 2>&1 + $TAG = ($tags | Select-Object -First 1) -replace '.*refs/tags/', '' + } + Write-Host "Building for tag: ${TAG}" + echo "tag=${TAG}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust stable + shell: powershell + run: | + if (Get-Command rustup -ErrorAction SilentlyContinue) { + rustup default stable + } else { + Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup-init.exe + .\rustup-init.exe -y --default-toolchain stable + echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + } + + - name: Install npm dependencies + shell: powershell + run: npm ci + + - name: Build Tauri app + shell: powershell + run: npm run tauri build + + - name: Upload to release + shell: powershell + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + $REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + $Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" } + $TAG = "${{ steps.tag.outputs.tag }}" + Write-Host "Release tag: ${TAG}" + + Write-Host "Waiting for release ${TAG} to be available..." + $RELEASE_ID = $null + + for ($i = 1; $i -le 30; $i++) { + try { + $release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop + $RELEASE_ID = $release.id + + if ($RELEASE_ID) { + Write-Host "Found release: ${TAG} (ID: ${RELEASE_ID})" + break + } + } catch {} + + Write-Host "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + Start-Sleep -Seconds 10 + } + + if (-not $RELEASE_ID) { + Write-Host "ERROR: Failed to find release for tag ${TAG} after 30 attempts." + exit 1 + } + + Get-ChildItem -Path src-tauri\target\release\bundle -Recurse -Include *.msi,*-setup.exe | ForEach-Object { + $filename = $_.Name + $encodedName = [System.Uri]::EscapeDataString($filename) + $size = [math]::Round($_.Length / 1MB, 1) + Write-Host "Uploading ${filename} (${size} MB)..." + + try { + $assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers + $existing = $assets | Where-Object { $_.name -eq $filename } + if ($existing) { + Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets/$($existing.id)" -Method Delete -Headers $Headers + } + } catch {} + + $uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}" + $result = curl.exe --fail --silent --show-error ` + -X POST ` + -H "Authorization: token $env:BUILD_TOKEN" ` + -H "Content-Type: application/octet-stream" ` + -T "$($_.FullName)" ` + "$uploadUrl" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "Upload successful: ${filename}" + } else { + Write-Host "WARNING: Upload failed for ${filename}: ${result}" + } + } diff --git a/.gitea/workflows/build-sidecar-linux.yml b/.gitea/workflows/build-sidecar-linux.yml new file mode 100644 index 0000000..e502896 --- /dev/null +++ b/.gitea/workflows/build-sidecar-linux.yml @@ -0,0 +1,121 @@ +name: Build Sidecar (Linux) + +on: + push: + tags: + - 'sidecar-v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. sidecar-v1.0.3)' + required: false + +jobs: + build-sidecar-linux: + name: Build Sidecar (Linux) + runs-on: ubuntu-latest + env: + PYTHON_VERSION: "3.11" + steps: + - name: Determine tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/sidecar-v*' | head -1 | sed 's|.*refs/tags/||') + fi + echo "Building for tag: ${TAG}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Install uv + run: | + if command -v uv &> /dev/null; then + echo "uv already installed: $(uv --version)" + else + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + fi + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y portaudio19-dev + + - name: Build sidecar (CUDA) + run: | + uv sync --frozen || uv sync + uv run pyinstaller local-transcription-headless.spec + + - name: Package sidecar (CUDA) + run: | + cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cuda.zip . + + - name: Build sidecar (CPU) + run: | + rm -rf dist/local-transcription-backend build/ + uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall + # Run pyinstaller directly from venv to prevent uv run from + # re-resolving torch back to the CUDA version via pyproject.toml sources + .venv/bin/pyinstaller local-transcription-headless.spec + + - name: Package sidecar (CPU) + run: | + cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cpu.zip . + + - name: Upload to sidecar release + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + sudo apt-get install -y jq + REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + TAG="${{ steps.tag.outputs.tag }}" + + echo "Waiting for sidecar release ${TAG} to be available..." + for i in $(seq 1 30); do + RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/tags/${TAG}") + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then + echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" + break + fi + + echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + sleep 10 + done + + if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then + echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." + exit 1 + fi + + for file in sidecar-*.zip; do + filename=$(basename "$file") + encoded_name=$(echo "$filename" | sed 's/ /%20/g') + echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." + + ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") + if [ -n "${ASSET_ID}" ]; then + curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${BUILD_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + -T "$file" \ + "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") + echo "Upload response: HTTP ${HTTP_CODE}" + done diff --git a/.gitea/workflows/build-sidecar-macos.yml b/.gitea/workflows/build-sidecar-macos.yml new file mode 100644 index 0000000..2ea624e --- /dev/null +++ b/.gitea/workflows/build-sidecar-macos.yml @@ -0,0 +1,112 @@ +name: Build Sidecar (macOS) + +on: + push: + tags: + - 'sidecar-v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. sidecar-v1.0.3)' + required: false + +jobs: + build-sidecar-macos: + name: Build Sidecar (macOS) + runs-on: macos-latest + env: + PYTHON_VERSION: "3.11" + steps: + - name: Determine tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG=$(git ls-remote --tags --sort=-v:refname origin 'refs/tags/sidecar-v*' | head -1 | sed 's|.*refs/tags/||') + fi + echo "Building for tag: ${TAG}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Install uv + run: | + if command -v uv &> /dev/null; then + echo "uv already installed: $(uv --version)" + else + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + fi + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install system dependencies + run: brew install portaudio + + - name: Build sidecar (CPU) + env: + UV_NO_SOURCES: "1" + run: | + # UV_NO_SOURCES bypasses pyproject.toml's [tool.uv.sources] which forces + # torch from the CUDA index (no macOS ARM wheels there). + # Default PyPI torch includes MPS (Apple Silicon GPU) support. + uv sync + .venv/bin/pyinstaller local-transcription-headless.spec + + - name: Package sidecar (CPU) + run: | + cd dist/local-transcription-backend && zip -r ../../sidecar-macos-aarch64-cpu.zip . + + - name: Upload to sidecar release + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + which jq || brew install jq + REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + TAG="${{ steps.tag.outputs.tag }}" + + echo "Waiting for sidecar release ${TAG} to be available..." + for i in $(seq 1 30); do + RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/tags/${TAG}") + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then + echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" + break + fi + + echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + sleep 10 + done + + if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then + echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." + exit 1 + fi + + for file in sidecar-*.zip; do + filename=$(basename "$file") + encoded_name=$(echo "$filename" | sed 's/ /%20/g') + echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." + + ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") + if [ -n "${ASSET_ID}" ]; then + curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ + "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${BUILD_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + -T "$file" \ + "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") + echo "Upload response: HTTP ${HTTP_CODE}" + done diff --git a/.gitea/workflows/build-sidecar-windows.yml b/.gitea/workflows/build-sidecar-windows.yml new file mode 100644 index 0000000..1062fb2 --- /dev/null +++ b/.gitea/workflows/build-sidecar-windows.yml @@ -0,0 +1,158 @@ +name: Build Sidecar (Windows) + +on: + push: + tags: + - 'sidecar-v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to build (e.g. sidecar-v1.0.3)' + required: false + +jobs: + build-sidecar-windows: + name: Build Sidecar (Windows) + runs-on: windows-latest + env: + PYTHON_VERSION: "3.11" + steps: + - name: Determine tag + id: tag + shell: powershell + run: | + $inputTag = "${{ github.event.inputs.tag }}" + $ref = "${{ github.ref }}" + $refName = "${{ github.ref_name }}" + + if ($inputTag) { + $TAG = $inputTag + } elseif ($ref -like "refs/tags/*") { + $TAG = $refName + } else { + $tags = git ls-remote --tags --sort=-v:refname origin 'refs/tags/sidecar-v*' 2>&1 + $TAG = ($tags | Select-Object -First 1) -replace '.*refs/tags/', '' + } + Write-Host "Building for tag: ${TAG}" + echo "tag=${TAG}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - name: Install uv + shell: powershell + run: | + if (Get-Command uv -ErrorAction SilentlyContinue) { + Write-Host "uv already installed: $(uv --version)" + } else { + irm https://astral.sh/uv/install.ps1 | iex + # Add both possible uv install locations to PATH + $uvPaths = @( + "$env:USERPROFILE\.local\bin", + "$env:USERPROFILE\.cargo\bin", + "$env:LOCALAPPDATA\uv\bin" + ) + foreach ($p in $uvPaths) { + if (Test-Path $p) { + echo $p | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + } + } + } + + - name: Set up Python + shell: powershell + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install 7-Zip + shell: powershell + run: | + if (-not (Get-Command 7z -ErrorAction SilentlyContinue)) { + choco install 7zip -y + } + + - name: Build sidecar (CUDA) + shell: powershell + run: | + uv sync --frozen + if ($LASTEXITCODE -ne 0) { uv sync } + uv run pyinstaller local-transcription-headless.spec + + - name: Package sidecar (CUDA) + shell: powershell + run: | + 7z a -tzip -mx=5 sidecar-windows-x86_64-cuda.zip .\dist\local-transcription-backend\* + + - name: Build sidecar (CPU) + shell: powershell + run: | + Remove-Item -Recurse -Force dist\local-transcription-backend, build -ErrorAction SilentlyContinue + uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall + # Run pyinstaller directly from venv to prevent uv run from + # re-resolving torch back to the CUDA version via pyproject.toml sources + .venv\Scripts\pyinstaller.exe local-transcription-headless.spec + + - name: Package sidecar (CPU) + shell: powershell + run: | + 7z a -tzip -mx=5 sidecar-windows-x86_64-cpu.zip .\dist\local-transcription-backend\* + + - name: Upload to sidecar release + shell: powershell + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + $REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + $Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" } + $TAG = "${{ steps.tag.outputs.tag }}" + + Write-Host "Waiting for sidecar release ${TAG} to be available..." + $RELEASE_ID = $null + + for ($i = 1; $i -le 30; $i++) { + try { + $release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop + $RELEASE_ID = $release.id + + if ($RELEASE_ID) { + Write-Host "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" + break + } + } catch {} + + Write-Host "Attempt ${i}/30: Release not ready yet, retrying in 10s..." + Start-Sleep -Seconds 10 + } + + if (-not $RELEASE_ID) { + Write-Host "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." + exit 1 + } + + Get-ChildItem -Path . -Filter "sidecar-*.zip" | ForEach-Object { + $filename = $_.Name + $encodedName = [System.Uri]::EscapeDataString($filename) + $size = [math]::Round($_.Length / 1MB, 1) + Write-Host "Uploading ${filename} (${size} MB)..." + + try { + $assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers + $existing = $assets | Where-Object { $_.name -eq $filename } + if ($existing) { + Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets/$($existing.id)" -Method Delete -Headers $Headers + } + } catch {} + + $uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}" + $result = curl.exe --fail --silent --show-error ` + -X POST ` + -H "Authorization: token $env:BUILD_TOKEN" ` + -H "Content-Type: application/octet-stream" ` + -T "$($_.FullName)" ` + "$uploadUrl" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host "Upload successful: ${filename}" + } else { + Write-Host "WARNING: Upload failed for ${filename}: ${result}" + } + } diff --git a/.gitea/workflows/build-sidecar.yml b/.gitea/workflows/build-sidecar.yml deleted file mode 100644 index d896e3a..0000000 --- a/.gitea/workflows/build-sidecar.yml +++ /dev/null @@ -1,431 +0,0 @@ -name: Build Sidecars - -on: - push: - branches: [main] - paths: - - 'client/**' - - 'server/**' - - 'backend/**' - - 'pyproject.toml' - - 'local-transcription-headless.spec' - workflow_dispatch: - -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 }} - has_changes: ${{ steps.check_changes.outputs.has_changes }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - name: Check for backend changes - id: check_changes - run: | - # If triggered by workflow_dispatch, always build - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - exit 0 - fi - # Check if relevant files changed in this commit - CHANGED=$(git diff --name-only HEAD~1 HEAD -- client/ server/ backend/ pyproject.toml local-transcription-headless.spec 2>/dev/null || echo "") - if [ -n "$CHANGED" ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "Backend changes detected: $CHANGED" - else - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "No backend changes detected, skipping sidecar build" - fi - - - name: Configure git - if: steps.check_changes.outputs.has_changes == 'true' - run: | - git config user.name "Gitea Actions" - git config user.email "actions@gitea.local" - - - name: Bump sidecar patch version - if: steps.check_changes.outputs.has_changes == 'true' - id: bump - run: | - # Read current version from pyproject.toml - CURRENT=$(grep '^version = ' 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 pyproject.toml - sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml - - # Update version.py - sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py - sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py - - echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT - echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT - - - name: Commit and tag - if: steps.check_changes.outputs.has_changes == 'true' - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - NEW_VERSION="${{ steps.bump.outputs.version }}" - TAG="${{ steps.bump.outputs.tag }}" - git add pyproject.toml version.py - git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]" - git tag "${TAG}" - - REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|") - git pull --rebase "${REMOTE_URL}" main || true - git push "${REMOTE_URL}" HEAD:main - git push "${REMOTE_URL}" "${TAG}" - - - name: Create Gitea release - if: steps.check_changes.outputs.has_changes == 'true' - 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}" - - # ── Linux sidecar (CUDA + CPU) ── - - build-sidecar-linux: - name: Build Sidecar (Linux) - needs: bump-sidecar-version - if: needs.bump-sidecar-version.outputs.has_changes == 'true' - runs-on: ubuntu-latest - env: - PYTHON_VERSION: "3.11" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-sidecar-version.outputs.tag }} - - - name: Install uv - run: | - if command -v uv &> /dev/null; then - echo "uv already installed: $(uv --version)" - else - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH - fi - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y portaudio19-dev - - - name: Build sidecar (CUDA) - run: | - uv sync --frozen || uv sync - uv run pyinstaller local-transcription-headless.spec - - - name: Package sidecar (CUDA) - run: | - cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cuda.zip . - - - name: Build sidecar (CPU) - run: | - rm -rf dist/local-transcription-backend build/ - uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall - # Run pyinstaller directly from venv to prevent uv run from - # re-resolving torch back to the CUDA version via pyproject.toml sources - .venv/bin/pyinstaller local-transcription-headless.spec - - - name: Package sidecar (CPU) - run: | - cd dist/local-transcription-backend && zip -r ../../sidecar-linux-x86_64-cpu.zip . - - - name: Upload to sidecar release - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - sudo apt-get install -y jq - REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - TAG="${{ needs.bump-sidecar-version.outputs.tag }}" - - echo "Waiting for sidecar release ${TAG} to be available..." - for i in $(seq 1 30); do - RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/tags/${TAG}") - RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') - - if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then - echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" - break - fi - - echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." - sleep 10 - done - - if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then - echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." - exit 1 - fi - - for file in sidecar-*.zip; do - filename=$(basename "$file") - encoded_name=$(echo "$filename" | sed 's/ /%20/g') - echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." - - ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") - if [ -n "${ASSET_ID}" ]; then - curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" - fi - - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${BUILD_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - -T "$file" \ - "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") - echo "Upload response: HTTP ${HTTP_CODE}" - done - - # ── Windows sidecar (CUDA + CPU) ── - - build-sidecar-windows: - name: Build Sidecar (Windows) - needs: bump-sidecar-version - if: needs.bump-sidecar-version.outputs.has_changes == 'true' - runs-on: windows-latest - env: - PYTHON_VERSION: "3.11" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-sidecar-version.outputs.tag }} - - - name: Install uv - shell: powershell - run: | - if (Get-Command uv -ErrorAction SilentlyContinue) { - Write-Host "uv already installed: $(uv --version)" - } else { - irm https://astral.sh/uv/install.ps1 | iex - # Add both possible uv install locations to PATH - $uvPaths = @( - "$env:USERPROFILE\.local\bin", - "$env:USERPROFILE\.cargo\bin", - "$env:LOCALAPPDATA\uv\bin" - ) - foreach ($p in $uvPaths) { - if (Test-Path $p) { - echo $p | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - } - } - } - - - name: Set up Python - shell: powershell - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install 7-Zip - shell: powershell - run: | - if (-not (Get-Command 7z -ErrorAction SilentlyContinue)) { - choco install 7zip -y - } - - - name: Build sidecar (CUDA) - shell: powershell - run: | - uv sync --frozen - if ($LASTEXITCODE -ne 0) { uv sync } - uv run pyinstaller local-transcription-headless.spec - - - name: Package sidecar (CUDA) - shell: powershell - run: | - 7z a -tzip -mx=5 sidecar-windows-x86_64-cuda.zip .\dist\local-transcription-backend\* - - - name: Build sidecar (CPU) - shell: powershell - run: | - Remove-Item -Recurse -Force dist\local-transcription-backend, build -ErrorAction SilentlyContinue - uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --force-reinstall - # Run pyinstaller directly from venv to prevent uv run from - # re-resolving torch back to the CUDA version via pyproject.toml sources - .venv\Scripts\pyinstaller.exe local-transcription-headless.spec - - - name: Package sidecar (CPU) - shell: powershell - run: | - 7z a -tzip -mx=5 sidecar-windows-x86_64-cpu.zip .\dist\local-transcription-backend\* - - - name: Upload to sidecar release - shell: powershell - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - $REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" - $Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" } - $TAG = "${{ needs.bump-sidecar-version.outputs.tag }}" - - Write-Host "Waiting for sidecar release ${TAG} to be available..." - $RELEASE_ID = $null - - for ($i = 1; $i -le 30; $i++) { - try { - $release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop - $RELEASE_ID = $release.id - - if ($RELEASE_ID) { - Write-Host "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" - break - } - } catch {} - - Write-Host "Attempt ${i}/30: Release not ready yet, retrying in 10s..." - Start-Sleep -Seconds 10 - } - - if (-not $RELEASE_ID) { - Write-Host "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." - exit 1 - } - - Get-ChildItem -Path . -Filter "sidecar-*.zip" | ForEach-Object { - $filename = $_.Name - $encodedName = [System.Uri]::EscapeDataString($filename) - $size = [math]::Round($_.Length / 1MB, 1) - Write-Host "Uploading ${filename} (${size} MB)..." - - try { - $assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers - $existing = $assets | Where-Object { $_.name -eq $filename } - if ($existing) { - Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets/$($existing.id)" -Method Delete -Headers $Headers - } - } catch {} - - $uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}" - $result = curl.exe --fail --silent --show-error ` - -X POST ` - -H "Authorization: token $env:BUILD_TOKEN" ` - -H "Content-Type: application/octet-stream" ` - -T "$($_.FullName)" ` - "$uploadUrl" 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "Upload successful: ${filename}" - } else { - Write-Host "WARNING: Upload failed for ${filename}: ${result}" - } - } - - # ── macOS sidecar (CPU only — no CUDA on macOS) ── - - build-sidecar-macos: - name: Build Sidecar (macOS) - needs: bump-sidecar-version - if: needs.bump-sidecar-version.outputs.has_changes == 'true' - runs-on: macos-latest - env: - PYTHON_VERSION: "3.11" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-sidecar-version.outputs.tag }} - - - name: Install uv - run: | - if command -v uv &> /dev/null; then - echo "uv already installed: $(uv --version)" - else - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH - fi - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install system dependencies - run: brew install portaudio - - - name: Build sidecar (CPU) - env: - UV_NO_SOURCES: "1" - run: | - # UV_NO_SOURCES bypasses pyproject.toml's [tool.uv.sources] which forces - # torch from the CUDA index (no macOS ARM wheels there). - # Default PyPI torch includes MPS (Apple Silicon GPU) support. - uv sync - .venv/bin/pyinstaller local-transcription-headless.spec - - - name: Package sidecar (CPU) - run: | - cd dist/local-transcription-backend && zip -r ../../sidecar-macos-aarch64-cpu.zip . - - - name: Upload to sidecar release - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - which jq || brew install jq - REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - TAG="${{ needs.bump-sidecar-version.outputs.tag }}" - - echo "Waiting for sidecar release ${TAG} to be available..." - for i in $(seq 1 30); do - RELEASE_JSON=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/tags/${TAG}") - RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') - - if [ -n "${RELEASE_ID}" ] && [ "${RELEASE_ID}" != "null" ]; then - echo "Found sidecar release: ${TAG} (ID: ${RELEASE_ID})" - break - fi - - echo "Attempt ${i}/30: Release not ready yet, retrying in 10s..." - sleep 10 - done - - if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then - echo "ERROR: Failed to find sidecar release for tag ${TAG} after 30 attempts." - exit 1 - fi - - for file in sidecar-*.zip; do - filename=$(basename "$file") - encoded_name=$(echo "$filename" | sed 's/ /%20/g') - echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." - - ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") - if [ -n "${ASSET_ID}" ]; then - curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" - fi - - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${BUILD_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - -T "$file" \ - "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") - echo "Upload response: HTTP ${HTTP_CODE}" - done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 42aed67..0d7a402 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -81,220 +81,3 @@ jobs: -d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \ "${REPO_API}/releases" echo "Created release: ${RELEASE_NAME}" - - # ── Platform builds (run after version bump) ── - - build-linux: - name: Build App (Linux) - needs: bump-version - runs-on: ubuntu-latest - env: - NODE_VERSION: "20" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-version.outputs.tag }} - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Rust stable - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils rpm - - - name: Install npm dependencies - run: npm ci - - - name: Build Tauri app - run: npm run tauri build - - - name: Upload to release - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - sudo apt-get install -y jq - REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - TAG="${{ needs.bump-version.outputs.tag }}" - echo "Release tag: ${TAG}" - - RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty') - - if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then - echo "ERROR: Failed to find release for tag ${TAG}." - exit 1 - fi - echo "Release ID: ${RELEASE_ID}" - - find src-tauri/target/release/bundle -type f \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) | while IFS= read -r file; do - filename=$(basename "$file") - encoded_name=$(echo "$filename" | sed 's/ /%20/g') - echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." - - ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") - if [ -n "${ASSET_ID}" ]; then - curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" - fi - - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${BUILD_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - -T "$file" \ - "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") - echo "Upload response: HTTP ${HTTP_CODE}" - done - - build-windows: - name: Build App (Windows) - needs: bump-version - runs-on: windows-latest - env: - NODE_VERSION: "20" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-version.outputs.tag }} - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Rust stable - shell: powershell - run: | - if (Get-Command rustup -ErrorAction SilentlyContinue) { - rustup default stable - } else { - Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup-init.exe - .\rustup-init.exe -y --default-toolchain stable - echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - } - - - name: Install npm dependencies - shell: powershell - run: npm ci - - - name: Build Tauri app - shell: powershell - run: npm run tauri build - - - name: Upload to release - shell: powershell - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - $REPO_API = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}" - $Headers = @{ "Authorization" = "token $env:BUILD_TOKEN" } - $TAG = "${{ needs.bump-version.outputs.tag }}" - Write-Host "Release tag: ${TAG}" - - $release = Invoke-RestMethod -Uri "${REPO_API}/releases/tags/${TAG}" -Headers $Headers -ErrorAction Stop - $RELEASE_ID = $release.id - Write-Host "Release ID: ${RELEASE_ID}" - - Get-ChildItem -Path src-tauri\target\release\bundle -Recurse -Include *.msi,*-setup.exe | ForEach-Object { - $filename = $_.Name - $encodedName = [System.Uri]::EscapeDataString($filename) - $size = [math]::Round($_.Length / 1MB, 1) - Write-Host "Uploading ${filename} (${size} MB)..." - - try { - $assets = Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets" -Headers $Headers - $existing = $assets | Where-Object { $_.name -eq $filename } - if ($existing) { - Invoke-RestMethod -Uri "${REPO_API}/releases/${RELEASE_ID}/assets/$($existing.id)" -Method Delete -Headers $Headers - } - } catch {} - - $uploadUrl = "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encodedName}" - $result = curl.exe --fail --silent --show-error ` - -X POST ` - -H "Authorization: token $env:BUILD_TOKEN" ` - -H "Content-Type: application/octet-stream" ` - -T "$($_.FullName)" ` - "$uploadUrl" 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "Upload successful: ${filename}" - } else { - Write-Host "WARNING: Upload failed for ${filename}: ${result}" - } - } - - build-macos: - name: Build App (macOS) - needs: bump-version - runs-on: macos-latest - env: - NODE_VERSION: "20" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.bump-version.outputs.tag }} - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Rust stable - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - - name: Install system dependencies - run: brew install --quiet create-dmg || true - - - name: Install npm dependencies - run: npm ci - - - name: Build Tauri app - run: npm run tauri build - - - name: Upload to release - env: - BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} - run: | - which jq || brew install jq - REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - TAG="${{ needs.bump-version.outputs.tag }}" - echo "Release tag: ${TAG}" - - RELEASE_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/tags/${TAG}" | jq -r '.id // empty') - - if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then - echo "ERROR: Failed to find release for tag ${TAG}." - exit 1 - fi - echo "Release ID: ${RELEASE_ID}" - - find src-tauri/target/release/bundle -type f -name "*.dmg" | while IFS= read -r file; do - filename=$(basename "$file") - encoded_name=$(echo "$filename" | sed 's/ /%20/g') - echo "Uploading ${filename} ($(du -h "$file" | cut -f1))..." - - ASSET_ID=$(curl -s -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets" | jq -r ".[] | select(.name == \"${filename}\") | .id // empty") - if [ -n "${ASSET_ID}" ]; then - curl -s -X DELETE -H "Authorization: token ${BUILD_TOKEN}" \ - "${REPO_API}/releases/${RELEASE_ID}/assets/${ASSET_ID}" - fi - - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${BUILD_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - -T "$file" \ - "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") - echo "Upload response: HTTP ${HTTP_CODE}" - done diff --git a/.gitea/workflows/sidecar-release.yml b/.gitea/workflows/sidecar-release.yml new file mode 100644 index 0000000..b4c846f --- /dev/null +++ b/.gitea/workflows/sidecar-release.yml @@ -0,0 +1,109 @@ +name: Sidecar Release + +on: + push: + branches: [main] + paths: + - 'client/**' + - 'server/**' + - 'backend/**' + - 'pyproject.toml' + - 'local-transcription-headless.spec' + workflow_dispatch: + +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 }} + has_changes: ${{ steps.check_changes.outputs.has_changes }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check for backend changes + id: check_changes + run: | + # If triggered by workflow_dispatch, always build + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + exit 0 + fi + # Check if relevant files changed in this commit + CHANGED=$(git diff --name-only HEAD~1 HEAD -- client/ server/ backend/ pyproject.toml local-transcription-headless.spec 2>/dev/null || echo "") + if [ -n "$CHANGED" ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "Backend changes detected: $CHANGED" + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "No backend changes detected, skipping sidecar build" + fi + + - name: Configure git + if: steps.check_changes.outputs.has_changes == 'true' + run: | + git config user.name "Gitea Actions" + git config user.email "actions@gitea.local" + + - name: Bump sidecar patch version + if: steps.check_changes.outputs.has_changes == 'true' + id: bump + run: | + # Read current version from pyproject.toml + CURRENT=$(grep '^version = ' 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 pyproject.toml + sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml + + # Update version.py + sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py + sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py + + echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT + echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT + + - name: Commit and tag + if: steps.check_changes.outputs.has_changes == 'true' + env: + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + NEW_VERSION="${{ steps.bump.outputs.version }}" + TAG="${{ steps.bump.outputs.tag }}" + git add pyproject.toml version.py + git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]" + git tag "${TAG}" + + REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|") + git pull --rebase "${REMOTE_URL}" main || true + git push "${REMOTE_URL}" HEAD:main + git push "${REMOTE_URL}" "${TAG}" + + - name: Create Gitea release + if: steps.check_changes.outputs.has_changes == 'true' + 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}" diff --git a/CLAUDE.md b/CLAUDE.md index f7b0683..b4b7a59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,8 +64,14 @@ local-transcription/ │ ├── web_display.py # FastAPI OBS display server (WebSocket + HTML) │ └── nodejs/ # Optional multi-user sync server ├── .gitea/workflows/ # CI/CD -│ ├── release.yml # Tauri app builds (Linux/Windows/macOS) -│ └── build-sidecar.yml # Python sidecar builds (CUDA + CPU) +│ ├── release.yml # Coordinator: version bump, tag, release creation +│ ├── build-app-linux.yml # Linux Tauri app build (triggered by v* tag) +│ ├── build-app-windows.yml # Windows Tauri app build (triggered by v* tag) +│ ├── build-app-macos.yml # macOS Tauri app build (triggered by v* tag) +│ ├── sidecar-release.yml # Sidecar coordinator: version bump, tag, release +│ ├── build-sidecar-linux.yml # Linux sidecar build (triggered by sidecar-v* tag) +│ ├── build-sidecar-windows.yml # Windows sidecar build (triggered by sidecar-v* tag) +│ └── build-sidecar-macos.yml # macOS sidecar build (triggered by sidecar-v* tag) ├── config/default_config.yaml # Default settings template ├── main.py # Legacy PySide6 GUI entry point ├── main_cli.py # CLI version for testing @@ -205,12 +211,21 @@ Uses Svelte 5 runes throughout (`$state`, `$derived`, `$effect`, `$props`). No S ## CI/CD -Two Gitea Actions workflows in `.gitea/workflows/`: +Eight Gitea Actions workflows in `.gitea/workflows/`, split into coordinators and per-OS builders: -- **`release.yml`**: Triggers on push to `main`. Auto-bumps version, builds Tauri app on Linux/Windows/macOS, uploads `.deb`, `.rpm`, `.msi`, `.dmg` to Gitea release. -- **`build-sidecar.yml`**: Triggers on changes to `client/`, `server/`, `backend/`, `pyproject.toml`. Builds headless Python sidecar via PyInstaller. CUDA + CPU for Linux/Windows, CPU-only for macOS. +**App release (Tauri):** +- **`release.yml`**: Coordinator. Triggers on push to `main`. Auto-bumps version in package.json/tauri.conf.json/Cargo.toml/version.py, commits, tags `v{VERSION}`, creates Gitea release. +- **`build-app-linux.yml`**: Triggers on `v*` tag push or `workflow_dispatch`. Builds Tauri app, uploads `.deb`/`.rpm`/`.AppImage`. +- **`build-app-windows.yml`**: Triggers on `v*` tag push or `workflow_dispatch`. Builds Tauri app, uploads `.msi`/`*-setup.exe`. +- **`build-app-macos.yml`**: Triggers on `v*` tag push or `workflow_dispatch`. Builds Tauri app, uploads `.dmg`. -Both require a `BUILD_TOKEN` secret (Gitea API token with release write access). +**Sidecar release (Python backend):** +- **`sidecar-release.yml`**: Coordinator. Triggers on push to `main` with changes in `client/`, `server/`, `backend/`, `pyproject.toml`, or `local-transcription-headless.spec`. Bumps version in pyproject.toml/version.py, tags `sidecar-v{VERSION}`, creates Gitea release. +- **`build-sidecar-linux.yml`**: Triggers on `sidecar-v*` tag push or `workflow_dispatch`. Builds CUDA + CPU sidecars via PyInstaller. +- **`build-sidecar-windows.yml`**: Triggers on `sidecar-v*` tag push or `workflow_dispatch`. Builds CUDA + CPU sidecars via PyInstaller. +- **`build-sidecar-macos.yml`**: Triggers on `sidecar-v*` tag push or `workflow_dispatch`. Builds CPU-only sidecar via PyInstaller. + +All per-OS build workflows can be re-run independently via `workflow_dispatch` with an optional `tag` input. All require a `BUILD_TOKEN` secret (Gitea API token with release write access). ## Common Patterns