diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 11ab8a0..04fb836 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -6,7 +6,43 @@ on: - 'v*' jobs: + # ── Create the release first (shared by all build jobs) ──── + create-release: + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.create.outputs.release_id }} + steps: + - name: Create Gitea release + id: create + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + TAG="${{ gitea.ref_name }}" + BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + + IS_PRE="false" + if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then + IS_PRE="true" + fi + + RELEASE=$(curl -s -X POST "$BASE_URL/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$TAG\", + \"name\": \"LedGrab $TAG\", + \"body\": \"## Downloads\\n\\n| Platform | File | How to run |\\n|----------|------|------------|\\n| Windows | \`LedGrab-${TAG}-win-x64.zip\` | Unzip → run \`LedGrab.bat\` → open http://localhost:8080 |\\n| Linux | \`LedGrab-${TAG}-linux-x64.tar.gz\` | Extract → run \`./run.sh\` → open http://localhost:8080 |\\n| Docker | See below | \`docker pull\` → \`docker run\` |\\n\\n### Docker\\n\\n\`\`\`bash\\ndocker pull ${{ gitea.server_url }}/${{ gitea.repository }}:${TAG}\\ndocker run -d -p 8080:8080 ${{ gitea.server_url }}/${{ gitea.repository }}:${TAG}\\n\`\`\`\", + \"draft\": false, + \"prerelease\": $IS_PRE + }") + + RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT" + echo "Created release ID: $RELEASE_ID" + + # ── Windows portable ZIP ─────────────────────────────────── build-windows: + needs: create-release runs-on: windows-latest steps: - name: Checkout @@ -31,44 +67,127 @@ jobs: path: build/LedGrab-*.zip retention-days: 90 - - name: Create Gitea release - shell: pwsh + - name: Attach ZIP to release + shell: bash 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" } + TAG="${{ gitea.ref_name }}" + RELEASE_ID="${{ needs.create-release.outputs.release_id }}" + BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + ZIP_FILE=$(ls build/LedGrab-*.zip | head -1) + ZIP_NAME=$(basename "$ZIP_FILE") - $baseUrl = "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" - $headers = @{ - "Authorization" = "token $env:GITEA_TOKEN" - "Content-Type" = "application/json" - } + curl -s -X POST \ + "$BASE_URL/releases/$RELEASE_ID/assets?name=$ZIP_NAME" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$ZIP_FILE" - # 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 + echo "Uploaded: $ZIP_NAME" - $release = Invoke-RestMethod -Method Post ` - -Uri "$baseUrl/releases" ` - -Headers $headers -Body $body + # ── Linux tarball ────────────────────────────────────────── + build-linux: + needs: create-release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 - Write-Host "Created release: $($release.html_url)" + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - # 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 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' - Write-Host "Uploaded: $($zipFile.Name)" + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libportaudio2 + + - name: Build Linux distribution + run: | + chmod +x build-dist.sh + ./build-dist.sh "${{ gitea.ref_name }}" + + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: LedGrab-${{ gitea.ref_name }}-linux-x64 + path: build/LedGrab-*.tar.gz + retention-days: 90 + + - name: Attach tarball to release + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + TAG="${{ gitea.ref_name }}" + RELEASE_ID="${{ needs.create-release.outputs.release_id }}" + BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + TAR_FILE=$(ls build/LedGrab-*.tar.gz | head -1) + TAR_NAME=$(basename "$TAR_FILE") + + curl -s -X POST \ + "$BASE_URL/releases/$RELEASE_ID/assets?name=$TAR_NAME" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$TAR_FILE" + + echo "Uploaded: $TAR_NAME" + + # ── Docker image ─────────────────────────────────────────── + build-docker: + needs: create-release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ gitea.server_url }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract version metadata + id: meta + run: | + TAG="${{ gitea.ref_name }}" + VERSION="${TAG#v}" + REGISTRY="${{ gitea.server_url }}/${{ gitea.repository }}" + # Lowercase the registry path (Docker requires it) + REGISTRY=$(echo "$REGISTRY" | tr '[:upper:]' '[:lower:]' | sed 's|https\?://||') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" + + # Build tag list: version + latest (only for stable releases) + TAGS="$REGISTRY:$TAG,$REGISTRY:$VERSION" + if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then + TAGS="$TAGS,$REGISTRY:latest" + fi + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: | + org.opencontainers.image.version=${{ steps.meta.outputs.version }} + org.opencontainers.image.revision=${{ gitea.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/build-dist.sh b/build-dist.sh new file mode 100644 index 0000000..60e8238 --- /dev/null +++ b/build-dist.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# +# Build a portable Linux distribution of LedGrab. +# Produces a self-contained tarball with virtualenv and launcher script. +# +# Usage: +# ./build-dist.sh [VERSION] +# ./build-dist.sh v0.1.0-alpha.1 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$SCRIPT_DIR/build" +DIST_NAME="LedGrab" +DIST_DIR="$BUILD_DIR/$DIST_NAME" +SERVER_DIR="$SCRIPT_DIR/server" +VENV_DIR="$DIST_DIR/venv" +APP_DIR="$DIST_DIR/app" + +# ── Version detection ──────────────────────────────────────── + +VERSION="${1:-}" + +if [ -z "$VERSION" ]; then + VERSION=$(git describe --tags --exact-match 2>/dev/null || true) +fi +if [ -z "$VERSION" ]; then + VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}" +fi +if [ -z "$VERSION" ]; then + VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' "$SERVER_DIR/src/wled_controller/__init__.py" 2>/dev/null || echo "0.0.0") +fi + +VERSION_CLEAN="${VERSION#v}" +TAR_NAME="LedGrab-v${VERSION_CLEAN}-linux-x64.tar.gz" + +echo "=== Building LedGrab v${VERSION_CLEAN} (Linux) ===" +echo " Output: build/$TAR_NAME" +echo "" + +# ── Clean ──────────────────────────────────────────────────── + +if [ -d "$DIST_DIR" ]; then + echo "[1/7] Cleaning previous build..." + rm -rf "$DIST_DIR" +fi +mkdir -p "$DIST_DIR" + +# ── Create virtualenv ──────────────────────────────────────── + +echo "[2/7] Creating virtualenv..." +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +pip install --upgrade pip --quiet + +# ── Install dependencies ───────────────────────────────────── + +echo "[3/7] Installing dependencies..." +pip install --quiet "${SERVER_DIR}[camera,notifications]" 2>&1 | { + grep -i 'error\|failed' || true +} + +# Remove the installed wled_controller package (PYTHONPATH handles app code) +SITE_PACKAGES="$VENV_DIR/lib/python*/site-packages" +rm -rf $SITE_PACKAGES/wled_controller* $SITE_PACKAGES/wled*.dist-info 2>/dev/null || true + +# Clean up caches +find "$VENV_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true +find "$VENV_DIR" -type d -name tests -exec rm -rf {} + 2>/dev/null || true +find "$VENV_DIR" -type d -name test -exec rm -rf {} + 2>/dev/null || true + +# ── Build frontend ─────────────────────────────────────────── + +echo "[4/7] Building frontend bundle..." +(cd "$SERVER_DIR" && npm ci --loglevel error && npm run build) 2>&1 | { + grep -v 'RemoteException' || true +} + +# ── Copy application files ─────────────────────────────────── + +echo "[5/7] Copying application files..." +mkdir -p "$APP_DIR" + +cp -r "$SERVER_DIR/src" "$APP_DIR/src" +cp -r "$SERVER_DIR/config" "$APP_DIR/config" +mkdir -p "$DIST_DIR/data" "$DIST_DIR/logs" + +# Clean up source maps and __pycache__ +find "$APP_DIR" -name "*.map" -delete 2>/dev/null || true +find "$APP_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + +# ── Create launcher ────────────────────────────────────────── + +echo "[6/7] Creating launcher..." +cat > "$DIST_DIR/run.sh" << 'LAUNCHER' +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +export PYTHONPATH="$SCRIPT_DIR/app/src" +export WLED_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml" + +mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs" + +echo "" +echo " =============================================" +echo " LedGrab vVERSION_PLACEHOLDER" +echo " Open http://localhost:8080 in your browser" +echo " =============================================" +echo "" + +source "$SCRIPT_DIR/venv/bin/activate" +exec python -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080 +LAUNCHER + +sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh" +chmod +x "$DIST_DIR/run.sh" + +# ── Create tarball ─────────────────────────────────────────── + +echo "[7/7] Creating $TAR_NAME..." +deactivate 2>/dev/null || true + +TAR_PATH="$BUILD_DIR/$TAR_NAME" +(cd "$BUILD_DIR" && tar -czf "$TAR_NAME" "$DIST_NAME") + +TAR_SIZE=$(du -h "$TAR_PATH" | cut -f1) +echo "" +echo "=== Build complete ===" +echo " Archive: $TAR_PATH" +echo " Size: $TAR_SIZE" +echo ""