Skip to content

Build Desktop App (Cross-Platform) #28

Build Desktop App (Cross-Platform)

Build Desktop App (Cross-Platform) #28

Workflow file for this run

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
)"