Build Desktop App (Cross-Platform) #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Desktop App (Cross-Platform) | |
| on: | |
| schedule: | |
| - cron: '0 0 * * *' # Daily at 8 AM Beijing time (00:00 UTC) | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Release version (e.g. 1.0.0-beta.1)' | |
| required: false | |
| default: '' | |
| electron_repo: | |
| description: 'Electron frontend repo (owner/repo)' | |
| required: false | |
| default: 'Project-N-E-K-O/N.E.K.O.-PC' | |
| electron_ref: | |
| description: 'Electron repo branch/tag/commit' | |
| required: false | |
| default: 'main' | |
| skip_signing: | |
| description: 'Skip code signing' | |
| required: false | |
| default: 'true' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| env: | |
| PYTHON_VERSION: '3.11' | |
| NODE_VERSION: '20' | |
| jobs: | |
| # ────────────────────────────────────────────── | |
| # Step 0: Calculate version | |
| # ────────────────────────────────────────────── | |
| version: | |
| if: github.repository == 'Project-N-E-K-O/N.E.K.O' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.calc.outputs.VERSION }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Calculate version | |
| id: calc | |
| run: | | |
| if [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| elif [[ -n "${{ github.event.inputs.version }}" ]]; then | |
| VERSION="${{ github.event.inputs.version }}" | |
| else | |
| COMMIT_SHORT=$(git rev-parse --short HEAD) | |
| DATE=$(date +%Y%m%d) | |
| VERSION="${DATE}-${COMMIT_SHORT}" | |
| fi | |
| echo "VERSION=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Resolved version: $VERSION" | |
| # ────────────────────────────────────────────── | |
| # Step 1: Build Python backend with Nuitka | |
| # ────────────────────────────────────────────── | |
| build-python: | |
| if: github.repository == 'Project-N-E-K-O/N.E.K.O' | |
| needs: version | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| platform: win | |
| artifact_name: python-backend-win | |
| output_binary: projectneko_server.exe | |
| - os: macos-15 | |
| platform: mac-x64 | |
| python_arch: x64 | |
| artifact_name: python-backend-mac-x64 | |
| output_binary: projectneko_server | |
| - os: macos-latest | |
| platform: mac-arm64 | |
| artifact_name: python-backend-mac-arm64 | |
| output_binary: projectneko_server | |
| - os: ubuntu-latest | |
| platform: linux | |
| artifact_name: python-backend-linux | |
| output_binary: projectneko_server | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 300 | |
| steps: | |
| - name: Checkout backend repo | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ env.PYTHON_VERSION }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| architecture: ${{ matrix.python_arch || '' }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Create venv and install dependencies | |
| shell: bash | |
| run: | | |
| uv venv .venv | |
| uv sync | |
| uv pip install --python .venv nuitka ordered-set zstandard | |
| # --- Generate runtime config files (not tracked in git) --- | |
| - name: Generate default config files | |
| shell: bash | |
| run: | | |
| python -c " | |
| import json, os | |
| configs = { | |
| 'config/characters.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/characters.en.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/characters.ja.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/characters.ko.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/characters.zh-CN.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/characters.zh-TW.json': {'主人': {'档案名': '', '性别': '', '昵称': ''}, '猫娘': {}, '当前猫娘': ''}, | |
| 'config/core_config.json': {}, | |
| 'config/user_preferences.json': [], | |
| } | |
| for path, data in configs.items(): | |
| if not os.path.exists(path): | |
| with open(path, 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| print(f'Created {path}') | |
| " | |
| # Copy browser_use prompt .md templates from installed package | |
| mkdir -p data/browser_use_prompts | |
| VENV_PYTHON=".venv/bin/python" | |
| if [ -f ".venv/Scripts/python.exe" ]; then | |
| VENV_PYTHON=".venv/Scripts/python.exe" | |
| fi | |
| $VENV_PYTHON -c " | |
| import shutil | |
| from pathlib import Path | |
| import browser_use.agent.system_prompts as sp | |
| src = Path(sp.__file__).parent | |
| dst = Path('data/browser_use_prompts') | |
| dst.mkdir(parents=True, exist_ok=True) | |
| for md in src.glob('*.md'): | |
| shutil.copy2(md, dst / md.name) | |
| print(f'Copied {md.name}') | |
| " | |
| echo "=== browser_use_prompts ===" | |
| ls -la data/browser_use_prompts/ | |
| echo "Default config files and directories ready." | |
| # --- Platform-specific system dependencies --- | |
| - name: Install Linux system dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| build-essential \ | |
| pkg-config \ | |
| portaudio19-dev \ | |
| patchelf \ | |
| ccache | |
| - name: Install macOS build tools | |
| if: runner.os == 'macOS' | |
| run: | | |
| brew install ccache portaudio || true | |
| # --- Pre-install Playwright Chromium for bundle --- | |
| - name: Install Playwright Chromium (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| mkdir -p playwright_browsers | |
| PLAYWRIGHT_BROWSERS_PATH="$(pwd)/playwright_browsers" \ | |
| .venv/bin/python -m playwright install chromium || \ | |
| echo "[WARNING] Playwright Chromium install failed; will download on first run." | |
| - name: Install Playwright Chromium (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| if (-not (Test-Path "playwright_browsers")) { New-Item -ItemType Directory -Path "playwright_browsers" } | |
| $env:PLAYWRIGHT_BROWSERS_PATH = "${{ github.workspace }}\playwright_browsers" | |
| .venv\Scripts\python.exe -m playwright install chromium | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Warning "Playwright Chromium install failed; will download on first run." | |
| } | |
| # --- Build with Nuitka --- | |
| - name: Build with Nuitka (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| NUITKA_OPTS="--standalone" | |
| NUITKA_OPTS="$NUITKA_OPTS --output-dir=dist" | |
| NUITKA_OPTS="$NUITKA_OPTS --output-filename=projectneko_server" | |
| # Config data files | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/__init__.py=config/__init__.py" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/api_providers.json=config/api_providers.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.json=config/characters.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.en.json=config/characters.en.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.ja.json=config/characters.ja.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.ko.json=config/characters.ko.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.zh-CN.json=config/characters.zh-CN.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/characters.zh-TW.json=config/characters.zh-TW.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/core_config.json=config/core_config.json" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/prompts_chara.py=config/prompts_chara.py" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/prompts_sys.py=config/prompts_sys.py" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=config/user_preferences.json=config/user_preferences.json" | |
| # Data directories | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=assets=assets" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=templates=templates" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=data/browser_use_prompts=data/browser_use_prompts" | |
| # Plugin runtime assets/configs: include exported frontend and bundled plugins | |
| # explicitly, even when they are gitignored locally. | |
| if [ -d "plugin/frontend/exported" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=plugin/frontend/exported=plugin/frontend/exported" | |
| fi | |
| if [ -d "plugin/plugins" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=plugin/plugins=plugin/plugins" | |
| fi | |
| # macOS: skip bundling playwright_browsers (Chromium .app path has spaces | |
| # that break Nuitka's xattr call; browser will download on first run) | |
| if [[ "$RUNNER_OS" != "macOS" ]]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=playwright_browsers=playwright_browsers" | |
| fi | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-dir=static=static" | |
| # Packages | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=uvicorn" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=fastapi" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=starlette" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=jinja2" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=websockets" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=langchain_community" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=langchain_core" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=langchain_openai" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=main_server" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=memory_server" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=agent_server" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=config" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=plugin" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=brain" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=main_logic" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=main_routers" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=memory" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=utils" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=steamworks" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=browser_use" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=browser_use_sdk" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package=playwright" | |
| # Package data | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=audiolab" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=pyrnnoise" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=browser_use" | |
| # NOTE: do NOT use --include-package-data=playwright here, | |
| # Nuitka's built-in playwright plugin handles driver/node automatically | |
| # and adding package-data would conflict with the exe at the same path. | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=jinja2" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=certifi" | |
| NUITKA_OPTS="$NUITKA_OPTS --include-package-data=bilibili_api" | |
| # Exclusions | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=plugin.plugins" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=audiolab" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=pyrnnoise" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=matplotlib" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=pytest" | |
| # macOS: exclude pyobjc frameworks (Foundation requires --mode=app) | |
| if [[ "$RUNNER_OS" == "macOS" ]]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=Foundation" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=AppKit" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=objc" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=PyObjCTools" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=CoreFoundation" | |
| NUITKA_OPTS="$NUITKA_OPTS --nofollow-import-to=Quartz" | |
| fi | |
| # Plugin & CI flags | |
| NUITKA_OPTS="$NUITKA_OPTS --enable-plugin=dill-compat" | |
| NUITKA_OPTS="$NUITKA_OPTS --assume-yes-for-downloads" | |
| # Steam files (include if present in repo) | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=steam_appid.txt=steam_appid.txt" | |
| if [ -f "libsteam_api.dylib" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=libsteam_api.dylib=libsteam_api.dylib" | |
| fi | |
| if [ -f "SteamworksPy.dylib" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=SteamworksPy.dylib=SteamworksPy.dylib" | |
| fi | |
| if [ -f "libsteam_api.so" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=libsteam_api.so=libsteam_api.so" | |
| fi | |
| if [ -f "SteamworksPy.so" ]; then | |
| NUITKA_OPTS="$NUITKA_OPTS --include-data-files=SteamworksPy.so=SteamworksPy.so" | |
| fi | |
| echo "=== Nuitka options ===" | |
| echo "$NUITKA_OPTS" | tr ' ' '\n' | |
| echo "======================" | |
| .venv/bin/python -m nuitka $NUITKA_OPTS launcher.py | |
| - name: Build with Nuitka (Windows) | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| set NUITKA_OPTS=--standalone --output-dir="dist" --output-filename="projectneko_server.exe" | |
| set NUITKA_OPTS=%NUITKA_OPTS% --windows-icon-from-ico=assets/icon.ico | |
| set NUITKA_OPTS=%NUITKA_OPTS% --company-name="Project N.E.K.O." | |
| set NUITKA_OPTS=%NUITKA_OPTS% --product-name="N.E.K.O. AI Assistant" | |
| set NUITKA_OPTS=%NUITKA_OPTS% --file-version=1.0.0.0 | |
| set NUITKA_OPTS=%NUITKA_OPTS% --product-version=1.0.0.0 | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/__init__.py=config/__init__.py | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/api_providers.json=config/api_providers.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.json=config/characters.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.en.json=config/characters.en.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.ja.json=config/characters.ja.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.ko.json=config/characters.ko.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.zh-CN.json=config/characters.zh-CN.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/characters.zh-TW.json=config/characters.zh-TW.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/core_config.json=config/core_config.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/prompts_chara.py=config/prompts_chara.py | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/prompts_sys.py=config/prompts_sys.py | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=config/user_preferences.json=config/user_preferences.json | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=assets=assets | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=templates=templates | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=data/browser_use_prompts=data/browser_use_prompts | |
| if exist "plugin\frontend\exported" set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=plugin/frontend/exported=plugin/frontend/exported | |
| if exist "plugin\plugins" set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=plugin/plugins=plugin/plugins | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=playwright_browsers=playwright_browsers | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-dir=static=static | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=uvicorn | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=fastapi | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=starlette | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=jinja2 | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=websockets | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=langchain_community | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=langchain_core | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=langchain_openai | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=main_server | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=memory_server | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=agent_server | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=config | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=plugin | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=brain | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=main_logic | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=main_routers | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=memory | |
| set NUITKA_OPTS=%NUITKA_OPTS% --nofollow-import-to=plugin.plugins | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=utils | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=steamworks | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=steam_api64.dll=steam_api64.dll | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=steam_api64.lib=steam_api64.lib | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=SteamworksPy64.dll=SteamworksPy64.dll | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-data-files=steam_appid.txt=steam_appid.txt | |
| set NUITKA_OPTS=%NUITKA_OPTS% --nofollow-import-to=audiolab | |
| set NUITKA_OPTS=%NUITKA_OPTS% --nofollow-import-to=pyrnnoise | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=audiolab | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=pyrnnoise | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=browser_use | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=browser_use | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=browser_use_sdk | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package=playwright | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=playwright | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=jinja2 | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=certifi | |
| set NUITKA_OPTS=%NUITKA_OPTS% --include-package-data=bilibili_api | |
| set NUITKA_OPTS=%NUITKA_OPTS% --enable-plugin=dill-compat | |
| set NUITKA_OPTS=%NUITKA_OPTS% --nofollow-import-to=matplotlib | |
| set NUITKA_OPTS=%NUITKA_OPTS% --nofollow-import-to=pytest | |
| set NUITKA_OPTS=%NUITKA_OPTS% --windows-console-mode=force | |
| set NUITKA_OPTS=%NUITKA_OPTS% --assume-yes-for-downloads | |
| .venv\Scripts\python.exe -m nuitka %NUITKA_OPTS% launcher.py | |
| # --- Rename Nuitka output directory --- | |
| - name: Rename build output | |
| shell: bash | |
| run: | | |
| if [ -d "dist/launcher.dist" ]; then | |
| mv dist/launcher.dist dist/Xiao8 | |
| fi | |
| # macOS: playwright_browsers was excluded from Nuitka (xattr bug with | |
| # spaces in .app paths), copy it manually into the output directory | |
| if [[ "$RUNNER_OS" == "macOS" && -d "playwright_browsers" ]]; then | |
| cp -R playwright_browsers dist/Xiao8/playwright_browsers | |
| echo "Copied playwright_browsers into dist/Xiao8/" | |
| fi | |
| echo "=== Build output ===" | |
| ls -la dist/Xiao8/ || echo "dist/Xiao8 not found!" | |
| - name: Upload Python backend artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: dist/Xiao8/ | |
| retention-days: 7 | |
| compression-level: 6 | |
| # ────────────────────────────────────────────── | |
| # Step 2: Build Electron desktop app | |
| # ────────────────────────────────────────────── | |
| build-electron: | |
| if: github.repository == 'Project-N-E-K-O/N.E.K.O' | |
| needs: [version, build-python] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| platform: win | |
| python_artifact: python-backend-win | |
| electron_args: '--win' | |
| artifact_name: desktop-win-x64 | |
| - os: macos-15 | |
| platform: mac-x64 | |
| python_artifact: python-backend-mac-x64 | |
| electron_args: '--mac --x64' | |
| artifact_name: desktop-mac-x64 | |
| - os: macos-latest | |
| platform: mac-arm64 | |
| python_artifact: python-backend-mac-arm64 | |
| electron_args: '--mac --arm64' | |
| artifact_name: desktop-mac-arm64 | |
| - os: ubuntu-latest | |
| platform: linux | |
| python_artifact: python-backend-linux | |
| electron_args: '--linux' | |
| artifact_name: desktop-linux-x64 | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 180 | |
| steps: | |
| - name: Checkout Electron frontend | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event.inputs.electron_repo || 'Project-N-E-K-O/N.E.K.O.-PC' }} | |
| ref: ${{ github.event.inputs.electron_ref || 'main' }} | |
| token: ${{ secrets.ELECTRON_REPO_TOKEN || secrets.GITHUB_TOKEN }} | |
| path: electron-app | |
| - name: Set up Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| cache-dependency-path: electron-app/package-lock.json | |
| - name: Download Python backend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ matrix.python_artifact }} | |
| path: electron-app/bin | |
| - name: Make server binary executable (Unix) | |
| if: runner.os != 'Windows' | |
| run: chmod +x electron-app/bin/projectneko_server | |
| # --- Cache Electron binary to avoid download failures --- | |
| - name: Cache Electron binary | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/AppData/Local/electron/Cache | |
| ~/Library/Caches/electron | |
| ~/.cache/electron | |
| key: electron-${{ matrix.os }}-${{ matrix.platform }}-${{ hashFiles('electron-app/package-lock.json') }} | |
| restore-keys: | | |
| electron-${{ matrix.os }}-${{ matrix.platform }}- | |
| - name: Install npm dependencies | |
| working-directory: electron-app | |
| run: npm ci || npm install | |
| - name: Update version in package.json | |
| working-directory: electron-app | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| pkg.version = '${{ needs.version.outputs.version }}'.replace(/^[^0-9]*/, ''); | |
| if (!/^\d+\.\d+\.\d+/.test(pkg.version)) { | |
| pkg.version = '0.0.0-' + pkg.version; | |
| } | |
| fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| console.log('Updated version to:', pkg.version); | |
| " | |
| - name: Install Linux build dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| dpkg \ | |
| fakeroot | |
| # Explicitly disable macOS code signing via package.json patch | |
| # N.E.K.O.-PC has hardenedRuntime, afterSign (notarize.js), and entitlements | |
| # which all require a valid signing identity; disable them all for CI builds | |
| - name: Disable macOS code signing | |
| if: runner.os == 'macOS' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing == 'true')) | |
| working-directory: electron-app | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| pkg.build = pkg.build || {}; | |
| pkg.build.mac = pkg.build.mac || {}; | |
| pkg.build.mac.identity = null; | |
| pkg.build.mac.hardenedRuntime = false; | |
| delete pkg.build.mac.entitlements; | |
| delete pkg.build.mac.entitlementsInherit; | |
| delete pkg.build.afterSign; | |
| fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| console.log('macOS code signing fully disabled'); | |
| " | |
| # Linux .deb requires a maintainer email; N.E.K.O.-PC package.json | |
| # only has author name without email, so patch it here | |
| - name: Set Linux deb maintainer | |
| if: runner.os == 'Linux' | |
| working-directory: electron-app | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| pkg.build = pkg.build || {}; | |
| pkg.build.linux = pkg.build.linux || {}; | |
| pkg.build.linux.maintainer = 'Project N.E.K.O. <projectneko@yahoo.com>'; | |
| fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| console.log('Set Linux deb maintainer'); | |
| " | |
| # --- Build Electron app --- | |
| - name: Build Electron app | |
| working-directory: electron-app | |
| run: npx electron-builder ${{ matrix.electron_args }} --publish never | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing == 'true')) && 'false' || 'true' }} | |
| WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} | |
| WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} | |
| CSC_LINK: ${{ secrets.MAC_CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: List build output | |
| working-directory: electron-app/dist | |
| shell: bash | |
| run: | | |
| echo "=== Build artifacts ===" | |
| ls -lahR . || true | |
| - name: Upload desktop artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: | | |
| electron-app/dist/*.exe | |
| electron-app/dist/*.dmg | |
| electron-app/dist/*.zip | |
| electron-app/dist/*.AppImage | |
| electron-app/dist/*.deb | |
| electron-app/dist/*.tar.gz | |
| electron-app/dist/*.blockmap | |
| electron-app/dist/latest*.yml | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # ────────────────────────────────────────────── | |
| # Step 3: Publish Nightly Release | |
| # ────────────────────────────────────────────── | |
| nightly: | |
| if: github.repository == 'Project-N-E-K-O/N.E.K.O' && always() && !cancelled() && needs.build-python.result == 'success' && needs.build-electron.result != 'failure' | |
| needs: [version, build-python, build-electron] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Organize release files | |
| run: | | |
| mkdir -p release | |
| VERSION="${{ needs.version.outputs.version }}" | |
| # Zip Python backend artifacts per platform | |
| for dir in artifacts/python-backend-*; do | |
| [ -d "$dir" ] || continue | |
| name=$(basename "$dir") | |
| echo "Zipping $name ..." | |
| (cd "$dir" && zip -qr "../../release/${name}-${VERSION}.zip" .) | |
| done | |
| # Collect Electron desktop artifacts | |
| find artifacts/desktop-* -type f \( \ | |
| -name "*.exe" -o -name "*.dmg" -o -name "*.zip" \ | |
| -o -name "*.AppImage" -o -name "*.deb" -o -name "*.tar.gz" \ | |
| -o -name "latest*.yml" \ | |
| \) -exec cp {} release/ \; 2>/dev/null || true | |
| echo "=== Nightly release files ===" | |
| ls -lah release/ | |
| - name: Delete old nightly release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release delete nightly --yes --cleanup-tag 2>/dev/null || true | |
| - name: Create nightly release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.version.outputs.version }}" | |
| DATE=$(date +%Y-%m-%d) | |
| gh release create nightly release/* \ | |
| --title "Nightly Build ${DATE}" \ | |
| --target "${{ github.sha }}" \ | |
| --prerelease \ | |
| --notes "$(cat <<NOTES | |
| ## Nightly Build ${DATE} (${VERSION}) | |
| > ⚠️ This is an **unsigned** nightly build for testing purposes. | |
| ### Desktop App (Electron + Python backend) | |
| | Platform | File | | |
| |----------|------| | |
| | Windows x64 | \`N.E.K.O.exe\` (Portable) | | |
| | macOS Intel | \`N.E.K.O-*.dmg\` / \`.zip\` | | |
| | macOS Apple Silicon | \`N.E.K.O-*-arm64.dmg\` / \`.zip\` | | |
| | Linux x64 | \`N.E.K.O-*.AppImage\` / \`.deb\` / \`.tar.gz\` | | |
| ### Python Backend Only (standalone, no Electron) | |
| | Platform | File | | |
| |----------|------| | |
| | Windows x64 | \`python-backend-win-*.zip\` | | |
| | macOS Intel | \`python-backend-mac-x64-*.zip\` | | |
| | macOS Apple Silicon | \`python-backend-mac-arm64-*.zip\` | | |
| | Linux x64 | \`python-backend-linux-*.zip\` | | |
| --- | |
| Built from [\`${VERSION}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}) | |
| NOTES | |
| )" |