name: Build Linux Release on: push: tags: - 'v*' permissions: contents: write env: PYTHON_VERSION: '3.13.6' jobs: build-linux: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 0 - name: Extract version from tag id: get_version shell: bash run: | tag="${GITHUB_REF_NAME}" version="${tag#v}" echo "Extracted version: $version" echo "VERSION=$version" >> "$GITHUB_OUTPUT" - name: Setup Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Cache Python dependencies uses: actions/cache@v4 with: path: | venv ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install system dependencies shell: bash run: | sudo apt-get update -y # Tools commonly required by cx_Freeze on Linux packaging sudo apt-get install -y --no-install-recommends \ rpm alien fakeroot patchelf desktop-file-utils file xz-utils zsync - name: Create virtual environment and install dependencies shell: bash run: | python -m venv venv source venv/bin/activate python -m pip install --upgrade pip pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir cx_Freeze yt-dlp - name: Prepare build variables shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" echo "VERSION=$version" >> "$GITHUB_ENV" arch="$(uname -m)" # x86_64 or aarch64 echo "ARCH=$arch" >> "$GITHUB_ENV" echo "Prepared build variables for version: $version on $(uname -a)" - name: Create cx_Freeze setup script (Linux) shell: bash run: | cat > setup_cxfreeze.py <<'PY' import os from cx_Freeze import setup, Executable version = os.environ.get("VERSION", "0.0.0") build_exe_options = dict( optimize=2, packages=[ "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", "requests", "PIL", "packaging", "markdown", "pyglet", "loguru", "setuptools", ], excludes=[ "PySide6.QtBluetooth", "PySide6.QtNetwork", "PySide6.QtOpenGL", "PySide6.QtPrintSupport", "PySide6.QtSvg", "PySide6.QtTest", "PySide6.QtXml", "PySide6.QtSql", "PySide6.QtHelp", "PySide6.QtMultimedia", "PySide6.QtQml", "PySide6.QtQuick", "PySide6.QtWebEngineCore", "PIL.ImageDraw", "PIL.ImageFont", "numpy", "scipy", "wx", "pandas", "tkinter", "yt_dlp", "unittest", "test", "tests", ], include_files=[ ("src", "src"), ("assets", "lib/assets"), ("ytsage.desktop", "share/applications/ytsage.desktop"), ("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"), ], ) executables = [ Executable( script="main.py", target_name="ytsage", icon="assets/branding/icons/icon.png", ) ] setup( name="ytsage", version=version, description="YTSage", options={ "build_exe": build_exe_options, # AppImage packaging "bdist_appimage": { # If ends with .AppImage, use verbatim file name "target_name": f"ytsage-v{version}.AppImage", }, # RPM packaging metadata "bdist_rpm": { "release": "1", "group": "Applications/Multimedia", }, # DEB packaging wraps alien to convert RPM "bdist_deb": {}, }, executables=executables, ) PY - name: Create desktop entry shell: bash run: | cat > ytsage.desktop <<'DESKTOP' [Desktop Entry] Type=Application Name=YTSage Comment=YouTube downloader Exec=/usr/bin/ytsage %U Icon=ytsage Terminal=false Categories=AudioVideo;Network;Utility; StartupWMClass=YTSage DESKTOP - name: Build AppImage and RPM shell: bash run: | set -e source venv/bin/activate # Ensure a clean build and build_exe first python setup_cxfreeze.py build_exe # Remove screenshots to reduce size before packaging build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true) if [ -n "$build_dir" ] && [ -d "$build_dir/lib/assets/branding/screenshots" ]; then rm -rf "$build_dir/lib/assets/branding/screenshots" echo "Removed screenshots folder from $build_dir" fi # Build AppImage python setup_cxfreeze.py bdist_appimage # Build RPM python setup_cxfreeze.py bdist_rpm echo "Post-build directory listing:" echo "-- dist --"; ls -lah dist || true echo "-- build --"; ls -lah build || true - name: Build native DEB package shell: bash run: | set -e version="${{ steps.get_version.outputs.VERSION }}" arch_uname="${ARCH}" # Map uname -m to Debian arch names case "$arch_uname" in x86_64) deb_arch=amd64 ;; aarch64) deb_arch=arm64 ;; *) deb_arch="$arch_uname" ;; esac # Locate build output build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1) if [ -z "$build_dir" ]; then echo "Error: build/exe.* directory not found" >&2 exit 1 fi # Staging root pkgroot=deb_pkg rm -rf "$pkgroot" mkdir -p "$pkgroot/DEBIAN" \ "$pkgroot/usr/bin" \ "$pkgroot/usr/share/applications" \ "$pkgroot/usr/share/pixmaps" \ "$pkgroot/opt/ytsage" # Install application payload under /opt/ytsage cp -a "$build_dir"/* "$pkgroot/opt/ytsage/" # Wrapper to ensure correct working directory cat > "$pkgroot/usr/bin/ytsage" <<'WRAP' #!/usr/bin/env bash set -euo pipefail APPDIR="/opt/ytsage" cd "$APPDIR" exec "$APPDIR/ytsage" "$@" WRAP chmod 0755 "$pkgroot/usr/bin/ytsage" # Desktop file and icon install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop" install -m 0644 assets/branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png" # Control file cat > "$pkgroot/DEBIAN/control" < Homepage: https://github.com/oop7/YTSage Description: YTSage - A modern YouTube downloader with a PySide6 interface Download videos, extract audio, fetch subtitles, and more. CONTROL # Post-install script to refresh desktop/menu caches (best-effort) cat > "$pkgroot/DEBIAN/postinst" <<'POSTINST' #!/bin/sh set -e if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q || true fi if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true fi exit 0 POSTINST chmod 0755 "$pkgroot/DEBIAN/postinst" # Triggers so Debian updates caches cat > "$pkgroot/DEBIAN/triggers" <<'TRIGGERS' interest-noawait /usr/share/applications interest-noawait /usr/share/icons/hicolor TRIGGERS # Build the deb deb_out="YTSage-v${version}-${deb_arch}.deb" dpkg-deb --build "$pkgroot" "$deb_out" mkdir -p artifacts mv "$deb_out" artifacts/ echo "Built native DEB: artifacts/$deb_out" - name: Package and prepare release artifacts (.AppImage, .rpm, .deb) shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" mkdir -p artifacts # Copy AppImage appimage_src=$(ls dist/ytsage-v*.AppImage 2>/dev/null | head -n1 || true) if [ -n "$appimage_src" ]; then cp "$appimage_src" "artifacts/YTSage-v${version}-${ARCH}.AppImage" echo "Copied AppImage: $appimage_src -> artifacts/YTSage-v${version}-${ARCH}.AppImage" else echo "Warning: No .AppImage found in dist/" fi # Copy RPM (normalize name) rpm_src=$(ls dist/ytsage-*.rpm 2>/dev/null | head -n1 || true) if [ -n "$rpm_src" ]; then cp "$rpm_src" "artifacts/YTSage-v${version}-${ARCH}.rpm" echo "Copied RPM: $rpm_src -> artifacts/YTSage-v${version}-${ARCH}.rpm" else echo "Warning: No .rpm found in dist/" fi # DEB is already staged in artifacts by the native packaging step ls -1 artifacts/*.deb 2>/dev/null || echo "Warning: No .deb found in artifacts/" echo "Final artifacts:" ls -lh artifacts || true - name: Create/Update draft release uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.ref_name }} name: YTSage ${{ github.ref_name }} draft: true prerelease: false append_body: true fail_on_unmatched_files: false body: | # YTSage ${{ github.ref_name }} **Release Date**: ${{ github.event.head_commit.timestamp }} **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ github.ref_name }} files: | artifacts/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}