Merge pull request #1 from AsieduDevelopmentHub/feature/reliability-s… #18
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
| name: Build Release Artifacts | |
| on: | |
| workflow_dispatch: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write | |
| env: | |
| # RELEASE_VERSION: git ref label (branch name or tag without v). ARTIFACT_VERSION: safe semver for filenames. | |
| RELEASE_VERSION: "" | |
| ARTIFACT_VERSION: "" | |
| # Starting port written into engine-config.json in CI (engine may increment). | |
| ENGINE_PORT: "8740" | |
| jobs: | |
| windows-installer-x64: | |
| name: Windows x64 installer | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Derive version from tag | |
| shell: pwsh | |
| run: | | |
| $ref = "${env:GITHUB_REF_NAME}" | |
| $run = "${env:GITHUB_RUN_NUMBER}" | |
| if ($ref -like "v*") { | |
| $ver = $ref.Substring(1) | |
| $artifactVer = $ver | |
| } else { | |
| $ver = $ref | |
| # Branch/manual runs: avoid vmain in installer names; keep unique per workflow run. | |
| $artifactVer = "0.0.0.$run" | |
| } | |
| "RELEASE_VERSION=$ver" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "ARTIFACT_VERSION=$artifactVer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| Write-Host "Release version: $ver (artifact files: v$artifactVer)" | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Cache pip | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~\AppData\Local\pip\Cache | |
| key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} | |
| - name: Inject version into project files | |
| shell: pwsh | |
| run: | | |
| $ver = "${env:RELEASE_VERSION}" | |
| # Flutter pubspec / Inno require semver x.y.z; branch names (e.g. main) are invalid. | |
| $pubVer = if ($ver -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { "0.0.0" } | |
| # Flutter pubspec version (build number from CI run) | |
| (Get-Content "dashboard/pubspec.yaml") ` | |
| -replace '^version:\s*.+$', "version: $pubVer+${env:GITHUB_RUN_NUMBER}" ` | |
| | Set-Content "dashboard/pubspec.yaml" | |
| # Engine version metadata | |
| (Get-Content "engine/__init__.py") ` | |
| -replace '__version__\s*=\s*\"[^\"]+\"', "__version__ = `"$ver`"" ` | |
| | Set-Content "engine/__init__.py" | |
| (Get-Content "engine/api/server.py") ` | |
| -replace 'version=\"[^\"]+\"', "version=`"$ver`"" ` | |
| | Set-Content "engine/api/server.py" | |
| # Inno Setup version + output name (use ARTIFACT_VERSION so branch runs are not vmain) | |
| $artifactVer = "${env:ARTIFACT_VERSION}" | |
| (Get-Content "installer/sentracore.iss") ` | |
| -replace '^AppVersion=.*$', "AppVersion=$pubVer" ` | |
| -replace '^OutputBaseFilename=.*$', "OutputBaseFilename=SentraCore_Setup_v$artifactVer" ` | |
| | Set-Content "installer/sentracore.iss" | |
| - name: Create venv + install engine deps | |
| shell: pwsh | |
| run: | | |
| python -m venv .venv | |
| .\.venv\Scripts\python.exe -m pip install --upgrade pip | |
| .\.venv\Scripts\python.exe -m pip install -r requirements.txt | |
| .\.venv\Scripts\python.exe -m pip install pyinstaller | |
| - name: Build engine (PyInstaller) | |
| shell: pwsh | |
| run: | | |
| .\.venv\Scripts\python.exe -m PyInstaller --name "SentraCoreEngine" ` | |
| --noconsole ` | |
| --onefile ` | |
| --hidden-import "uvicorn.logging" ` | |
| --hidden-import "uvicorn.loops" ` | |
| --hidden-import "uvicorn.loops.auto" ` | |
| --hidden-import "uvicorn.protocols" ` | |
| --hidden-import "uvicorn.protocols.http" ` | |
| --hidden-import "uvicorn.protocols.http.auto" ` | |
| --hidden-import "uvicorn.protocols.websockets" ` | |
| --hidden-import "uvicorn.protocols.websockets.auto" ` | |
| --hidden-import "engine.api.server" ` | |
| --clean ` | |
| engine/main.py | |
| if (!(Test-Path "dist\\SentraCoreEngine.exe")) { throw "Engine exe not found" } | |
| - name: Validate engine runs (health check) | |
| shell: pwsh | |
| run: | | |
| $distDir = Join-Path (Get-Location) "dist" | |
| if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null } | |
| $cfgPath = Join-Path $distDir "engine-config.json" | |
| # Kill anything listening on the start port (best-effort). | |
| $startPort = [int]${env:ENGINE_PORT} | |
| $pids = (Get-NetTCPConnection -State Listen -LocalPort $startPort -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique) | |
| foreach ($procId in $pids) { try { Stop-Process -Id $procId -Force } catch {} } | |
| # Write engine-config.json next to engine (single source of truth). | |
| $cfg = @{ | |
| host = "127.0.0.1" | |
| port = $startPort | |
| bind_host = "127.0.0.1" | |
| status = "stopped" | |
| last_error = "" | |
| pid = 0 | |
| } | ConvertTo-Json | |
| $tmp = "$cfgPath.tmp" | |
| $cfg | Out-File -FilePath $tmp -Encoding utf8 | |
| Move-Item -Force $tmp $cfgPath | |
| $cfgFull = [System.IO.Path]::GetFullPath($cfgPath) | |
| $p = Start-Process -FilePath ".\\dist\\SentraCoreEngine.exe" -PassThru -WindowStyle Hidden -Environment @{ "SENTRACORE_ENGINE_CONFIG" = $cfgFull } | |
| try { | |
| $ok = $false | |
| for ($i=0; $i -lt 40; $i++) { | |
| try { | |
| $cfgDisk = $null | |
| try { $cfgDisk = Get-Content "$cfgFull" -Raw | ConvertFrom-Json } catch {} | |
| # Never use $host — it shadows PowerShell's automatic $Host and breaks this loop. | |
| $connectHost = if ($cfgDisk -and $cfgDisk.host) { $cfgDisk.host } else { "127.0.0.1" } | |
| $port = if ($cfgDisk -and $cfgDisk.port) { [int]$cfgDisk.port } else { $startPort } | |
| $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 2 "http://${connectHost}:${port}/api/v1/health" | |
| if ($r.StatusCode -eq 200) { $ok = $true; break } | |
| } catch {} | |
| Start-Sleep -Milliseconds 500 | |
| } | |
| if (-not $ok) { throw "Engine health check failed" } | |
| } finally { | |
| try { Stop-Process -Id $p.Id -Force } catch {} | |
| try { Stop-Process -Name "SentraCoreEngine" -Force } catch {} | |
| } | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| cache: true | |
| - name: Build dashboard (Flutter Windows x64) | |
| shell: pwsh | |
| working-directory: dashboard | |
| run: | | |
| flutter --version | |
| flutter pub get | |
| flutter build windows --release | |
| if (!(Test-Path "build\\windows\\x64\\runner\\Release\\sentracore_dashboard.exe")) { throw "Dashboard exe not found" } | |
| - name: Ship engine-config.json next to Windows binaries (installer payload) | |
| shell: pwsh | |
| run: | | |
| $cfg = "dashboard\assets\engine-config.json" | |
| if (!(Test-Path $cfg)) { throw "Missing $cfg" } | |
| $releaseDir = "dashboard\build\windows\x64\runner\Release" | |
| Copy-Item $cfg (Join-Path $releaseDir "engine-config.json") -Force | |
| Copy-Item $cfg "dist\engine-config.json" -Force | |
| Write-Host "engine-config.json -> Release + dist" | |
| - name: Code sign engine + dashboard (optional) | |
| shell: pwsh | |
| env: | |
| WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} | |
| WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} | |
| run: | | |
| if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64)) { | |
| Write-Host "Skipping Authenticode signing (set repo secret WINDOWS_CODESIGN_PFX_BASE64 + WINDOWS_CODESIGN_PFX_PASSWORD; see docs/architecture/building.md)." | |
| exit 0 | |
| } | |
| $pfxPath = Join-Path $env:RUNNER_TEMP "sentracore-codesign.pfx" | |
| [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64)) | |
| & ".\scripts\sign-windows-artifacts.ps1" -PfxPath $pfxPath -RepoRoot (Get-Location).Path | |
| Remove-Item -LiteralPath $pfxPath -Force | |
| - name: Install Inno Setup | |
| shell: pwsh | |
| run: | | |
| choco install innosetup -y --no-progress | |
| - name: Build installer (ISCC) | |
| shell: pwsh | |
| run: | | |
| $iscc = "${env:ProgramFiles(x86)}\\Inno Setup 6\\ISCC.exe" | |
| if (!(Test-Path $iscc)) { throw "ISCC not found at $iscc" } | |
| & $iscc "installer\\sentracore.iss" | |
| Get-ChildItem "dist" -Filter "SentraCore_Setup_v*.exe" | Select-Object -First 1 | ForEach-Object { $_.FullName } | |
| - name: Code sign installer (optional) | |
| shell: pwsh | |
| env: | |
| WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} | |
| WINDOWS_CODESIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PFX_PASSWORD }} | |
| run: | | |
| if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64)) { | |
| Write-Host "Skipping installer signing (same secrets as engine/dashboard step)." | |
| exit 0 | |
| } | |
| $pfxPath = Join-Path $env:RUNNER_TEMP "sentracore-codesign-installer.pfx" | |
| [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64)) | |
| & ".\scripts\sign-windows-artifacts.ps1" -PfxPath $pfxPath -RepoRoot (Get-Location).Path -InstallerOnly | |
| Remove-Item -LiteralPath $pfxPath -Force | |
| - name: Upload installer artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: SentraCore_Windows_x64_installer | |
| path: dist/SentraCore_Setup_v*.exe | |
| macos-app: | |
| name: macOS .app + engine | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Derive version from tag | |
| run: | | |
| REF="${GITHUB_REF_NAME}" | |
| RUN="${GITHUB_RUN_NUMBER:-0}" | |
| if [[ "$REF" == v* ]]; then | |
| VER="${REF:1}" | |
| ART="$VER" | |
| else | |
| VER="$REF" | |
| ART="0.0.0.${RUN}" | |
| fi | |
| echo "RELEASE_VERSION=$VER" >> "$GITHUB_ENV" | |
| echo "ARTIFACT_VERSION=$ART" >> "$GITHUB_ENV" | |
| echo "Release version: $VER (artifact files: v$ART)" | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Cache pip | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/Library/Caches/pip | |
| ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} | |
| - name: Inject version into project files | |
| run: | | |
| python - <<'PY' | |
| import os, re, pathlib | |
| ver = os.environ["RELEASE_VERSION"] | |
| run = os.environ.get("GITHUB_RUN_NUMBER", "1") | |
| m = re.match(r"^(\d+\.\d+\.\d+)", ver.strip()) | |
| pub_ver = m.group(1) if m else "0.0.0" | |
| p = pathlib.Path("dashboard/pubspec.yaml") | |
| p.write_text(re.sub(r"^version:\s*.+$", f"version: {pub_ver}+{run}", p.read_text(), flags=re.M)) | |
| init = pathlib.Path("engine/__init__.py") | |
| init.write_text(re.sub(r'__version__\s*=\s*\"[^\"]+\"', f'__version__ = \"{ver}\"', init.read_text())) | |
| srv = pathlib.Path("engine/api/server.py") | |
| srv.write_text(re.sub(r'version=\"[^\"]+\"', f'version=\"{ver}\"', srv.read_text())) | |
| PY | |
| - name: Install engine deps + PyInstaller | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -r requirements.txt | |
| python -m pip install pyinstaller | |
| - name: Build engine (PyInstaller) | |
| run: | | |
| python -m PyInstaller --name "SentraCoreEngine" \ | |
| --onefile \ | |
| --windowed \ | |
| --hidden-import "uvicorn.logging" \ | |
| --hidden-import "uvicorn.loops" \ | |
| --hidden-import "uvicorn.loops.auto" \ | |
| --hidden-import "uvicorn.protocols" \ | |
| --hidden-import "uvicorn.protocols.http" \ | |
| --hidden-import "uvicorn.protocols.http.auto" \ | |
| --hidden-import "uvicorn.protocols.websockets" \ | |
| --hidden-import "uvicorn.protocols.websockets.auto" \ | |
| --hidden-import "engine.api.server" \ | |
| --clean \ | |
| engine/main.py | |
| test -f dist/SentraCoreEngine | |
| - name: Validate engine runs (health check) | |
| run: | | |
| chmod +x dist/SentraCoreEngine | |
| START_PORT="${ENGINE_PORT:-8740}" | |
| # Kill anything on start port (best-effort). | |
| lsof -ti "tcp:${START_PORT}" | xargs kill -9 2>/dev/null || true | |
| # Write engine-config.json next to engine. | |
| cat > dist/engine-config.json <<EOF | |
| { | |
| "host": "127.0.0.1", | |
| "port": ${START_PORT}, | |
| "bind_host": "127.0.0.1", | |
| "status": "stopped", | |
| "last_error": "", | |
| "pid": 0 | |
| } | |
| EOF | |
| SENTRACORE_ENGINE_CONFIG="$(pwd)/dist/engine-config.json" ./dist/SentraCoreEngine & | |
| PID=$! | |
| OK=0 | |
| for i in {1..40}; do | |
| PORT="$(python - <<'PY' | |
| import json, pathlib | |
| p = pathlib.Path("dist/engine-config.json") | |
| try: | |
| j = json.loads(p.read_text()) | |
| print(int(j.get("port", 0)) or 0) | |
| except Exception: | |
| print(0) | |
| PY | |
| )" | |
| if [ -z "$PORT" ] || [ "$PORT" = "0" ]; then PORT="$START_PORT"; fi | |
| if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/api/v1/health" >/dev/null; then | |
| OK=1 | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| kill $PID || true | |
| pkill -f SentraCoreEngine || true | |
| if [ "$OK" -ne 1 ]; then | |
| echo "Engine health check failed" | |
| exit 1 | |
| fi | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| cache: true | |
| - name: Build dashboard (Flutter macOS) | |
| working-directory: dashboard | |
| run: | | |
| flutter pub get | |
| flutter build macos --release | |
| - name: Bundle engine into .app + sign (ad-hoc) | |
| run: | | |
| set -euo pipefail | |
| RELEASE_DIR="dashboard/build/macos/Build/Products/Release" | |
| if [ ! -d "$RELEASE_DIR" ]; then | |
| echo "Missing Release dir: $RELEASE_DIR" | |
| exit 1 | |
| fi | |
| APP="$(find "$RELEASE_DIR" -maxdepth 1 -name "*.app" -print -quit)" | |
| if [ -z "${APP}" ] || [ ! -d "$APP" ]; then | |
| echo "No .app bundle under $RELEASE_DIR — contents:" | |
| ls -la "$RELEASE_DIR" || true | |
| exit 1 | |
| fi | |
| echo "Using app bundle: $APP" | |
| chmod +x dist/SentraCoreEngine | |
| codesign --force --sign - dist/SentraCoreEngine | |
| cp dist/SentraCoreEngine "$APP/Contents/MacOS/SentraCoreEngine" | |
| chmod +x "$APP/Contents/MacOS/SentraCoreEngine" | |
| cp dashboard/assets/engine-config.json "$APP/Contents/MacOS/engine-config.json" | |
| codesign --force --deep --sign - "$APP" | |
| - name: Package (zip) | |
| run: | | |
| set -euo pipefail | |
| RELEASE_DIR="dashboard/build/macos/Build/Products/Release" | |
| APP="$(find "$RELEASE_DIR" -maxdepth 1 -name "*.app" -print -quit)" | |
| if [ -z "${APP}" ] || [ ! -d "$APP" ]; then | |
| echo "No .app bundle under $RELEASE_DIR" | |
| ls -la "$RELEASE_DIR" || true | |
| exit 1 | |
| fi | |
| zip -r "SentraCore_macOS_${ARTIFACT_VERSION}.zip" "$APP" | |
| - name: Upload macOS artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: SentraCore_macOS_app | |
| path: SentraCore_macOS_*.zip | |
| linux-bundle: | |
| name: Linux bundle + engine | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Derive version from tag | |
| run: | | |
| REF="${GITHUB_REF_NAME}" | |
| RUN="${GITHUB_RUN_NUMBER:-0}" | |
| if [[ "$REF" == v* ]]; then | |
| VER="${REF:1}" | |
| ART="$VER" | |
| else | |
| VER="$REF" | |
| ART="0.0.0.${RUN}" | |
| fi | |
| echo "RELEASE_VERSION=$VER" >> "$GITHUB_ENV" | |
| echo "ARTIFACT_VERSION=$ART" >> "$GITHUB_ENV" | |
| echo "Release version: $VER (artifact files: v$ART)" | |
| - name: Install Linux desktop build deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| clang cmake ninja-build pkg-config \ | |
| libgtk-3-dev liblzma-dev \ | |
| libfuse2 curl | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Cache pip | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} | |
| - name: Inject version into project files | |
| run: | | |
| python - <<'PY' | |
| import os, re, pathlib | |
| ver = os.environ["RELEASE_VERSION"] | |
| run = os.environ.get("GITHUB_RUN_NUMBER", "1") | |
| m = re.match(r"^(\d+\.\d+\.\d+)", ver.strip()) | |
| pub_ver = m.group(1) if m else "0.0.0" | |
| p = pathlib.Path("dashboard/pubspec.yaml") | |
| p.write_text(re.sub(r"^version:\s*.+$", f"version: {pub_ver}+{run}", p.read_text(), flags=re.M)) | |
| init = pathlib.Path("engine/__init__.py") | |
| init.write_text(re.sub(r'__version__\s*=\s*\"[^\"]+\"', f'__version__ = \"{ver}\"', init.read_text())) | |
| srv = pathlib.Path("engine/api/server.py") | |
| srv.write_text(re.sub(r'version=\"[^\"]+\"', f'version=\"{ver}\"', srv.read_text())) | |
| PY | |
| - name: Install engine deps + PyInstaller | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -r requirements.txt | |
| python -m pip install pyinstaller | |
| - name: Build engine (PyInstaller) | |
| run: | | |
| python -m PyInstaller --name "SentraCoreEngine" \ | |
| --onefile \ | |
| --hidden-import "uvicorn.logging" \ | |
| --hidden-import "uvicorn.loops" \ | |
| --hidden-import "uvicorn.loops.auto" \ | |
| --hidden-import "uvicorn.protocols" \ | |
| --hidden-import "uvicorn.protocols.http" \ | |
| --hidden-import "uvicorn.protocols.http.auto" \ | |
| --hidden-import "uvicorn.protocols.websockets" \ | |
| --hidden-import "uvicorn.protocols.websockets.auto" \ | |
| --hidden-import "engine.api.server" \ | |
| --clean \ | |
| engine/main.py | |
| test -f dist/SentraCoreEngine | |
| - name: Validate engine runs (health check) | |
| run: | | |
| chmod +x dist/SentraCoreEngine | |
| START_PORT="${ENGINE_PORT:-8740}" | |
| fuser -k "${START_PORT}/tcp" 2>/dev/null || true | |
| cat > dist/engine-config.json <<EOF | |
| { | |
| "host": "127.0.0.1", | |
| "port": ${START_PORT}, | |
| "bind_host": "0.0.0.0", | |
| "status": "stopped", | |
| "last_error": "", | |
| "pid": 0 | |
| } | |
| EOF | |
| SENTRACORE_ENGINE_CONFIG="$(pwd)/dist/engine-config.json" ./dist/SentraCoreEngine & | |
| PID=$! | |
| OK=0 | |
| for i in {1..40}; do | |
| PORT="$(python - <<'PY' | |
| import json, pathlib | |
| p = pathlib.Path("dist/engine-config.json") | |
| try: | |
| j = json.loads(p.read_text()) | |
| print(int(j.get("port", 0)) or 0) | |
| except Exception: | |
| print(0) | |
| PY | |
| )" | |
| if [ -z "$PORT" ] || [ "$PORT" = "0" ]; then PORT="$START_PORT"; fi | |
| if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/api/v1/health" >/dev/null; then | |
| OK=1 | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| kill $PID || true | |
| pkill -f SentraCoreEngine || true | |
| if [ "$OK" -ne 1 ]; then | |
| echo "Engine health check failed" | |
| exit 1 | |
| fi | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| cache: true | |
| - name: Build dashboard (Flutter Linux) | |
| working-directory: dashboard | |
| run: | | |
| flutter pub get | |
| flutter build linux --release | |
| - name: Package (AppImage) | |
| run: | | |
| # Create an AppDir with Flutter bundle + engine | |
| rm -rf SentraCore.AppDir | |
| mkdir -p SentraCore.AppDir/usr/bin | |
| cp -R dashboard/build/linux/x64/release/bundle/* SentraCore.AppDir/usr/bin/ | |
| cp dist/SentraCoreEngine SentraCore.AppDir/usr/bin/SentraCoreEngine | |
| chmod +x SentraCore.AppDir/usr/bin/SentraCoreEngine | |
| cp dashboard/assets/engine-config.json SentraCore.AppDir/usr/bin/engine-config.json | |
| # AppRun launcher | |
| cat > SentraCore.AppDir/AppRun <<'EOF' | |
| #!/bin/sh | |
| set -e | |
| HERE="$(dirname "$(readlink -f "$0")")" | |
| cd "$HERE/usr/bin" | |
| # Create engine-config.json (single source of truth) next to binaries. | |
| if [ ! -f "./engine-config.json" ]; then | |
| cat > "./engine-config.json" <<'JSON' | |
| { | |
| "host": "127.0.0.1", | |
| "port": 8740, | |
| "bind_host": "0.0.0.0", | |
| "status": "stopped", | |
| "last_error": "", | |
| "pid": 0 | |
| } | |
| JSON | |
| fi | |
| # Start engine in background (best-effort) and stop it when UI exits. | |
| SENTRACORE_ENGINE_CONFIG="$HERE/usr/bin/engine-config.json" ./SentraCoreEngine >/dev/null 2>&1 & | |
| ENGINE_PID=$! | |
| trap "kill $ENGINE_PID >/dev/null 2>&1 || true; pkill -f SentraCoreEngine >/dev/null 2>&1 || true" EXIT | |
| # Wait until engine responds (avoid UI starting before backend). | |
| OK=0 | |
| for i in $(seq 1 40); do | |
| PORT="$(sed -n 's/.*\"port\"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p' ./engine-config.json | head -n 1)" | |
| if [ -z "$PORT" ]; then PORT="8740"; fi | |
| if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/api/v1/health" >/dev/null; then | |
| OK=1 | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| if [ "$OK" -ne 1 ]; then | |
| echo "Engine health check failed" | |
| exit 1 | |
| fi | |
| exec ./sentracore_dashboard "$@" | |
| EOF | |
| chmod +x SentraCore.AppDir/AppRun | |
| # Desktop entry | |
| cat > SentraCore.AppDir/sentracore.desktop <<'EOF' | |
| [Desktop Entry] | |
| Type=Application | |
| Name=SentraCore | |
| Exec=sentracore_dashboard | |
| Icon=sentracore | |
| Categories=Utility; | |
| EOF | |
| # Minimal icon (1x1 PNG) to satisfy appimagetool | |
| printf 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7cS2kAAAAASUVORK5CYII=' | base64 -d > SentraCore.AppDir/sentracore.png | |
| # Download appimagetool and build AppImage | |
| curl -L -o appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" | |
| chmod +x appimagetool | |
| ./appimagetool SentraCore.AppDir SentraCore_linux.AppImage | |
| test -f SentraCore_linux.AppImage | |
| - name: Rename AppImage with version | |
| run: | | |
| mv SentraCore_linux.AppImage "SentraCore_linux_${ARTIFACT_VERSION}.AppImage" | |
| - name: Upload Linux artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: SentraCore_Linux_bundle | |
| path: SentraCore_linux_*.AppImage | |
| github-release: | |
| name: Publish GitHub Release | |
| needs: [windows-installer-x64, macos-app, linux-bundle] | |
| if: startsWith(github.ref, 'refs/tags/') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: List artifacts | |
| run: ls -R artifacts | |
| - name: Create GitHub Release (attach assets) | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| name: SentraCore ${{ github.ref_name }} | |
| body: | | |
| ## Highlights | |
| - Updated dashboard UI with new home page and settings page. | |
| - Added new settings page to configure engine settings. | |
| - Added new home page to display engine status and settings. | |
| - Website: modern GitHub Pages landing site with direct-download buttons (via GitHub Releases API). | |
| ## Downloads | |
| Grab the correct asset for your OS from the files attached to this release: | |
| - Windows: `SentraCore_Setup_v*.exe` | |
| - macOS: `SentraCore_macOS_*.zip` | |
| - Linux: `SentraCore_linux_*.AppImage` | |
| ## Notes | |
| - If your system blocks the Releases API, the website buttons fall back to the Releases page. | |
| files: | | |
| artifacts/**/SentraCore_Setup_v*.exe | |
| artifacts/**/SentraCore_macOS_*.zip | |
| artifacts/**/SentraCore_linux_*.AppImage | |