Skip to content

fix: resolve shell interpretation errors in release workflow #29

fix: resolve shell interpretation errors in release workflow

fix: resolve shell interpretation errors in release workflow #29

Workflow file for this run

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'
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 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
pip install pyinstaller==6.11.1
- name: Build Python backend with PyInstaller
run: |
cd backend
source .venv/bin/activate
python -m PyInstaller backend_server.spec
- name: Install frontend requirements
run: |
cd frontend
npm install
- 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: ''
run: |
cd frontend
npm run tauri build
- 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 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
pip install pyinstaller
# 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
npm install
# 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 npm install from frontend folder covers this
# Build the final application
- name: Build Tauri application
env:
# Pass the Base64 secret DIRECTLY here.
# Tauri will handle the decoding automatically.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
run: |
cd frontend
npm run tauri build
# 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.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
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_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
- 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
# Upload Windows artifacts
- name: Upload MSI installer
if: steps.find_files.outputs.msi_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.msi_path }}
asset_name: Local_Lens_${{ github.ref_name }}_x64_en-US.msi
asset_content_type: application/x-msi
- name: Upload EXE installer
if: steps.find_files.outputs.exe_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.exe_path }}
asset_name: Local_Lens_${{ github.ref_name }}_x64-setup.exe
asset_content_type: application/x-msdownload
- name: Upload Windows checksums
if: steps.find_files.outputs.checksums_win_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.checksums_win_path }}
asset_name: checksums-windows.txt
asset_content_type: text/plain
# Upload macOS artifacts
- name: Upload DMG
if: steps.find_files.outputs.dmg_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.dmg_path }}
asset_name: Local_Lens_${{ github.ref_name }}_aarch64.dmg
asset_content_type: application/x-apple-diskimage
- name: Upload macOS app tarball (for updater)
if: steps.find_files.outputs.tarball_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.tarball_path }}
asset_name: Local_Lens.app.tar.gz
asset_content_type: application/gzip
- name: Upload macOS checksums
if: steps.find_files.outputs.checksums_mac_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.checksums_mac_path }}
asset_name: checksums-macos.txt
asset_content_type: text/plain
- name: Upload macOS fix script
if: steps.find_files.outputs.fix_script_path
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_files.outputs.fix_script_path }}
asset_name: Fix_Local_Lens.command
asset_content_type: application/x-sh
- name: Upload Homebrew Cask formula
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: local-lens.rb
asset_name: local-lens.rb
asset_content_type: text/x-ruby
# Upload latest.json for Tauri auto-updater
- name: Upload latest.json
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: latest.json
asset_name: latest.json
asset_content_type: application/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"