From 7f6bb8911aae64ec1e1a360d9258e18fd7ec11c4 Mon Sep 17 00:00:00 2001 From: SlavaVlad Date: Sat, 27 Jun 2026 03:42:02 +0300 Subject: [PATCH] =?UTF-8?q?CI:=20full=20rewrite=20=E2=80=94=20zero=20exter?= =?UTF-8?q?nal=20actions,=20AppImage=20+=20exe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove actions/checkout@v4/v3 (Gitea incompatibility) - Checkout via git clone with server_url + RELEASE_TOKEN - Remove upload-artifact@v4 (GHESNotSupportedError) - Upload directly to Gitea release via API - Linux → AppImage (portable, self-contained) - Windows → .exe + ZIP - Fix PYTHON_WIN_URL, icon generation, checkout URL --- .gitea/workflows/release.yml | 333 +++++++++++++++++++++-------------- 1 file changed, 196 insertions(+), 137 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ad3aaf4..c85054b 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,6 +1,9 @@ # ============================================================================= # 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: @@ -21,6 +24,11 @@ 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: @@ -31,16 +39,20 @@ jobs: name: Validate source runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - clean: true + - 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 ${{ env.PYTHON_VERSION }} + - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -50,9 +62,9 @@ jobs: - name: Syntax check (compileall — hard gate) run: python -m compileall -q *.py - - name: Lint info (pyflakes — non-blocking advisory) + - name: Lint info (pyflakes — advisory) run: | - python -m pyflakes *.py || echo "Pyflakes warnings above (advisory only)" + python -m pyflakes *.py || echo "Pyflakes warnings above (advisory)" - name: Verify .spec run: | @@ -77,13 +89,15 @@ jobs: target: [linux, windows] steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - clean: true + - 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 }}" - # ---- Common: Setup Python ---- - - name: Setup Python ${{ env.PYTHON_VERSION }} + - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -100,21 +114,83 @@ jobs: pip install pyinstaller # ---- Linux: native build ---- - - name: Build Linux native (PyInstaller) + - name: Build Linux (PyInstaller) if: matrix.target == 'linux' run: pyinstaller Warlock-Studio.spec --noconfirm - - name: Package Linux (tar.gz) + - name: Package Linux (AppImage) if: matrix.target == 'linux' run: | - v="${{ github.ref_name }}" && v="${v#v}" - [ "${{ github.event_name }}" = "workflow_dispatch" ] && v="${{ github.event.inputs.version }}" - [ -z "$v" ] && v="dev" - a="${{ env.APP_NAME }}-${v}-linux-x64.tar.gz" - tar -czf "${a}" -C dist "${{ env.APP_NAME }}" - echo "ARCHIVE_NAME=${a}" >> $GITHUB_ENV + version="${{ github.ref_name }}" && version="${version#v}" + [ "${{ github.event_name }}" = "workflow_dispatch" ] && version="${{ github.event.inputs.version }}" + [ -z "$version" ] && version="dev" - - name: Verify Linux binary + # 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 }}" @@ -127,8 +203,6 @@ jobs: sudo dpkg --add-architecture i386 sudo apt-get update -qq sudo apt-get install -y -qq wine wine32 wine64 xvfb - - # Download Windows Python into stable location mkdir -p /tmp/winbuild wget -q "${{ env.PYTHON_WIN_URL }}" -O /tmp/winbuild/python-installer.exe @@ -138,34 +212,27 @@ jobs: export DISPLAY=:99 Xvfb :99 -screen 0 1024x768x16 &>/dev/null & sleep 1 - - # Install Python for Windows silently wine /tmp/winbuild/python-installer.exe \ /quiet InstallAllUsers=1 PrependPath=1 \ Include_doc=0 Include_tcltk=1 \ - TargetDir='C:\Python310' 2>&1 | tail -5 - - # Verify + TargetDir='C:\Python310' 2>&1 | tail -3 wine python --version 2>&1 - wine pip --version 2>&1 - name: Install Windows dependencies under Wine if: matrix.target == 'windows' run: | - # Map project directory as Windows drive - WINEPROJ="Z:$(pwd)" - # Install pip deps + winedir="Z:$(pwd)" wine pip install --no-input --no-cache-dir \ - -r "${WINEPROJ}/requirements.txt" 2>&1 | tail -5 + -r "${winedir}/requirements.txt" 2>&1 | tail -3 wine pip install --no-input --no-cache-dir \ - pyinstaller onnxruntime-directml 2>&1 | tail -5 + pyinstaller onnxruntime-directml 2>&1 | tail -3 - name: Build Windows binary (PyInstaller via Wine) if: matrix.target == 'windows' run: | - WINEPROJ="Z:$(pwd)" - wine pyinstaller "${WINEPROJ}/Warlock-Studio.spec" \ - --noconfirm --distpath "${WINEPROJ}/dist" 2>&1 | tail -10 + 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' @@ -173,119 +240,111 @@ jobs: ls -lh "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" file "dist/${{ env.APP_NAME }}/${{ env.APP_NAME }}.exe" - - name: Package Windows (ZIP) + - name: Package Windows (.exe + runtime → ZIP) if: matrix.target == 'windows' run: | - v="${{ github.ref_name }}" && v="${v#v}" - [ "${{ github.event_name }}" = "workflow_dispatch" ] && v="${{ github.event.inputs.version }}" - [ -z "$v" ] && v="dev" - a="${{ env.APP_NAME }}-${v}-win64.zip" + 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 "${a}" "${{ env.APP_NAME }}" + zip -r "${archive}" "${{ env.APP_NAME }}" cd .. - mv "dist/${a}" . - echo "ARCHIVE_NAME=${a}" >> $GITHUB_ENV + mv "dist/${archive}" . + echo "ARCHIVE_NAME=${archive}" >> $GITHUB_ENV - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ env.APP_NAME }}-${{ matrix.target }} - path: ${{ env.ARCHIVE_NAME }} + # ---- 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 — create Gitea release + attach binaries + # RELEASE — finalize (verify all assets uploaded) # ========================================================================== release: - name: Create Release + name: Finalize Release needs: [build] runs-on: ubuntu-22.04 if: github.event_name != 'workflow_dispatch' || !github.event.inputs.dry_run steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: actions/download-artifact@v4 - with: - pattern: ${{ env.APP_NAME }}-* - path: artifacts - merge-multiple: true - - - name: List artifacts - run: ls -lh artifacts/ - - - name: Determine version - id: version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - else - echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT - v="${{ github.ref_name }}" && echo "version=${v#v}" >> $GITHUB_OUTPUT - fi - - - name: Prepare release notes - run: | - if [ -f CHANGELOG.md ]; then - awk '/^## /{if(c) exit; c=1; next} c' CHANGELOG.md > release-notes.md 2>/dev/null || true - fi - [ -s release-notes.md ] || echo "Release ${{ steps.version.outputs.tag }}" > release-notes.md - - - name: Create Gitea Release - id: create_release + - name: Show release status env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - BODY=$(cat release-notes.md | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") - PAYLOAD='{ - "tag_name": "${{ steps.version.outputs.tag }}", - "target_commitish": "${{ github.sha }}", - "name": "Warlock Studio ${{ steps.version.outputs.version }}", - "body": '"${BODY}"', - "draft": false, - "prerelease": false - }' + tag="${{ github.ref_name }}" + server="${{ gitea.server_url }}" + repo="${{ gitea.repository }}" - RESP=$(curl -s -X POST \ - -H "Authorization: token ${RELEASE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "${PAYLOAD}" \ - "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases") - - ID=$(echo "${RESP}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") - - if [ -z "${ID}" ]; then - echo "ERROR creating release:" - echo "${RESP}" - exit 1 - fi - - echo "release_id=${ID}" >> $GITHUB_OUTPUT - echo "Release ID: ${ID}" - - - name: Upload assets to release - env: - RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: | - ID="${{ steps.create_release.outputs.release_id }}" - URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${ID}/assets" - - for a in artifacts/*; do - [ -f "${a}" ] || continue - fname=$(basename "${a}") - echo "Uploading: ${fname} ($(stat -c%s "${a}") bytes)" - - curl -s -X POST \ - -H "Authorization: token ${RELEASE_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@${a}" \ - "${URL}?name=${fname}" > /dev/null - - if [ $? -eq 0 ]; then - echo " OK" - else - echo " FAIL" - fi - done + 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"