name: Build macOS Release on: push: tags: - 'v*' permissions: contents: write env: PYTHON_VERSION: '3.13' jobs: build-macos: runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v6 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 ~/Library/Caches/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - 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 dmgbuild - name: Prepare build variables shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" echo "VERSION=$version" >> "$GITHUB_ENV" arch="$(uname -m)" # arm64 or x86_64 echo "ARCH=$arch" >> "$GITHUB_ENV" if [ "$arch" = "arm64" ]; then echo "ARCH_SUFFIX=arm64" >> "$GITHUB_ENV" else echo "ARCH_SUFFIX=x64" >> "$GITHUB_ENV" fi echo "Prepared build variables for version: $version on $(uname -a)" - name: Create cx_Freeze setup script 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/branding/icons", "lib/assets/branding/icons"), ("assets/Icon", "lib/assets/Icon"), ("assets/sound", "lib/assets/sound"), ("languages", "lib/languages"), ], ) executables = [ Executable( script="main.py", target_name=f"YTSage-v{version}", icon="assets/branding/icons/icon.icns", ) ] setup( name="YTSage", version=version, description="YTSage", options={ "build_exe": build_exe_options, "bdist_mac": { "iconfile": "assets/branding/icons/icon.icns", "bundle_name": f"YTSage-v{version}", }, "bdist_dmg": { "volume_label": f"YTSage v{version}", "applications_shortcut": True, # Sensible defaults; can be customized later if desired "format": "UDZO", "filesystem": "HFS+", "default_view": "icon-view", }, }, executables=executables, ) PY - name: Build .app bundle (bdist_mac) shell: bash run: | source venv/bin/activate python setup_cxfreeze.py bdist_mac # Locate the built .app (cx_Freeze may place it under build/ or dist/) app_path="" for cand in "dist/YTSage-v${VERSION}.app" "build/dist/YTSage-v${VERSION}.app" "build/YTSage-v${VERSION}.app"; do if [ -d "$cand" ]; then app_path="$cand"; break; fi done if [ -z "$app_path" ]; then app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) fi if [ -n "$app_path" ] && [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" echo "Removed screenshots folder from .app bundle at $app_path" fi echo "Post-bdist_mac directory listing:" echo "-- dist --"; ls -lah dist || true echo "-- build --"; ls -lah build || true - name: Build DMG (bdist_dmg) shell: bash run: | source venv/bin/activate python setup_cxfreeze.py bdist_dmg echo "Post-bdist_dmg directory listing:" echo "-- dist --"; ls -lah dist || true echo "-- build --"; ls -lah build || true - name: Package and prepare release artifacts (.app.zip and .dmg) shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" echo "Preparing artifacts for version: $version" mkdir -p artifacts # Find the .app bundle (preferring versioned name) app_path="" for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do if [ -d "$cand" ]; then app_path="$cand"; break; fi done if [ -z "$app_path" ]; then app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) fi if [ -n "$app_path" ] && [ -d "$app_path" ]; then app_base="$(basename "$app_path")" app_parent="$(dirname "$app_path")" # Ensure screenshots folder is not shipped if [ -d "$app_path/Contents/Resources/lib/assets/branding/screenshots" ]; then rm -rf "$app_path/Contents/Resources/lib/assets/branding/screenshots" echo "Removed screenshots folder from .app bundle at $app_path" fi (cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base") echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" else echo "Warning: .app bundle not found in dist/ or build/" fi # Try to find the generated DMG in dist or build dmg_src="" for cand in "dist/YTSage-v${version}.dmg"; do if [ -f "$cand" ]; then dmg_src="$cand"; break; fi done if [ -z "$dmg_src" ]; then dmg_src=$(ls dist/*.dmg build/*.dmg build/dist/*.dmg 2>/dev/null | head -n1 || true) fi if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then # Rename the DMG to a consistent name in artifacts cp "$dmg_src" "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" echo "Copied DMG to artifacts from $dmg_src -> artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" else echo "Warning: No DMG found in dist/ or build/" fi echo "Final artifacts:" ls -lh artifacts || true - name: Create/Update draft release uses: softprops/action-gh-release@v2 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 }}