Build Zen Browser Portable #480
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
| # ============================================================================ | |
| # Zen Browser Portable — Build & Release Pipeline | |
| # ============================================================================ | |
| # This workflow tracks new releases from zen-browser/desktop, | |
| # downloads the Windows installer, extracts it, and creates a portable zip. | |
| # | |
| # Triggers: | |
| # - Manual (workflow_dispatch) — optional version can be specified | |
| # - Automatic (schedule) — checks for new releases every 6 hours | |
| # ============================================================================ | |
| name: Build Zen Browser Portable | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| zen_version: | |
| description: 'Zen Browser version (leave empty for latest)' | |
| required: false | |
| default: '' | |
| force_rebuild: | |
| description: 'Rebuild even if release already exists' | |
| required: false | |
| type: boolean | |
| default: false | |
| schedule: | |
| # Check for new releases every 6 hours (00:00, 06:00, 12:00, 18:00 UTC) | |
| - cron: '0 */6 * * *' | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-portable: | |
| name: Build & Release | |
| runs-on: windows-latest | |
| steps: | |
| # --------------------------------------------------------------- | |
| # 1. Checkout repository | |
| # --------------------------------------------------------------- | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| # --------------------------------------------------------------- | |
| # 2. Get latest Zen Browser release info | |
| # --------------------------------------------------------------- | |
| - name: Get Zen Browser release info | |
| id: zen_release | |
| shell: pwsh | |
| run: | | |
| $inputVersion = "${{ github.event.inputs.zen_version }}" | |
| $forceRebuild = "${{ github.event.inputs.force_rebuild }}" | |
| # Get version info from GitHub API | |
| if ($inputVersion) { | |
| Write-Host "📌 Using specified version: $inputVersion" | |
| $releaseUrl = "https://api.github.com/repos/zen-browser/desktop/releases/tags/$inputVersion" | |
| } else { | |
| Write-Host "🔍 Searching for latest version..." | |
| $releaseUrl = "https://api.github.com/repos/zen-browser/desktop/releases/latest" | |
| } | |
| try { | |
| $release = Invoke-RestMethod -Uri $releaseUrl -Headers @{ | |
| 'User-Agent' = 'Zen-Browser-Portable-CI' | |
| 'Accept' = 'application/vnd.github+json' | |
| } | |
| } catch { | |
| Write-Host "::error::Failed to fetch Zen Browser release info: $_" | |
| exit 1 | |
| } | |
| $version = $release.tag_name | |
| $releaseName = $release.name | |
| $releaseBody = $release.body | |
| Write-Host "✅ Zen Browser version: $version" | |
| Write-Host " Release name: $releaseName" | |
| echo "version=$version" >> $env:GITHUB_OUTPUT | |
| echo "release_name=$releaseName" >> $env:GITHUB_OUTPUT | |
| # Save release body to file (multi-line output) | |
| $releaseBody | Out-File -FilePath "upstream_release_notes.txt" -Encoding utf8 | |
| # Check if this version is already published | |
| $existingReleases = gh release list --limit 100 --json tagName 2>$null | ConvertFrom-Json | |
| $alreadyExists = $existingReleases | Where-Object { $_.tagName -eq "v$version" } | |
| if ($alreadyExists -and $forceRebuild -ne 'true') { | |
| Write-Host "::warning::⏭️ Release v$version already exists. Skipping." | |
| echo "skip=true" >> $env:GITHUB_OUTPUT | |
| } else { | |
| if ($alreadyExists) { | |
| Write-Host "🔄 Existing release will be deleted and recreated (force_rebuild=true)" | |
| gh release delete "v$version" --yes --cleanup-tag 2>$null | |
| } | |
| echo "skip=false" >> $env:GITHUB_OUTPUT | |
| } | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --------------------------------------------------------------- | |
| # 3. Download Windows installer | |
| # --------------------------------------------------------------- | |
| - name: Download Zen Browser installer | |
| if: steps.zen_release.outputs.skip == 'false' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.zen_release.outputs.version }}" | |
| $url = "https://github.com/zen-browser/desktop/releases/download/$version/zen.installer.exe" | |
| Write-Host "⬇️ Downloading: $url" | |
| $ProgressPreference = 'SilentlyContinue' | |
| $maxRetries = 3 | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| try { | |
| Invoke-WebRequest -Uri $url -OutFile "zen.installer.exe" -UseBasicParsing | |
| break | |
| } catch { | |
| if ($i -eq $maxRetries) { | |
| Write-Host "::error::Download failed after $maxRetries attempts: $_" | |
| exit 1 | |
| } | |
| Write-Host "⚠️ Attempt $i failed, retrying..." | |
| Start-Sleep -Seconds 5 | |
| } | |
| } | |
| $fileSize = [math]::Round((Get-Item "zen.installer.exe").Length / 1MB, 2) | |
| Write-Host "✅ Download completed: $fileSize MB" | |
| # --------------------------------------------------------------- | |
| # 4. Extract installer to app/win | |
| # --------------------------------------------------------------- | |
| - name: Extract installer to app/win | |
| if: steps.zen_release.outputs.skip == 'false' | |
| shell: pwsh | |
| run: | | |
| Write-Host "📦 Extracting installer..." | |
| # Extract NSIS installer with 7-Zip (pre-installed on windows-latest) | |
| 7z x zen.installer.exe -oextracted -y | Select-Object -Last 5 | |
| # Show extracted contents (debug) | |
| Write-Host "`n=== Extracted root contents ===" | |
| Get-ChildItem -Path "extracted" -Depth 0 | Format-Table Name, Length, PSIsContainer | |
| # Mozilla/Zen NSIS installers put browser files under 'core/' | |
| if (Test-Path "extracted/core") { | |
| $sourceDir = "extracted/core" | |
| Write-Host "✅ 'core/' directory found" | |
| } elseif (Test-Path "extracted/zen.exe") { | |
| $sourceDir = "extracted" | |
| Write-Host "✅ Files found at root" | |
| } else { | |
| Write-Host "::error::❌ Expected directory structure not found!" | |
| Write-Host "Extracted contents:" | |
| Get-ChildItem -Path "extracted" -Recurse -Depth 2 | Format-Table FullName | |
| exit 1 | |
| } | |
| # Clean and recreate target directory | |
| if (Test-Path "app/win") { | |
| Remove-Item -Path "app/win/*" -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| New-Item -Path "app/win" -ItemType Directory -Force | Out-Null | |
| # Copy browser files | |
| Copy-Item -Path "$sourceDir\*" -Destination "app\win\" -Recurse -Force | |
| Write-Host "✅ Files copied to app/win/" | |
| # Verify zen.exe exists | |
| if (Test-Path "app/win/zen.exe") { | |
| $fileCount = (Get-ChildItem -Path "app/win" -Recurse -File).Count | |
| $totalSize = [math]::Round((Get-ChildItem -Path "app/win" -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB, 2) | |
| Write-Host "✅ Verification passed: $fileCount files, total $totalSize MB" | |
| } else { | |
| Write-Host "::error::❌ zen.exe not found! app/win contents:" | |
| Get-ChildItem -Path "app/win" -Depth 1 | Format-Table FullName | |
| exit 1 | |
| } | |
| # --------------------------------------------------------------- | |
| # 5. Build PortableZen.exe with C# | |
| # --------------------------------------------------------------- | |
| - name: Build PortableZen.exe | |
| if: steps.zen_release.outputs.skip == 'false' | |
| shell: pwsh | |
| run: | | |
| Write-Host "🔨 Building PortableZen.exe..." | |
| # .NET Framework C# compiler included with Windows | |
| $cscPaths = @( | |
| "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe", | |
| "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" | |
| ) | |
| $csc = $null | |
| foreach ($p in $cscPaths) { | |
| if (Test-Path $p) { $csc = $p; break } | |
| } | |
| if (-not $csc) { | |
| Write-Host "::error::csc.exe not found!" | |
| exit 1 | |
| } | |
| Write-Host " Compiler: $csc" | |
| & $csc /nologo /out:PortableZen.exe /target:winexe /win32icon:zen-browser.ico src\launcher.cs | |
| if (Test-Path "PortableZen.exe") { | |
| $size = [math]::Round((Get-Item "PortableZen.exe").Length / 1KB, 1) | |
| Write-Host "✅ Build successful: PortableZen.exe ($size KB)" | |
| } else { | |
| Write-Host "::error::Failed to create PortableZen.exe!" | |
| exit 1 | |
| } | |
| # --------------------------------------------------------------- | |
| # 6. Create portable zip with only required files | |
| # --------------------------------------------------------------- | |
| - name: Create portable zip | |
| if: steps.zen_release.outputs.skip == 'false' | |
| id: create_zip | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.zen_release.outputs.version }}" | |
| $zipName = "Zen-Portable-v$version.zip" | |
| Write-Host "📦 Creating portable ZIP: $zipName" | |
| Write-Host " Included items:" | |
| Write-Host " ├── app/ (Zen Browser files)" | |
| Write-Host " ├── data/ (Profile skeleton)" | |
| Write-Host " ├── PortableZen.exe" | |
| Write-Host " └── zenbrowser.bat" | |
| # Create staging directory (only required files) | |
| $staging = "staging" | |
| New-Item -Path $staging -ItemType Directory -Force | Out-Null | |
| # 1) app directory (with browser files) | |
| Copy-Item -Path "app" -Destination "$staging\app" -Recurse -Force | |
| # 2) data directory (profile skeleton — .gitkeep, user.js, profile.ini) | |
| Copy-Item -Path "data" -Destination "$staging\data" -Recurse -Force | |
| # 3) PortableZen.exe | |
| Copy-Item -Path "PortableZen.exe" -Destination "$staging\PortableZen.exe" -Force | |
| # 4) zenbrowser.bat | |
| Copy-Item -Path "zenbrowser.bat" -Destination "$staging\zenbrowser.bat" -Force | |
| # Verify staging contents — only allowed items | |
| $rootItems = Get-ChildItem -Path $staging -Name | |
| $allowedItems = @('app', 'data', 'PortableZen.exe', 'zenbrowser.bat') | |
| foreach ($item in $rootItems) { | |
| if ($item -notin $allowedItems) { | |
| Write-Host "::error::❌ Disallowed item found: $item" | |
| exit 1 | |
| } | |
| } | |
| Write-Host "✅ Staging verification passed (only allowed files present)" | |
| # Compress with 7-Zip (better compression ratio) | |
| Push-Location $staging | |
| 7z a -tzip -mx=7 "..\$zipName" * | Select-Object -Last 3 | |
| Pop-Location | |
| $zipSize = [math]::Round((Get-Item $zipName).Length / 1MB, 2) | |
| Write-Host "✅ ZIP created: $zipName ($zipSize MB)" | |
| echo "zip_name=$zipName" >> $env:GITHUB_OUTPUT | |
| # --------------------------------------------------------------- | |
| # 7. Create GitHub Release and upload zip | |
| # --------------------------------------------------------------- | |
| - name: Create GitHub Release | |
| if: steps.zen_release.outputs.skip == 'false' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.zen_release.outputs.version }}" | |
| $zipName = "${{ steps.create_zip.outputs.zip_name }}" | |
| $tagName = "v$version" | |
| # Release notes | |
| $releaseNotes = @" | |
| ## 🌀 Zen Browser Portable — v$version | |
| Based on [Zen Browser v$version](https://github.com/zen-browser/desktop/releases/tag/$version). | |
| Automatically built by GitHub Actions CI/CD pipeline. | |
| --- | |
| ### 📥 Installation | |
| 1. Download ``$zipName`` from the **Assets** section below. | |
| 2. Extract the ZIP to any location (USB, desktop, etc.). | |
| 3. Launch with ``PortableZen.exe`` or ``zenbrowser.bat``. | |
| ### 📁 ZIP Contents | |
| | Item | Description | | |
| |------|-------------| | |
| | ``app/win/`` | Zen Browser application files | | |
| | ``data/`` | Profile data (portable, stays local) | | |
| | ``PortableZen.exe`` | Portable launcher (EXE) | | |
| | ``zenbrowser.bat`` | Alternative launcher (BAT) | | |
| ### 🔗 Upstream Release Notes | |
| Original changelog: [Zen Browser v$version](https://github.com/zen-browser/desktop/releases/tag/$version) | |
| --- | |
| > ⚠️ This is a community project, not an official Zen Browser distribution. | |
| "@ | |
| # Create release and upload zip | |
| gh release create $tagName $zipName ` | |
| --title "Zen Browser Portable v$version" ` | |
| --notes "$releaseNotes" ` | |
| --latest | |
| Write-Host "🎉 Release created: v$version" | |
| Write-Host " https://github.com/${{ github.repository }}/releases/tag/$tagName" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --------------------------------------------------------------- | |
| # 8. Cleanup | |
| # --------------------------------------------------------------- | |
| - name: Cleanup | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| Remove-Item -Path "zen.installer.exe" -Force -ErrorAction SilentlyContinue | |
| Remove-Item -Path "extracted" -Recurse -Force -ErrorAction SilentlyContinue | |
| Remove-Item -Path "staging" -Recurse -Force -ErrorAction SilentlyContinue | |
| Remove-Item -Path "upstream_release_notes.txt" -Force -ErrorAction SilentlyContinue | |
| Write-Host "🧹 Temporary files cleaned up" |