Skip to content

build(deps): bump mariadb from 12.2 to 12.3 in /doc/deploy (#1889) #205

build(deps): bump mariadb from 12.2 to 12.3 in /doc/deploy (#1889)

build(deps): bump mariadb from 12.2 to 12.3 in /doc/deploy (#1889) #205

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: .