Repository navigation
fix(mac): use single-file EXE instead of BUNDLE for macOS build #3
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
| # ============================================================================= | |
| # Build panelagent desktop bundles on tag push and attach to a GitHub Release. | |
| # ============================================================================= | |
| # Trigger: push a tag matching v* (e.g. v1.0.0). Two jobs run in parallel: | |
| # - build-windows: produces dist/panelagent.exe | |
| # - build-macos: produces PanelAgent-mac-arm64.zip (contains PanelAgent.app) | |
| # Both upload to the SAME release for that tag, so users pick their platform | |
| # from one page. Manual dispatch also supported from the Actions tab. | |
| # ============================================================================= | |
| name: Build desktop bundles | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write # required to create the release and upload assets | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Windows (x86_64) — single-file panelagent.exe | |
| # --------------------------------------------------------------------------- | |
| build-windows: | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install backend deps + PyInstaller | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r backend/requirements.txt | |
| pip install pyinstaller | |
| - name: Build static frontend (export mode) | |
| env: | |
| PANELAGENT_STATIC_EXPORT: "1" | |
| NEXT_PUBLIC_API_URL: "http://127.0.0.1:8000" | |
| run: | | |
| cd frontend | |
| npm install | |
| Remove-Item -Recurse -Force "src/app/api" -ErrorAction SilentlyContinue | |
| npm run build | |
| if (-Not (Test-Path "out/index.html")) { throw "next build did not produce out/index.html" } | |
| shell: pwsh | |
| - name: Build exe | |
| run: | | |
| python -m PyInstaller panelagent.spec --noconfirm --clean | |
| if (-Not (Test-Path "dist/panelagent.exe")) { throw "PyInstaller did not produce dist/panelagent.exe" } | |
| shell: pwsh | |
| - name: Smoke test (start exe + poll health + dump log + kill) | |
| continue-on-error: true | |
| run: | | |
| Write-Host "Launching dist\panelagent.exe..." | |
| $p = Start-Process -FilePath "dist\panelagent.exe" -PassThru | |
| $ok = $false | |
| for ($i = 0; $i -lt 24; $i++) { | |
| Start-Sleep -Seconds 5 | |
| try { | |
| $resp = Invoke-WebRequest -Uri "http://127.0.0.1:8000/api/v1/health" -UseBasicParsing -TimeoutSec 5 | |
| if ($resp.StatusCode -eq 200) { | |
| Write-Host "Health check OK after $($i*5)s: $($resp.Content)" | |
| $ok = $true | |
| break | |
| } | |
| } catch { | |
| Write-Host "Waiting... ($($_.Exception.Message.Split([Environment]::NewLine)[0]))" | |
| } | |
| } | |
| Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue | |
| Start-Sleep -Seconds 2 | |
| $logPath = "$env:USERPROFILE\.panelagent\launcher.log" | |
| Write-Host "Launcher log: $logPath" | |
| if (Test-Path $logPath) { | |
| Write-Host "===== launcher.log =====" | |
| Get-Content $logPath | ForEach-Object { Write-Host $_ } | |
| Write-Host "===== end launcher.log =====" | |
| } else { | |
| Write-Host "No launcher.log found (exe may have crashed before logging was set up)." | |
| } | |
| if (-not $ok) { | |
| Write-Host "::warning::Smoke test could not reach health endpoint within 120s. See launcher.log above. The exe will still be uploaded." | |
| } | |
| shell: pwsh | |
| - name: Upload exe to the release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: PanelAgent ${{ github.ref_name }} | |
| files: dist/panelagent.exe | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| # --------------------------------------------------------------------------- | |
| # macOS (Apple Silicon / arm64) — PanelAgent.app | |
| # --------------------------------------------------------------------------- | |
| build-macos: | |
| runs-on: macos-14 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install backend deps + PyInstaller | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r backend/requirements.txt | |
| pip install pyinstaller | |
| - name: Build static frontend (export mode) | |
| env: | |
| PANELAGENT_STATIC_EXPORT: "1" | |
| NEXT_PUBLIC_API_URL: "http://127.0.0.1:8000" | |
| run: | | |
| cd frontend | |
| npm install | |
| # Proxy route handler is incompatible with output: export; remove | |
| # it from this build working copy only (kept in repo for Linux). | |
| rm -rf src/app/api | |
| npm run build | |
| test -f out/index.html || { echo "next build did not produce out/index.html"; exit 1; } | |
| - name: Build macOS binary | |
| run: | | |
| python -m PyInstaller panelagent-mac.spec --noconfirm --clean | |
| echo "=== dist/ contents ===" | |
| ls -la dist/ | |
| test -f dist/PanelAgent || { echo "PyInstaller did not produce dist/PanelAgent"; exit 1; } | |
| echo "APP_PATH=dist/PanelAgent" >> "$GITHUB_ENV" | |
| - name: Smoke test (launch binary + poll health + dump log + kill) | |
| continue-on-error: true | |
| run: | | |
| exe="${APP_PATH}" | |
| echo "Launching $exe..." | |
| chmod +x "$exe" | |
| "$exe" & | |
| pid=$! | |
| ok=false | |
| for i in $(seq 1 24); do | |
| sleep 5 | |
| if curl -sf http://127.0.0.1:8000/api/v1/health >/dev/null 2>&1; then | |
| echo "Health check OK after $((i*5))s" | |
| ok=true | |
| break | |
| fi | |
| echo "Waiting..." | |
| done | |
| kill $pid 2>/dev/null || true | |
| pkill -f "PanelAgent" 2>/dev/null || true | |
| sleep 2 | |
| log="$HOME/.panelagent/launcher.log" | |
| echo "Launcher log: $log" | |
| if [ -f "$log" ]; then | |
| echo "===== launcher.log =====" | |
| cat "$log" | |
| echo "===== end launcher.log =====" | |
| else | |
| echo "No launcher.log found." | |
| fi | |
| if [ "$ok" = "false" ]; then | |
| echo "::warning::Smoke test could not reach health endpoint within 120s. The .app will still be uploaded." | |
| fi | |
| - name: Zip binary for upload | |
| run: | | |
| cd dist | |
| # ditto preserves macOS permissions (executable bit) better than zip. | |
| ditto -c -k --keepParent PanelAgent PanelAgent-mac-arm64.zip | |
| test -f PanelAgent-mac-arm64.zip | |
| ls -la PanelAgent-mac-arm64.zip | |
| - name: Upload macOS zip to the release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: PanelAgent ${{ github.ref_name }} | |
| files: dist/PanelAgent-mac-arm64.zip | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true |