ci: rename Tauri artifacts to URL-safe Local_Lens_v{version} pattern #46
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Build | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g., v1.0.0)' | |
| required: true | |
| default: 'v1.0.0' | |
| permissions: | |
| contents: write | |
| env: | |
| PYTHON_VERSION: '3.11' | |
| NODE_VERSION: '18' | |
| jobs: | |
| # ============================================================================ | |
| # macOS Build (Apple Silicon) | |
| # ============================================================================ | |
| build-macos: | |
| runs-on: macos-14 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install system dependencies (Homebrew) | |
| run: | | |
| brew install cmake dlib imagemagick | |
| - name: Setup Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin | |
| - name: Create Python virtual environment | |
| run: | | |
| cd backend | |
| python -m venv .venv | |
| - name: Install backend requirements | |
| run: | | |
| cd backend | |
| source .venv/bin/activate | |
| python -m pip install --upgrade pip setuptools wheel | |
| pip install -r requirements.txt | |
| - name: Build Python backend with PyInstaller | |
| run: | | |
| cd backend | |
| source .venv/bin/activate | |
| python -m PyInstaller backend_server.spec | |
| - name: Smoke test backend startup (macOS) | |
| run: | | |
| cd backend | |
| source .venv/bin/activate | |
| ./dist/backend_server/backend_server > backend-smoke.log 2>&1 & | |
| backend_pid=$! | |
| sleep 10 | |
| echo "=== backend smoke log ===" | |
| cat backend-smoke.log | |
| if grep -E "(ModuleNotFoundError|ImportError|Failed to execute script)" backend-smoke.log; then | |
| echo "Backend smoke test failed: startup import error detected" | |
| kill "$backend_pid" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| if ! kill -0 "$backend_pid" 2>/dev/null; then | |
| echo "Backend smoke test failed: backend process exited early" | |
| exit 1 | |
| fi | |
| echo "Backend smoke test passed: process is running without import errors" | |
| kill "$backend_pid" 2>/dev/null || true | |
| - name: Install frontend requirements | |
| run: | | |
| cd frontend | |
| npx rimraf node_modules | |
| pnpm install --force | |
| - name: Copy backend bundle for Tauri | |
| run: | | |
| cd frontend | |
| node ensure-backend.js | |
| - name: Build Tauri application | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| cd frontend | |
| pnpm run tauri build | |
| # Tauri derives filenames from productName ("Local Lens") which produces | |
| # spaces in the filenames. Rename to a URL-safe, predictable pattern so | |
| # Homebrew cask URLs and Tauri updater latest.json never break. | |
| - name: Normalize macOS artifact filenames | |
| run: | | |
| tag="${{ github.ref_name }}" # e.g. v2.4.1 | |
| cd frontend/src-tauri/target/release/bundle | |
| # Rename DMG | |
| DMG=$(find dmg -name "*.dmg" 2>/dev/null | head -1) | |
| if [ -n "$DMG" ]; then | |
| mv "$DMG" "dmg/Local_Lens_${tag}_aarch64.dmg" | |
| echo "Renamed DMG → Local_Lens_${tag}_aarch64.dmg" | |
| fi | |
| # Rename updater tarball | |
| TARBALL=$(find macos -name "*.app.tar.gz" ! -name "*.sig" 2>/dev/null | head -1) | |
| if [ -n "$TARBALL" ]; then | |
| mv "$TARBALL" "macos/Local_Lens_${tag}_aarch64.app.tar.gz" | |
| echo "Renamed tarball → Local_Lens_${tag}_aarch64.app.tar.gz" | |
| fi | |
| # Rename updater signature | |
| SIG=$(find macos -name "*.app.tar.gz.sig" 2>/dev/null | head -1) | |
| if [ -n "$SIG" ]; then | |
| mv "$SIG" "macos/Local_Lens_${tag}_aarch64.app.tar.gz.sig" | |
| echo "Renamed sig → Local_Lens_${tag}_aarch64.app.tar.gz.sig" | |
| fi | |
| - name: Find macOS build artifacts | |
| id: find_files | |
| run: | | |
| cd frontend/src-tauri/target/release/bundle | |
| # Find the DMG file | |
| DMG_FILE=$(find dmg -name "*.dmg" 2>/dev/null | head -1) | |
| if [ -n "$DMG_FILE" ]; then | |
| DMG_PATH="$(pwd)/$DMG_FILE" | |
| DMG_NAME=$(basename "$DMG_FILE") | |
| echo "dmg_path=$DMG_PATH" >> $GITHUB_OUTPUT | |
| echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT | |
| echo "Found DMG: $DMG_PATH" | |
| fi | |
| # Find the .app.tar.gz file (for updater) | |
| TARBALL=$(find macos -name "*.app.tar.gz" 2>/dev/null | head -1) | |
| if [ -n "$TARBALL" ]; then | |
| TARBALL_PATH="$(pwd)/$TARBALL" | |
| TARBALL_NAME=$(basename "$TARBALL") | |
| echo "tarball_path=$TARBALL_PATH" >> $GITHUB_OUTPUT | |
| echo "tarball_name=$TARBALL_NAME" >> $GITHUB_OUTPUT | |
| echo "Found tarball: $TARBALL_PATH" | |
| fi | |
| # Find the signature file | |
| SIG_FILE=$(find macos -name "*.app.tar.gz.sig" 2>/dev/null | head -1) | |
| if [ -n "$SIG_FILE" ]; then | |
| SIG_PATH="$(pwd)/$SIG_FILE" | |
| SIGNATURE=$(cat "$SIG_PATH") | |
| echo "sig_path=$SIG_PATH" >> $GITHUB_OUTPUT | |
| echo "signature=$SIGNATURE" >> $GITHUB_OUTPUT | |
| echo "Found signature: $SIG_PATH" | |
| fi | |
| - name: Generate macOS checksums | |
| run: | | |
| cd frontend/src-tauri/target/release/bundle | |
| # Generate checksums for DMG (in dmg folder so artifact upload finds it) | |
| if [ -f "${{ steps.find_files.outputs.dmg_path }}" ]; then | |
| shasum -a 256 "${{ steps.find_files.outputs.dmg_path }}" | awk '{print $1 " " FILENAME}' FILENAME="${{ steps.find_files.outputs.dmg_name }}" > dmg/checksums-macos.txt | |
| echo "Generated checksums:" | |
| cat dmg/checksums-macos.txt | |
| else | |
| echo "No DMG found for checksums" | |
| fi | |
| - name: Create macOS Gatekeeper fix script | |
| run: | | |
| cat > "Fix_Local_Lens.command" << 'SCRIPT' | |
| #!/bin/bash | |
| # Local Lens - macOS Gatekeeper Fix | |
| # Double-click this file after copying Local Lens to Applications | |
| echo "" | |
| echo "🔧 Local Lens - Fixing macOS Security Block" | |
| echo "============================================" | |
| echo "" | |
| APP_PATH="/Applications/Local Lens.app" | |
| USER_APP_PATH="$HOME/Applications/Local Lens.app" | |
| if [ -d "$APP_PATH" ]; then | |
| TARGET="$APP_PATH" | |
| echo "Found app in /Applications" | |
| elif [ -d "$USER_APP_PATH" ]; then | |
| TARGET="$USER_APP_PATH" | |
| echo "Found app in ~/Applications" | |
| else | |
| echo "❌ Local Lens not found in Applications!" | |
| echo "" | |
| echo "Please:" | |
| echo "1. Drag 'Local Lens.app' to your Applications folder first" | |
| echo "2. Then run this script again" | |
| echo "" | |
| read -p "Press Enter to close..." | |
| exit 1 | |
| fi | |
| echo "Fixing: $TARGET" | |
| echo "" | |
| echo "Step 1/3: Removing quarantine flag..." | |
| xattr -cr "$TARGET" | |
| echo "Step 2/3: Applying ad-hoc signature..." | |
| codesign --force --deep --sign - "$TARGET" 2>/dev/null || echo " (signature step skipped)" | |
| echo "Step 3/3: Setting executable permissions..." | |
| chmod -R +x "$TARGET/Contents/MacOS/" | |
| chmod -R +x "$TARGET/Contents/Resources/backend_server_bundle/" 2>/dev/null || true | |
| echo "" | |
| echo "✅ Done! You can now open Local Lens normally." | |
| echo "" | |
| read -p "Press Enter to close..." | |
| SCRIPT | |
| chmod +x "Fix_Local_Lens.command" | |
| echo "Created Fix_Local_Lens.command" | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-artifacts | |
| path: | | |
| frontend/src-tauri/target/release/bundle/dmg/*.dmg | |
| frontend/src-tauri/target/release/bundle/dmg/checksums-macos.txt | |
| frontend/src-tauri/target/release/bundle/macos/*.app.tar.gz | |
| frontend/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig | |
| Fix_Local_Lens.command | |
| retention-days: 5 | |
| # ============================================================================ | |
| # Windows Build | |
| # ============================================================================ | |
| build-windows: | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| # Backend: Create virtual environment and install requirements | |
| - name: Create Python virtual environment | |
| run: | | |
| cd backend | |
| python -m venv venv | |
| - name: Activate venv and install backend requirements | |
| run: | | |
| cd backend | |
| .\venv\Scripts\activate | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| # Backend: Build with PyInstaller using your spec file | |
| - name: Build Python backend with PyInstaller | |
| run: | | |
| cd backend | |
| .\venv\Scripts\activate | |
| python -m PyInstaller backend_server.spec | |
| # Frontend: Install requirements in src-tauri folder | |
| - name: Install frontend requirements | |
| run: | | |
| cd frontend | |
| npx rimraf node_modules | |
| pnpm install --force | |
| # Run ensure-backend.js to copy and rename the backend executable | |
| - name: Copy and rename backend executable | |
| run: | | |
| cd frontend | |
| node ensure-backend.js | |
| - name: Install Tauri requirements | |
| run: | | |
| cd frontend/src-tauri | |
| # Install any additional Tauri-specific requirements if needed | |
| # Currently using pnpm install from frontend folder covers this | |
| # Build the final application | |
| - name: Build Tauri application | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| cd frontend | |
| pnpm run tauri build | |
| # Rename Windows artifacts to URL-safe pattern matching the macOS convention | |
| - name: Normalize Windows artifact filenames | |
| run: | | |
| $tag = "${{ github.ref_name }}" # e.g. v2.4.1 | |
| $bundlePath = "frontend\src-tauri\target\release\bundle" | |
| # Rename MSI | |
| $msi = Get-ChildItem -Path "$bundlePath\msi" -Filter "*.msi" | Where-Object { $_.Name -notlike "*.sig" } | Select-Object -First 1 | |
| if ($msi) { | |
| $newName = "Local_Lens_${tag}_x64_en-US.msi" | |
| Rename-Item $msi.FullName $newName | |
| Write-Output "Renamed MSI -> $newName" | |
| } | |
| # Rename MSI signature | |
| $sig = Get-ChildItem -Path "$bundlePath\msi" -Filter "*.msi.sig" | Select-Object -First 1 | |
| if ($sig) { | |
| $newName = "Local_Lens_${tag}_x64_en-US.msi.sig" | |
| Rename-Item $sig.FullName $newName | |
| Write-Output "Renamed MSI sig -> $newName" | |
| } | |
| # Generate checksums for verification | |
| - name: Generate checksums | |
| run: | | |
| cd frontend\src-tauri\target\release\bundle | |
| $files = Get-ChildItem -Recurse -Include "*.msi", "*.exe" | Where-Object { $_.Directory.Name -eq "msi" -or $_.Directory.Name -eq "nsis" } | |
| $checksums = @() | |
| foreach ($file in $files) { | |
| $hash = Get-FileHash -Path $file.FullName -Algorithm SHA256 | |
| $checksums += "$($hash.Hash.ToLower()) $($file.Name)" | |
| } | |
| $checksums | Out-File -FilePath "checksums-windows.txt" -Encoding utf8 | |
| Write-Output "Generated checksums:" | |
| Get-Content "checksums-windows.txt" | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-artifacts | |
| path: | | |
| frontend/src-tauri/target/release/bundle/msi/*.msi | |
| frontend/src-tauri/target/release/bundle/msi/*.msi.sig | |
| frontend/src-tauri/target/release/bundle/nsis/*.exe | |
| frontend/src-tauri/target/release/bundle/nsis/*.exe.sig | |
| frontend/src-tauri/target/release/bundle/checksums-windows.txt | |
| retention-days: 5 | |
| - name: Build Summary | |
| run: | | |
| Write-Output "✅ Windows build completed successfully!" | |
| Write-Output "📦 Artifacts uploaded for release job" | |
| # ============================================================================ | |
| # Create GitHub Release (after both builds complete) | |
| # ============================================================================ | |
| create-release: | |
| runs-on: ubuntu-latest | |
| needs: [build-windows, build-macos] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download Windows artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: windows-artifacts | |
| path: artifacts/windows | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: macos-artifacts | |
| path: artifacts/macos | |
| - name: List downloaded artifacts | |
| run: | | |
| echo "=== Windows Artifacts ===" | |
| find artifacts/windows -type f | |
| echo "" | |
| echo "=== macOS Artifacts ===" | |
| find artifacts/macos -type f | |
| - name: Get version | |
| id: get_version | |
| run: | | |
| tag="${{ github.ref_name }}" | |
| version="${tag#v}" | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| echo "Version: $version" | |
| - name: Extract release notes | |
| id: release_notes | |
| run: | | |
| version="${{ steps.get_version.outputs.version }}" | |
| changelog_path="CHANGELOG.md" | |
| if [ -f "$changelog_path" ]; then | |
| # Extract notes for this version | |
| notes=$(awk "/^## \[$version\]/{flag=1; next} /^## \[/{flag=0} flag" "$changelog_path" | sed '/^$/d') | |
| if [ -n "$notes" ]; then | |
| echo "Found release notes for version $version" | |
| # Use delimiter for multiline output | |
| echo "notes<<EOF" >> $GITHUB_OUTPUT | |
| echo "$notes" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| else | |
| echo "No specific notes found, using default" | |
| echo "notes=Release $version - See CHANGELOG.md for details" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "CHANGELOG.md not found, using default notes" | |
| echo "notes=Release $version" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Find artifact files | |
| id: find_files | |
| run: | | |
| # Windows artifacts | |
| MSI_FILE=$(find artifacts/windows -name "*.msi" ! -name "*.sig" 2>/dev/null | head -1) | |
| MSI_SIG=$(find artifacts/windows -name "*.msi.sig" 2>/dev/null | head -1) | |
| EXE_FILE=$(find artifacts/windows -name "*.exe" ! -name "*.sig" 2>/dev/null | head -1) | |
| CHECKSUMS_WIN=$(find artifacts/windows -name "checksums-windows.txt" 2>/dev/null | head -1) | |
| # macOS artifacts | |
| DMG_FILE=$(find artifacts/macos -name "*.dmg" 2>/dev/null | head -1) | |
| TARBALL=$(find artifacts/macos -name "*.app.tar.gz" 2>/dev/null | head -1) | |
| TARBALL_SIG=$(find artifacts/macos -name "*.app.tar.gz.sig" 2>/dev/null | head -1) | |
| CHECKSUMS_MAC=$(find artifacts/macos -name "checksums-macos.txt" 2>/dev/null | head -1) | |
| FIX_SCRIPT=$(find artifacts/macos -name "Fix_Local_Lens.command" 2>/dev/null | head -1) | |
| echo "msi_path=$MSI_FILE" >> $GITHUB_OUTPUT | |
| echo "msi_sig_path=$MSI_SIG" >> $GITHUB_OUTPUT | |
| echo "exe_path=$EXE_FILE" >> $GITHUB_OUTPUT | |
| echo "checksums_win_path=$CHECKSUMS_WIN" >> $GITHUB_OUTPUT | |
| echo "dmg_path=$DMG_FILE" >> $GITHUB_OUTPUT | |
| echo "tarball_path=$TARBALL" >> $GITHUB_OUTPUT | |
| echo "tarball_sig_path=$TARBALL_SIG" >> $GITHUB_OUTPUT | |
| echo "checksums_mac_path=$CHECKSUMS_MAC" >> $GITHUB_OUTPUT | |
| echo "fix_script_path=$FIX_SCRIPT" >> $GITHUB_OUTPUT | |
| # Get DMG SHA256 for Homebrew cask | |
| if [ -n "$DMG_FILE" ] && [ -f "$DMG_FILE" ]; then | |
| DMG_SHA256=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') | |
| echo "dmg_sha256=$DMG_SHA256" >> $GITHUB_OUTPUT | |
| echo "DMG SHA256: $DMG_SHA256" | |
| fi | |
| # Read signatures for latest.json | |
| if [ -n "$MSI_SIG" ] && [ -f "$MSI_SIG" ]; then | |
| WIN_SIG=$(cat "$MSI_SIG") | |
| echo "win_signature=$WIN_SIG" >> $GITHUB_OUTPUT | |
| fi | |
| if [ -n "$TARBALL_SIG" ] && [ -f "$TARBALL_SIG" ]; then | |
| MAC_SIG=$(cat "$TARBALL_SIG") | |
| echo "mac_signature=$MAC_SIG" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Generate latest.json for Tauri updater | |
| run: | | |
| version="${{ steps.get_version.outputs.version }}" | |
| tag="${{ github.ref_name }}" | |
| repo="${{ github.repository }}" | |
| pub_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| win_sig="${{ steps.find_files.outputs.win_signature }}" | |
| mac_sig="${{ steps.find_files.outputs.mac_signature }}" | |
| # Get release notes using heredoc to avoid backtick interpretation | |
| cat > /tmp/notes.txt << 'NOTES_EOF' | |
| ${{ steps.release_notes.outputs.notes }} | |
| NOTES_EOF | |
| notes=$(cat /tmp/notes.txt) | |
| # Use jq to generate valid JSON with proper escaping | |
| jq -n \ | |
| --arg version "$version" \ | |
| --arg notes "$notes" \ | |
| --arg pub_date "$pub_date" \ | |
| --arg win_sig "$win_sig" \ | |
| --arg win_url "https://github.com/$repo/releases/download/$tag/Local_Lens_${tag}_x64_en-US.msi" \ | |
| --arg mac_sig "$mac_sig" \ | |
| --arg mac_url "https://github.com/$repo/releases/download/$tag/Local_Lens_${tag}_aarch64.app.tar.gz" \ | |
| '{ | |
| version: $version, | |
| notes: $notes, | |
| pub_date: $pub_date, | |
| platforms: { | |
| "windows-x86_64": { | |
| signature: $win_sig, | |
| url: $win_url | |
| }, | |
| "darwin-aarch64": { | |
| signature: $mac_sig, | |
| url: $mac_url | |
| } | |
| } | |
| }' > latest.json | |
| echo "Generated latest.json:" | |
| cat latest.json | |
| - name: Generate Homebrew Cask formula | |
| run: | | |
| version="${{ steps.get_version.outputs.version }}" | |
| sha256="${{ steps.find_files.outputs.dmg_sha256 }}" | |
| cat > local-lens.rb << EOF | |
| cask "local-lens" do | |
| version "$version" | |
| sha256 "$sha256" | |
| url "https://github.com/${{ github.repository }}/releases/download/v#{version}/Local_Lens_v#{version}_aarch64.dmg", | |
| verified: "github.com/${{ github.repository }}/" | |
| name "Local Lens" | |
| desc "AI-powered offline photo organizer with face recognition" | |
| homepage "https://github.com/${{ github.repository }}" | |
| livecheck do | |
| url :url | |
| strategy :github_latest | |
| end | |
| auto_updates true | |
| depends_on arch: :arm64 | |
| app "Local Lens.app" | |
| postflight do | |
| system_command "/usr/bin/xattr", | |
| args: ["-cr", "#{appdir}/Local Lens.app"], | |
| sudo: false | |
| end | |
| zap trash: [ | |
| "~/.config/LocalLens", | |
| ] | |
| caveats <<~EOS | |
| Local Lens is not notarized by Apple. | |
| If you see a security warning, right-click the app → Open → Click "Open" | |
| EOS | |
| end | |
| EOF | |
| echo "Generated Homebrew Cask formula:" | |
| cat local-lens.rb | |
| - name: Create Release and Upload Assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: Local Lens ${{ github.ref_name }} | |
| draft: true | |
| prerelease: false | |
| body: | | |
| ## Local Lens ${{ github.ref_name }} | |
| ${{ steps.release_notes.outputs.notes }} | |
| --- | |
| ### Installation | |
| #### macOS via Homebrew (Recommended) | |
| brew install ashesbloom/locallens/local-lens | |
| Homebrew automatically handles Gatekeeper - no extra steps needed! | |
| #### Windows | |
| 1. Download the .msi (enterprise) or .exe (individual) installer | |
| 2. Run the installer | |
| 3. Launch Local Lens from the Start Menu | |
| #### macOS Manual Install (DMG) | |
| 1. Download the DMG file from this release | |
| 2. Open the DMG and drag Local Lens to Applications | |
| 3. Fix Gatekeeper block (choose one): | |
| - Download and double-click Fix_Local_Lens.command | |
| - If blocked: run `bash ~/Downloads/Fix_Local_Lens.command` | |
| - Right-click the app, Open, click Open in the dialog | |
| - Terminal: xattr -cr "/Applications/Local Lens.app" | |
| --- | |
| ### macOS Security Note | |
| Local Lens is not notarized with Apple (requires $99/year developer fee). | |
| macOS may show a damaged or unidentified developer warning. | |
| This is normal for open-source apps. | |
| The Fix_Local_Lens.command script automatically removes quarantine, | |
| applies an ad-hoc signature, and sets permissions. | |
| --- | |
| ### Security and Verification | |
| - Source Code: Available at this tag | |
| - Build Process: See Actions tab for build logs | |
| - Checksums: SHA256 checksums provided for verification | |
| files: | | |
| ${{ steps.find_files.outputs.msi_path }} | |
| ${{ steps.find_files.outputs.exe_path }} | |
| ${{ steps.find_files.outputs.checksums_win_path }} | |
| ${{ steps.find_files.outputs.dmg_path }} | |
| ${{ steps.find_files.outputs.tarball_path }} | |
| ${{ steps.find_files.outputs.checksums_mac_path }} | |
| ${{ steps.find_files.outputs.fix_script_path }} | |
| local-lens.rb | |
| latest.json | |
| - name: Release Summary | |
| run: | | |
| echo "✅ Release created successfully!" | |
| echo "📦 Uploaded artifacts:" | |
| echo " Windows: MSI, EXE, checksums" | |
| echo " macOS: DMG, app tarball, checksums, Fix_Local_Lens.command" | |
| echo "📋 latest.json generated for auto-updater (Windows + macOS)" | |
| echo "🍺 local-lens.rb generated for Homebrew tap" | |
| echo "🔒 Release created as draft - review and publish manually" |