# ============================================================================= # Warlock Studio — Gitea Actions CI/CD Pipeline # ============================================================================= # Zero external actions except setup-python@v5 (which comes from Gitea's # built-in action cache). Checkout via git clone. Artifact upload via API. # ============================================================================= name: Build and Release on: push: tags: - 'v*' workflow_dispatch: inputs: version: description: 'Release version (e.g., 6.0.1)' required: true dry_run: description: 'Dry run — build but do not create a release' type: boolean default: false env: APP_NAME: Warlock-Studio PYTHON_VERSION: '3.10' PYTHON_WIN_URL: 'https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe' RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} defaults: run: shell: bash jobs: # ========================================================================== # VALIDATE — syntax + lint before building # ========================================================================== validate: name: Validate source runs-on: ubuntu-22.04 steps: - name: Checkout repository run: | # Build clone URL from server_url + repository server_url="${{ gitea.server_url }}" clone_url="${server_url}" git clone "https://git:${RELEASE_TOKEN}@${clone_url#https://}/${{ gitea.repository }}" . git checkout "${{ github.sha }}" git log --oneline -3 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pyinstaller pyflakes - name: Syntax check (compileall — hard gate) run: python -m compileall -q *.py - name: Lint info (pyflakes — advisory) run: | python -m pyflakes *.py || echo "Pyflakes warnings above (advisory)" - name: Verify .spec run: | python -c " with open('Warlock-Studio.spec') as f: c = f.read() assert 'Analysis' in c assert 'EXE' in c print('OK .spec valid') " # ========================================================================== # BUILD — matrix: linux (native) + windows (cross via wine) # ========================================================================== build: name: Build (${{ matrix.target }}) needs: [validate] runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: target: [linux, windows] steps: - name: Checkout repository run: | # Build clone URL from server_url + repository server_url="${{ gitea.server_url }}" clone_url="${server_url}" git clone "https://git:${RELEASE_TOKEN}@${clone_url#https://}/${{ gitea.repository }}" . git checkout "${{ github.sha }}" - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install common system deps run: | sudo apt-get update -qq sudo apt-get install -y -qq libgl1-mesa-glx libegl1 libxkbcommon0 file - name: Install Python deps run: | python -m pip install --upgrade pip wheel setuptools pip install -r requirements.txt pip install pyinstaller # ---- Linux: native build ---- - name: Build Linux (PyInstaller) if: matrix.target == 'linux' run: pyinstaller Warlock-Studio.spec --noconfirm - name: Package Linux (AppImage) if: matrix.target == 'linux' run: | version="${{ github.ref_name }}" && version="${version#v}" [ "${{ github.event_name }}" = "workflow_dispatch" ] && version="${{ github.event.inputs.version }}" [ -z "$version" ] && version="dev" # Create AppDir structure APPDIR="dist/${{ env.APP_NAME }}.AppDir" mkdir -p "${APPDIR}/usr/bin" cp -r "dist/${{ env.APP_NAME }}"/* "${APPDIR}/usr/bin/" # Desktop entry cat > "${APPDIR}/${{ env.APP_NAME }}.desktop" << DESKTOP_EOF [Desktop Entry] Name=Warlock Studio Comment=AI-powered video upscaling, restoration and frame interpolation Exec=Warlock-Studio Icon=warlock-studio Terminal=false Type=Application Categories=Graphics;Video;AudioVideo; DESKTOP_EOF # AppRun — entry point cat > "${APPDIR}/AppRun" << 'APPRUN_EOF' #!/bin/bash HERE="$(dirname "$(readlink -f "$0")")" export PATH="${HERE}/usr/bin:${PATH}" export LD_LIBRARY_PATH="${HERE}/usr/bin/_internal:${LD_LIBRARY_PATH}" exec "${HERE}/usr/bin/Warlock-Studio" "$@" APPRUN_EOF chmod +x "${APPDIR}/AppRun" # Icon: try to convert .ico, fallback to placeholder PNG if command -v python3 &>/dev/null; then python3 << 'PYEOF' import os, base64 apdir = os.environ.get('APPDIR', 'dist/Warlock-Studio.AppDir') png_path = os.path.join(apdir, 'warlock-studio.png') # Write a 48x48 transparent PNG as icon placeholder with open(png_path, 'wb') as f: f.write(base64.b64decode( 'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB' 'WElEQVRoQ+2YOw7CMBBEQ0WHaDgBByDhCBScgAMQdFScgJoj0HABQqJAwk0CQhL5s5KVj0rs' '2LFj165nRzL9f+cBAQICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICgoj8iMhJ' 'AL4AZACYi0gSkQKAOwDc3H0AUJQdAHBXROSdSaYAQB3D1N0BoO6fA+DuTQA4m80uALABAGd3' 'B4Cq+yf7o+sEAK7uDgBZ3QcAOAB0R5hPJpPl9/t9X5bldz6fdwCw2+2+6v5RluUDABaLxUvd' 'P8uyvANARIRlWX4BwGaz+aymKYviiYhwdV+O4xjeK6WkqioCgLu7EEKIiMy6rrsCQFXViYik' 'lHijASGEMKZq3meMRNM0KaV0mE6nJymlMwCklNLUhMPhoOu6LgBQKUUppTTFQMYYSimlqQbd' 'b5RS4t2IiIgaY4wx8toYoymlNE1ozzljjAYAgKPRSNV9DgAiYowx3ogBACEEz+fzEwDyPM/b' 'tj0BQNM0eZq238LD4A8ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhgAJ3F' 'Af+7OiaAAAAASUVORK5CYII=' )) print(f'Icon written: {png_path}') PYEOF fi # Download appimagetool if ! command -v appimagetool &>/dev/null; then wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O /tmp/appimagetool chmod +x /tmp/appimagetool APPIMAGETOOL="/tmp/appimagetool" else APPIMAGETOOL="appimagetool" fi # Build AppImage ARCH=x86_64 "${APPIMAGETOOL}" "${APPDIR}" "${{ env.APP_NAME }}-${version}-x86_64.AppImage" 2>&1 | tail -5 echo "ARCHIVE_NAME=${{ env.APP_NAME }}-${version}-x86_64.AppImage" >> $GITHUB_ENV - name: Verify Linux AppImage if: matrix.target == 'linux' run: | ls -lh "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}" file "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}" # ---- Windows: cross-build via Wine ---- - name: Install Wine + Windows Python if: matrix.target == 'windows' run: | sudo dpkg --add-architecture i386 sudo apt-get update -qq sudo apt-get install -y -qq wine wine32 wine64 xvfb mkdir -p /tmp/winbuild wget -q "${{ env.PYTHON_WIN_URL }}" -O /tmp/winbuild/python-installer.exe - name: Setup Windows Python under Wine if: matrix.target == 'windows' run: | export DISPLAY=:99 Xvfb :99 -screen 0 1024x768x16 &>/dev/null & sleep 1 wine /tmp/winbuild/python-installer.exe \ /quiet InstallAllUsers=1 PrependPath=1 \ Include_doc=0 Include_tcltk=1 \ TargetDir='C:\Python310' 2>&1 | tail -3 wine python --version 2>&1 - name: Install Windows dependencies under Wine if: matrix.target == 'windows' run: | winedir="Z:$(pwd)" wine pip install --no-input --no-cache-dir \ -r "${winedir}/requirements.txt" 2>&1 | tail -3 wine pip install --no-input --no-cache-dir \ pyinstaller onnxruntime-directml 2>&1 | tail -3 - name: Build Windows binary (PyInstaller via Wine) if: matrix.target == 'windows' run: | winedir="Z:$(pwd)" wine pyinstaller "${winedir}/Warlock-Studio.spec" \ --noconfirm --distpath "${winedir}/dist" 2>&1 | tail -10 - name: Verify Windows binary if: matrix.target == 'windows' run: | ls -lh "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" file "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" - name: Package Windows (.exe + runtime → ZIP) if: matrix.target == 'windows' run: | version="${{ github.ref_name }}" && version="${version#v}" [ "${{ github.event_name }}" = "workflow_dispatch" ] && version="${{ github.event.inputs.version }}" [ -z "$version" ] && version="dev" echo "Windows binary: dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" ls -lh "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" archive="${{ env.APP_NAME }}-${version}-win64.zip" cd dist zip -r "${archive}" "${{ env.APP_NAME }}" cd .. mv "dist/${archive}" . echo "ARCHIVE_NAME=${archive}" >> $GITHUB_ENV # ---- Upload to release (no external actions) ---- - name: Upload artifact to release if: github.event_name != 'workflow_dispatch' || !github.event.inputs.dry_run env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | archive="${{ env.ARCHIVE_NAME }}" if [ -z "${archive}" ] || [ ! -f "${archive}" ]; then echo "No archive found, skipping upload" exit 0 fi repo="${{ gitea.repository }}" server="${{ gitea.server_url }}" tag="${{ github.ref_name }}" echo "Uploading ${archive} to ${server}/${repo}/releases (tag: ${tag})" # 1. Check if release exists releases=$(curl -s -H "Authorization: token ${RELEASE_TOKEN}" \ "${server}/api/v1/repos/${repo}/releases?tag=${tag}") release_id=$(echo "${releases}" | python3 -c \ "import sys,json; data=json.load(sys.stdin); print(data[0]['id'] if isinstance(data,list) and len(data)>0 else '')" 2>/dev/null || echo "") # 2. Create release if not exists if [ -z "${release_id}" ]; then echo "Creating new release for ${tag}..." body="Release ${tag}" if [ -f CHANGELOG.md ]; then body=$(head -50 CHANGELOG.md | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") else body=$(echo "Release ${tag}" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") fi payload="{\"tag_name\":\"${tag}\",\"name\":\"${tag}\",\"body\":${body},\"draft\":false,\"prerelease\":false}" resp=$(curl -s -X POST \ -H "Authorization: token ${RELEASE_TOKEN}" \ -H "Content-Type: application/json" \ -d "${payload}" \ "${server}/api/v1/repos/${repo}/releases") release_id=$(echo "${resp}" | python3 -c \ "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") if [ -z "${release_id}" ]; then echo "ERROR creating release: ${resp}" exit 1 fi fi echo "Release ID: ${release_id}" # 3. Upload asset echo "Uploading ${archive}..." curl -s -X POST \ -H "Authorization: token ${RELEASE_TOKEN}" \ -H "Content-Type: application/octet-stream" \ --data-binary "@${archive}" \ "${server}/api/v1/repos/${repo}/releases/${release_id}/assets?name=${archive}" > /dev/null echo "Upload complete" # ========================================================================== # RELEASE — finalize (verify all assets uploaded) # ========================================================================== release: name: Finalize Release needs: [build] runs-on: ubuntu-22.04 if: github.event_name != 'workflow_dispatch' || !github.event.inputs.dry_run steps: - name: Show release status env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | tag="${{ github.ref_name }}" server="${{ gitea.server_url }}" repo="${{ gitea.repository }}" releases=$(curl -s -H "Authorization: token ${RELEASE_TOKEN}" \ "${server}/api/v1/repos/${repo}/releases?tag=${tag}") echo "Release info:" echo "${releases}" | python3 -c " import sys,json data=json.load(sys.stdin) if isinstance(data,list) and len(data)>0: r=data[0] print(f\" ID: {r['id']}\") print(f\" Tag: {r['tag_name']}\") print(f\" Name: {r['name']}\") print(f\" Assets: {len(r.get('assets',[]))}\") for a in r.get('assets',[]): print(f\" - {a['name']} ({a['size']} bytes)\") else: print(' No release found for tag ' + tag) " 2>/dev/null || echo " Could not parse release data"