build(deps): bump mariadb from 12.2 to 12.3 in /doc/deploy (#1889) #205
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: Build Electron Desktop Offline Installers | |
| on: | |
| release: | |
| types: [prereleased] | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: # Allows manually triggering | |
| jobs: | |
| package: | |
| permissions: | |
| contents: write | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| # Install Bun | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| # TODO: Temporary disabled because we must have an account in snapcraft.io, | |
| # when we release the app we should edit packages.json to add | |
| # to add | |
| # "build": { | |
| # "snap": { | |
| # "grade": "stable", | |
| # "confinement": "strict", | |
| # "plugs": ["network", "home", "removable-media"], | |
| # "publish": false | |
| # }, | |
| # "linux": { | |
| # "target": [ | |
| # ... | |
| # { "target": "snap", "arch": ["x64"] } | |
| # # Install Snapcraft only in Ubuntu | |
| # - name: Install Snapcraft | |
| # if: matrix.os == 'ubuntu-latest' | |
| # run: | | |
| # sudo snap install snapcraft --classic | |
| # Extract version from tag | |
| - name: Get version from tag or commit | |
| id: get_version | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| else | |
| shortsha=$(echo $GITHUB_SHA | cut -c1-8) | |
| echo "version=v0.0.0-alpha-build$shortsha" >> $GITHUB_OUTPUT | |
| fi | |
| # Install Make on Windows | |
| - name: Install Make on Windows | |
| if: matrix.os == 'windows-latest' | |
| run: choco install make --yes | |
| shell: pwsh | |
| - name: Decode Windows certificate | |
| if: matrix.os == 'windows-latest' | |
| run: | | |
| if [ -n "${{ secrets.CERT_P12 }}" ]; then | |
| echo "${{ secrets.CERT_P12 }}" | base64 --decode > windows-cert.pfx | |
| else | |
| echo "No CERT_P12 secret set, skipping certificate decode" | |
| fi | |
| shell: bash | |
| # Use the Makefile package target | |
| - name: Package application using Makefile | |
| env: | |
| # choose publish mode depending on trigger: | |
| # - when triggered by a release.prereleased -> publish (PUBLISH=always) | |
| # - when triggered by push/merged PR -> do not publish (PUBLISH=never) | |
| # - skip publishing on forks | |
| PUBLISH: ${{ github.event_name == 'release' && github.repository_owner == 'exelearning' && 'always' || 'never' }} | |
| run: | | |
| # GitHub Releases API token for electron-builder | |
| export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" | |
| if [ "${{ github.repository_owner }}" = "exelearning" ]; then | |
| echo "Official repo detected. Enabling signing/notarization only when secrets are available." | |
| if [ -n "${{ secrets.MAC_CERT_P12 }}" ]; then | |
| export CSC_LINK="${{ secrets.MAC_CERT_P12 }}" | |
| fi | |
| if [ -n "${{ secrets.MAC_CSC_KEY_PASSWORD }}" ]; then | |
| export CSC_KEY_PASSWORD="${{ secrets.MAC_CSC_KEY_PASSWORD }}" | |
| fi | |
| if [ -n "${{ secrets.APPLE_ID }}" ]; then | |
| export APPLE_ID="${{ secrets.APPLE_ID }}" | |
| fi | |
| if [ -n "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" ]; then | |
| export APPLE_APP_SPECIFIC_PASSWORD="${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" | |
| fi | |
| if [ -n "${{ secrets.APPLE_TEAM_ID }}" ]; then | |
| export APPLE_TEAM_ID="${{ secrets.APPLE_TEAM_ID }}" | |
| fi | |
| if [ -n "${{ secrets.CERT_P12 }}" ]; then | |
| export WIN_CSC_LINK="./windows-cert.pfx" | |
| fi | |
| if [ -n "${{ secrets.CSC_KEY_PASSWORD }}" ]; then | |
| export WIN_CSC_KEY_PASSWORD="${{ secrets.CSC_KEY_PASSWORD }}" | |
| fi | |
| else | |
| echo "Fork detected. Building unsigned installers (no macOS signing/notarization)." | |
| export CSC_IDENTITY_AUTO_DISCOVERY=false | |
| export SKIP_NOTARIZE=1 | |
| unset CSC_LINK CSC_KEY_PASSWORD WIN_CSC_LINK WIN_CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID | |
| fi | |
| # Run the package command with the version | |
| make package VERSION=${{ steps.get_version.outputs.version }} PUBLISH=${{ env.PUBLISH }} | |
| shell: bash | |
| # Always upload installers as workflow artifacts so test job can use them. | |
| # If the trigger was a release and electron-builder already published, this | |
| # artifact is an extra safe copy; if it was a push/PR-merge, this is the only copy. | |
| - name: Upload installers artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: installers-${{ matrix.os }} | |
| path: | | |
| # release/*.msi | |
| release/*.exe | |
| release/*.dmg | |
| release/*.deb | |
| # release/*.rpm | |
| test-install: | |
| env: | |
| APP_BOOT_TIMEOUT: 300 # increase if needed | |
| APP_PORT: 41309 # the one used in Electron | |
| SCREENSHOT_DELAY: 20 # seconds before screenshots | |
| INSTALL_POLL_TIMEOUT: 240 # seconds to wait for install | |
| INSTALL_POLL_INTERVAL: 2 # seconds between checks | |
| needs: package | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - name: Download installers | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: installers-${{ matrix.os }} | |
| path: installers | |
| # ---------- Ubuntu test ---------- | |
| - name: Test install on Linux (.deb only) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y xvfb xdotool imagemagick curl openbox dbus-x11 || true | |
| sudo apt-get install -y --no-install-recommends \ | |
| libgtk-3-0 libnss3 libxss1 libgbm1 libxshmfence1 libdrm2 \ | |
| libatk-bridge2.0-0 libatspi2.0-0 libx11-xcb1 libxtst6 || true | |
| sudo apt-get install -y --no-install-recommends libasound2t64 || true | |
| installer=$(ls installers/*.deb 2>/dev/null || true) | |
| if [ -z "$installer" ]; then echo "No .deb installer found" >&2; exit 1; fi | |
| sudo apt-get install -y "./$installer" || { sudo apt-get -f install -y && sudo apt-get install -y "./$installer"; } | |
| # X virtual | |
| Xvfb :99 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 & | |
| export DISPLAY=:99 | |
| # Window manager so that _NET_ACTIVE_WINDOW works | |
| openbox >/tmp/openbox.log 2>&1 & | |
| sleep 1 | |
| export ELECTRON_DISABLE_SANDBOX=1 | |
| export ELECTRON_DISABLE_GPU=1 | |
| echo "Opening eXeLearning app..." | |
| app_cmd="/opt/eXeLearning/exelearning" | |
| command -v exelearning >/dev/null 2>&1 && app_cmd="$(command -v exelearning)" || true | |
| dbus-run-session -- "$app_cmd" > ubuntu-app.log 2>&1 & | |
| APP_PID=$! | |
| echo "App pid: $APP_PID" | |
| echo "Waiting until eXeLearning is opened..." | |
| # Wait until the server responds or the window appears | |
| end=$((SECONDS + ${APP_BOOT_TIMEOUT:-120})) | |
| until curl -sf "http://localhost:${APP_PORT:-41309}/" >/dev/null 2>&1 || \ | |
| xdotool search --name 'eXeLearning' >/dev/null 2>&1 || \ | |
| [ $SECONDS -ge $end ]; do | |
| sleep 1 | |
| done | |
| echo "eXeLearning is already opened, taking first screenshot..." | |
| sleep ${SCREENSHOT_DELAY:-4} | |
| import -display :99 -window root ubuntu-screenshot-1.png || true | |
| echo "Sending Ctrl+P to the current focused window..." | |
| xdotool key ctrl+p | |
| echo "Taking second screenshot..." | |
| sleep ${SCREENSHOT_DELAY:-4} | |
| import -display :99 -window root ubuntu-screenshot-2.png || true | |
| # ---------- macOS test ---------- | |
| - name: Test install on macOS | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| set -euo pipefail | |
| installer=$(ls installers/*.dmg 2>/dev/null || true) | |
| if [ -z "$installer" ]; then echo "No DMG installer found" >&2; exit 1; fi | |
| ATTACH_OUT=$(hdiutil attach -nobrowse -noverify -noautoopen -readonly -mountRandom /Volumes "$installer" 2>&1) || true | |
| echo "$ATTACH_OUT" | |
| VOL=$(echo "$ATTACH_OUT" | awk '/\/Volumes\//{print $NF}' | tail -n1) | |
| [ -z "${VOL:-}" ] && { echo "Failed to attach DMG"; exit 1; } | |
| APP_PATH=$(find "$VOL" -maxdepth 2 -type d -name "*.app" -print -quit) | |
| [ -z "$APP_PATH" ] && { echo "No .app found in $VOL"; hdiutil detach "$VOL" || true; exit 1; } | |
| xattr -dr com.apple.quarantine "$APP_PATH" || true | |
| cp -R "$APP_PATH" /Applications/ | |
| hdiutil detach "$VOL" || true | |
| export ELECTRON_DISABLE_GPU=1 | |
| # Open app and wait for readiness | |
| open -a "eXeLearning" || open -n "/Applications/eXeLearning.app" || true | |
| end=$((SECONDS + ${APP_BOOT_TIMEOUT:-120})) | |
| until pgrep -x "eXeLearning" >/dev/null 2>&1 || \ | |
| curl -sf "http://localhost:${APP_PORT:-41309}/" >/dev/null 2>&1 || \ | |
| [ $SECONDS -ge $end ]; do | |
| sleep 1 | |
| done | |
| # Activate app, small pause and first screenshot | |
| osascript -e 'tell application "eXeLearning" to activate' || true | |
| sleep ${SCREENSHOT_DELAY:-4} | |
| screencapture -x mac-screenshot-1.png || true | |
| # Cmd+P, wait and second screenshot | |
| osascript -e 'tell application "System Events" to keystroke "p" using command down' || true | |
| sleep ${SCREENSHOT_DELAY:-4} | |
| screencapture -x mac-screenshot-2.png || true | |
| LOG1="$HOME/Library/Application Support/eXeLearning/logs/main.log" | |
| LOG2="$HOME/Library/Application Support/eXeLearning/log/main.log" | |
| if [ -f "$LOG1" ]; then cp "$LOG1" mac-app.log; elif [ -f "$LOG2" ]; then cp "$LOG2" mac-app.log; fi | |
| # ---------- Windows test (deterministic launch, no autolaunch) ---------- | |
| - name: Test install on Windows (deterministic) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| # -------- Settings -------- | |
| $procName = 'eXeLearning' | |
| $port = [int]($env:APP_PORT ?? 41309) | |
| $bootTimeout = [int]($env:APP_BOOT_TIMEOUT ?? 300) | |
| $shotDelay = [int]($env:SCREENSHOT_DELAY ?? 6) | |
| # -------- Find installer and install silently -------- | |
| $installer = Get-ChildItem installers\*Setup*.exe -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if (-not $installer) { throw "No Windows installer found in 'installers'." } | |
| # /S = silent for NSIS oneClick; avoids UI + autolaunch races | |
| Start-Process $installer.FullName -ArgumentList '/S' -Wait -NoNewWindow | |
| # -------- Locate installed EXE -------- | |
| $exePath = @( | |
| (Join-Path $env:LOCALAPPDATA 'Programs\eXeLearning\eXeLearning.exe'), | |
| (Join-Path ${env:ProgramFiles} 'eXeLearning\eXeLearning.exe'), | |
| (Join-Path ${env:ProgramFiles(x86)} 'eXeLearning\eXeLearning.exe') | |
| ) | Where-Object { Test-Path $_ } | Select-Object -First 1 | |
| if (-not $exePath) { | |
| $exePath = Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Programs') -Recurse -Depth 3 -Filter 'eXeLearning.exe' -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | ForEach-Object FullName | |
| } | |
| if (-not $exePath) { throw "Installed EXE not found to launch." } | |
| # -------- Launch with deterministic env for CI -------- | |
| $psi = New-Object System.Diagnostics.ProcessStartInfo | |
| $psi.FileName = $exePath | |
| $psi.WorkingDirectory = Split-Path -Parent $exePath | |
| $psi.UseShellExecute = $false | |
| # Force headless-friendly Electron | |
| $psi.Environment['ELECTRON_DISABLE_GPU'] = '1' | |
| $psi.Environment['ELECTRON_ENABLE_LOGGING'] = '1' | |
| # Ask the app to skip heavy init in CI | |
| $psi.Environment['CI'] = '1' | |
| $psi.Environment['EXE_SKIP_HEAVY_INIT'] = '1' | |
| # Prefer IPv4 loopback for consistency in Windows Server | |
| $psi.Environment['APP_HOST'] = '127.0.0.1' | |
| $proc = [System.Diagnostics.Process]::Start($psi) | |
| try { $null = $proc.WaitForInputIdle(10000) } catch {} | |
| # -------- Readiness: HTTP on 127.0.0.1 or a window handle -------- | |
| $ready = $false | |
| $deadline = (Get-Date).AddSeconds($bootTimeout) | |
| do { | |
| Start-Sleep -Seconds 4 | |
| # bail out if process died | |
| try { $alive = -not $proc.HasExited } catch { $alive = $true } | |
| if (-not $alive) { throw "Process exited before readiness." } | |
| # Window handle sometimes remains 0 in headless sessions; HTTP is the signal that matters | |
| if ($proc.MainWindowHandle -ne 0) { $ready = $true } | |
| if (-not $ready) { | |
| try { | |
| $resp = Invoke-WebRequest -UseBasicParsing -TimeoutSec 1 -Uri ("http://127.0.0.1:{0}/" -f $port) -ErrorAction SilentlyContinue | |
| if ($resp.StatusCode -ge 200 -and $resp.StatusCode -lt 500) { $ready = $true } | |
| } catch {} | |
| } | |
| } until ($ready -or (Get-Date) -ge $deadline) | |
| if (-not $ready) { | |
| Write-Error "Timed out waiting for readiness ($bootTimeout s)." | |
| $log1 = Join-Path $env:APPDATA "eXeLearning\logs\main.log" | |
| $log2 = Join-Path $env:APPDATA "eXeLearning\log\main.log" | |
| if (Test-Path $log1) { Get-Content $log1 -Tail 200 } | |
| elseif (Test-Path $log2) { Get-Content $log2 -Tail 200 } | |
| exit 1 | |
| } | |
| # -------- Bring to front and screenshots -------- | |
| try { | |
| $ws = New-Object -ComObject WScript.Shell | |
| $null = $ws.AppActivate($procName) | |
| } catch {} | |
| Start-Sleep -Seconds $shotDelay | |
| Add-Type -AssemblyName System.Windows.Forms | |
| Add-Type -AssemblyName System.Drawing | |
| [System.Windows.Forms.Application]::EnableVisualStyles() | |
| $bmp = New-Object Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width,[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height) | |
| $g = [Drawing.Graphics]::FromImage($bmp) | |
| $g.CopyFromScreen([System.Drawing.Point]::Empty,[System.Drawing.Point]::Empty,$bmp.Size) | |
| $bmp.Save("windows-screenshot-1.png") | |
| $g.Dispose(); $bmp.Dispose() | |
| [System.Windows.Forms.SendKeys]::SendWait("^p") | |
| Start-Sleep -Seconds ([int]($env:SCREENSHOT_DELAY ?? 3)) | |
| $bmp2 = New-Object Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width,[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height) | |
| $g2 = [Drawing.Graphics]::FromImage($bmp2) | |
| $g2.CopyFromScreen([System.Drawing.Point]::Empty,[System.Drawing.Point]::Empty,$bmp2.Size) | |
| $bmp2.Save("windows-screenshot-2.png") | |
| $g2.Dispose(); $bmp2.Dispose() | |
| # -------- Collect logs -------- | |
| $log1 = Join-Path $env:APPDATA "eXeLearning\logs\main.log" | |
| $log2 = Join-Path $env:APPDATA "eXeLearning\log\main.log" | |
| if (Test-Path $log1) { Copy-Item $log1 windows-app.log -ErrorAction SilentlyContinue } | |
| elseif (Test-Path $log2) { Copy-Item $log2 windows-app.log -ErrorAction SilentlyContinue } | |
| # -------- Cleanup -------- | |
| try { | |
| if ($proc.MainWindowHandle -ne 0) { $proc.CloseMainWindow() | Out-Null } | |
| Start-Sleep -Seconds 2 | |
| if (-not $proc.HasExited) { $proc.Kill() } | |
| } catch {} | |
| - name: Upload screenshots and logs (per OS) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: screenshots-and-logs-${{ matrix.os }} | |
| path: | | |
| ubuntu-*.png | |
| mac-*.png | |
| windows-*.png | |
| ubuntu-*.log | |
| mac-*.log | |
| windows-*.log | |
| collect-artifacts: | |
| needs: test-install | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all per-OS artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: screenshots-and-logs-* | |
| merge-multiple: true # leave all files in the workspace | |
| - name: Re-upload merged artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: screenshots-and-logs | |
| path: . |