Replace deploy workflow with portable Windows release build
- Remove old Docker-based deploy.yml - Add release.yml: builds portable ZIP on tag push, uploads to Gitea - Add build-dist.ps1: downloads embedded Python, installs deps, bundles app - Add scrollbar-gutter: stable to prevent layout shift Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
name: Build and Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy to /opt/wled-controller
|
|
||||||
run: |
|
|
||||||
DEPLOY_DIR=/opt/wled-controller
|
|
||||||
|
|
||||||
# Ensure deploy directory exists
|
|
||||||
mkdir -p "$DEPLOY_DIR/data" "$DEPLOY_DIR/logs" "$DEPLOY_DIR/config"
|
|
||||||
|
|
||||||
# Copy server files to deploy directory
|
|
||||||
rsync -a --delete \
|
|
||||||
--exclude 'data/' \
|
|
||||||
--exclude 'logs/' \
|
|
||||||
server/ "$DEPLOY_DIR/"
|
|
||||||
|
|
||||||
# Build and restart
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
docker compose down
|
|
||||||
docker compose up -d --build
|
|
||||||
74
.gitea/workflows/release.yml
Normal file
74
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Build Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Build portable distribution
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
.\build-dist.ps1 -Version "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: LedGrab-${{ gitea.ref_name }}-win-x64
|
||||||
|
path: build/LedGrab-*.zip
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
$tag = "${{ gitea.ref_name }}"
|
||||||
|
$zipFile = Get-ChildItem "build\LedGrab-*.zip" | Select-Object -First 1
|
||||||
|
if (-not $zipFile) { throw "ZIP not found" }
|
||||||
|
|
||||||
|
$baseUrl = "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
$headers = @{
|
||||||
|
"Authorization" = "token $env:GITEA_TOKEN"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
$body = @{
|
||||||
|
tag_name = $tag
|
||||||
|
name = "LedGrab $tag"
|
||||||
|
body = "Portable Windows build — unzip, run ``LedGrab.bat``, open http://localhost:8080"
|
||||||
|
draft = $false
|
||||||
|
prerelease = ($tag -match '(alpha|beta|rc)')
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
$release = Invoke-RestMethod -Method Post `
|
||||||
|
-Uri "$baseUrl/releases" `
|
||||||
|
-Headers $headers -Body $body
|
||||||
|
|
||||||
|
Write-Host "Created release: $($release.html_url)"
|
||||||
|
|
||||||
|
# Upload ZIP asset
|
||||||
|
$uploadHeaders = @{
|
||||||
|
"Authorization" = "token $env:GITEA_TOKEN"
|
||||||
|
}
|
||||||
|
$uploadUrl = "$baseUrl/releases/$($release.id)/assets?name=$($zipFile.Name)"
|
||||||
|
Invoke-RestMethod -Method Post -Uri $uploadUrl `
|
||||||
|
-Headers $uploadHeaders `
|
||||||
|
-ContentType "application/octet-stream" `
|
||||||
|
-InFile $zipFile.FullName
|
||||||
|
|
||||||
|
Write-Host "Uploaded: $($zipFile.Name)"
|
||||||
250
build-dist.ps1
Normal file
250
build-dist.ps1
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Build a portable Windows distribution of LedGrab.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Downloads embedded Python, installs all dependencies, copies app code,
|
||||||
|
builds the frontend bundle, and produces a self-contained ZIP.
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
Version string (e.g. "0.1.0" or "v0.1.0"). Auto-detected from git tag
|
||||||
|
or __init__.py if omitted.
|
||||||
|
|
||||||
|
.PARAMETER PythonVersion
|
||||||
|
Embedded Python version to download. Default: 3.11.9
|
||||||
|
|
||||||
|
.PARAMETER SkipFrontend
|
||||||
|
Skip npm ci + npm run build (use if frontend is already built).
|
||||||
|
|
||||||
|
.PARAMETER SkipPerf
|
||||||
|
Skip installing optional [perf] extras (dxcam, bettercam, windows-capture).
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\build-dist.ps1
|
||||||
|
.\build-dist.ps1 -Version "0.2.0"
|
||||||
|
.\build-dist.ps1 -SkipFrontend -SkipPerf
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$Version = "",
|
||||||
|
[string]$PythonVersion = "3.11.9",
|
||||||
|
[switch]$SkipFrontend,
|
||||||
|
[switch]$SkipPerf
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue' # faster downloads
|
||||||
|
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
$BuildDir = Join-Path $ScriptRoot "build"
|
||||||
|
$DistName = "LedGrab"
|
||||||
|
$DistDir = Join-Path $BuildDir $DistName
|
||||||
|
$ServerDir = Join-Path $ScriptRoot "server"
|
||||||
|
$PythonDir = Join-Path $DistDir "python"
|
||||||
|
$AppDir = Join-Path $DistDir "app"
|
||||||
|
|
||||||
|
# ── Version detection ──────────────────────────────────────────
|
||||||
|
|
||||||
|
if (-not $Version) {
|
||||||
|
# Try git tag
|
||||||
|
try {
|
||||||
|
$gitTag = git describe --tags --exact-match 2>$null
|
||||||
|
if ($gitTag) { $Version = $gitTag }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (-not $Version) {
|
||||||
|
# Try env var (CI)
|
||||||
|
if ($env:GITEA_REF_NAME) { $Version = $env:GITEA_REF_NAME }
|
||||||
|
elseif ($env:GITHUB_REF_NAME) { $Version = $env:GITHUB_REF_NAME }
|
||||||
|
}
|
||||||
|
if (-not $Version) {
|
||||||
|
# Parse from __init__.py
|
||||||
|
$initFile = Join-Path $ServerDir "src\wled_controller\__init__.py"
|
||||||
|
$match = Select-String -Path $initFile -Pattern '__version__\s*=\s*"([^"]+)"'
|
||||||
|
if ($match) { $Version = $match.Matches[0].Groups[1].Value }
|
||||||
|
}
|
||||||
|
if (-not $Version) { $Version = "0.0.0" }
|
||||||
|
|
||||||
|
# Strip leading 'v' for filenames
|
||||||
|
$VersionClean = $Version -replace '^v', ''
|
||||||
|
$ZipName = "LedGrab-v${VersionClean}-win-x64.zip"
|
||||||
|
|
||||||
|
Write-Host "=== Building LedGrab v${VersionClean} ===" -ForegroundColor Cyan
|
||||||
|
Write-Host " Python: $PythonVersion"
|
||||||
|
Write-Host " Output: build\$ZipName"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ── Clean ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (Test-Path $DistDir) {
|
||||||
|
Write-Host "[1/8] Cleaning previous build..."
|
||||||
|
Remove-Item -Recurse -Force $DistDir
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
|
||||||
|
|
||||||
|
# ── Download embedded Python ───────────────────────────────────
|
||||||
|
|
||||||
|
$PythonZipUrl = "https://www.python.org/ftp/python/${PythonVersion}/python-${PythonVersion}-embed-amd64.zip"
|
||||||
|
$PythonZipPath = Join-Path $BuildDir "python-embed.zip"
|
||||||
|
|
||||||
|
Write-Host "[2/8] Downloading embedded Python ${PythonVersion}..."
|
||||||
|
if (-not (Test-Path $PythonZipPath)) {
|
||||||
|
Invoke-WebRequest -Uri $PythonZipUrl -OutFile $PythonZipPath
|
||||||
|
}
|
||||||
|
Write-Host " Extracting to python/..."
|
||||||
|
Expand-Archive -Path $PythonZipPath -DestinationPath $PythonDir -Force
|
||||||
|
|
||||||
|
# ── Patch ._pth to enable site-packages ────────────────────────
|
||||||
|
|
||||||
|
Write-Host "[3/8] Patching Python path configuration..."
|
||||||
|
$pthFile = Get-ChildItem -Path $PythonDir -Filter "python*._pth" | Select-Object -First 1
|
||||||
|
if (-not $pthFile) { throw "Could not find python*._pth in $PythonDir" }
|
||||||
|
|
||||||
|
$pthContent = Get-Content $pthFile.FullName -Raw
|
||||||
|
# Uncomment 'import site'
|
||||||
|
$pthContent = $pthContent -replace '#\s*import site', 'import site'
|
||||||
|
# Add Lib\site-packages if not present
|
||||||
|
if ($pthContent -notmatch 'Lib\\site-packages') {
|
||||||
|
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
|
||||||
|
}
|
||||||
|
Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline
|
||||||
|
Write-Host " Patched $($pthFile.Name)"
|
||||||
|
|
||||||
|
# ── Install pip ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host "[4/8] Installing pip..."
|
||||||
|
$GetPipPath = Join-Path $BuildDir "get-pip.py"
|
||||||
|
if (-not (Test-Path $GetPipPath)) {
|
||||||
|
Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPipPath
|
||||||
|
}
|
||||||
|
$python = Join-Path $PythonDir "python.exe"
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
& $python $GetPipPath --no-warn-script-location 2>&1 | Out-Null
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Failed to install pip" }
|
||||||
|
|
||||||
|
# ── Install dependencies ──────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host "[5/8] Installing dependencies..."
|
||||||
|
$extras = "camera,notifications"
|
||||||
|
if (-not $SkipPerf) { $extras += ",perf" }
|
||||||
|
|
||||||
|
# Install the project (pulls all deps via pyproject.toml), then remove
|
||||||
|
# the installed package itself — PYTHONPATH handles app code loading.
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
& $python -m pip install --no-warn-script-location "${ServerDir}[${extras}]" 2>&1 | ForEach-Object {
|
||||||
|
if ($_ -match 'ERROR|Failed') { Write-Host " $_" -ForegroundColor Red }
|
||||||
|
}
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host " Some optional deps may have failed (continuing)..." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the installed wled_controller package to avoid duplication
|
||||||
|
$sitePackages = Join-Path $PythonDir "Lib\site-packages"
|
||||||
|
Get-ChildItem -Path $sitePackages -Filter "wled*" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-ChildItem -Path $sitePackages -Filter "wled*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Clean up caches and test files to reduce size
|
||||||
|
Write-Host " Cleaning up caches..."
|
||||||
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "tests" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "test" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# ── Build frontend ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (-not $SkipFrontend) {
|
||||||
|
Write-Host "[6/8] Building frontend bundle..."
|
||||||
|
Push-Location $ServerDir
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
& npm ci --loglevel error 2>&1 | Out-Null
|
||||||
|
& npm run build 2>&1 | ForEach-Object {
|
||||||
|
$line = "$_"
|
||||||
|
if ($line -and $line -notmatch 'RemoteException') { Write-Host " $line" }
|
||||||
|
}
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[6/8] Skipping frontend build (--SkipFrontend)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Copy application files ─────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host "[7/8] Copying application files..."
|
||||||
|
New-Item -ItemType Directory -Path $AppDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy source code (includes static/dist bundle, templates, locales)
|
||||||
|
$srcDest = Join-Path $AppDir "src"
|
||||||
|
Copy-Item -Path (Join-Path $ServerDir "src") -Destination $srcDest -Recurse
|
||||||
|
|
||||||
|
# Copy config
|
||||||
|
$configDest = Join-Path $AppDir "config"
|
||||||
|
Copy-Item -Path (Join-Path $ServerDir "config") -Destination $configDest -Recurse
|
||||||
|
|
||||||
|
# Create empty data/ and logs/ directories
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $DistDir "data") -Force | Out-Null
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $DistDir "logs") -Force | Out-Null
|
||||||
|
|
||||||
|
# Clean up source maps and __pycache__ from app code
|
||||||
|
Get-ChildItem -Path $srcDest -Recurse -Filter "*.map" | Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-ChildItem -Path $srcDest -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# ── Create launcher ────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host "[8/8] Creating launcher..."
|
||||||
|
|
||||||
|
$launcherContent = @'
|
||||||
|
@echo off
|
||||||
|
title LedGrab v%VERSION%
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
:: Set paths
|
||||||
|
set PYTHONPATH=%~dp0app\src
|
||||||
|
set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
||||||
|
|
||||||
|
:: Create data directory if missing
|
||||||
|
if not exist "%~dp0data" mkdir "%~dp0data"
|
||||||
|
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo =============================================
|
||||||
|
echo LedGrab v%VERSION%
|
||||||
|
echo Open http://localhost:8080 in your browser
|
||||||
|
echo =============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Start the server (open browser after short delay)
|
||||||
|
start "" /b cmd /c "timeout /t 2 /nobreak >nul && start http://localhost:8080"
|
||||||
|
"%~dp0python\python.exe" -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
|
pause
|
||||||
|
'@
|
||||||
|
|
||||||
|
$launcherContent = $launcherContent -replace '%VERSION%', $VersionClean
|
||||||
|
$launcherPath = Join-Path $DistDir "LedGrab.bat"
|
||||||
|
Set-Content -Path $launcherPath -Value $launcherContent -Encoding ASCII
|
||||||
|
|
||||||
|
# ── Create ZIP ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$ZipPath = Join-Path $BuildDir $ZipName
|
||||||
|
if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath }
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Creating $ZipName..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Use 7-Zip if available (faster, handles locked files), else fall back to Compress-Archive
|
||||||
|
$7z = Get-Command 7z -ErrorAction SilentlyContinue
|
||||||
|
if ($7z) {
|
||||||
|
& 7z a -tzip -mx=7 $ZipPath "$DistDir\*" | Select-Object -Last 3
|
||||||
|
} else {
|
||||||
|
Compress-Archive -Path "$DistDir\*" -DestinationPath $ZipPath -CompressionLevel Optimal
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipSize = (Get-Item $ZipPath).Length / 1MB
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Build complete ===" -ForegroundColor Green
|
||||||
|
Write-Host " Archive: $ZipPath"
|
||||||
|
Write-Host " Size: $([math]::Round($zipSize, 1)) MB"
|
||||||
|
Write-Host ""
|
||||||
@@ -81,6 +81,7 @@ html {
|
|||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
Reference in New Issue
Block a user