v2.3.2 — bump version, rebuild all PDFs at 2.3.2 #124
Workflow file for this run
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
| # ELI v2 — cross-platform release pipeline. | |
| # | |
| # Trigger: push a tag `v<version>` (must match [project].version in | |
| # pyproject.toml — the guard job fails the run with a clear message if not). | |
| # | |
| # git tag v2.1.0 && git push origin v2.1.0 | |
| # | |
| # Builds in parallel and uploads to the GitHub Release for that tag: | |
| # Windows : ELI-Setup-<v>.exe (Inno Setup) + ELI_v2-<v>-windows-x64.zip (portable, ELI.exe inside) | |
| # macOS : ELI_v2-<v>-macos-arm64.dmg (drag-to-Applications, ad-hoc signed) | |
| # Linux : ELI_v2-<v>-x86_64.AppImage | |
| # plus SHA256SUMS.txt. | |
| # | |
| # Piper TTS voices ARE bundled (pulled from the project's local-assets-v2.1 | |
| # release — same set previous full tarballs shipped). GGUF model weights are | |
| # runtime downloads (too big to bundle). Binaries are unsigned; see | |
| # docs/RELEASE_PIPELINE.md for optional Windows/Apple signing steps. | |
| name: release | |
| on: | |
| push: | |
| tags: ["v*"] | |
| workflow_dispatch: {} # manual dry-run: builds artifacts, skips the Release | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| PYTHON_VERSION: "3.11" # newest version with binary wheels for the FULL stack (piper-phonemize, tflite-runtime cap at 3.11) | |
| PYTHONHASHSEED: "0" | |
| PIP_DISABLE_PIP_VERSION_CHECK: "1" | |
| PIP_NO_CACHE_DIR: "1" | |
| # Extras that MUST be in the frozen app; the build fails if any is missing. | |
| REQUIRED_EXTRAS: "gui llm server docs analysis extras" | |
| # Extras bundled when their wheels resolve on the runner; skipped (loudly) | |
| # otherwise — every optional import in ELI degrades cleanly by design. | |
| # NOTE: this list is consumed by ALL THREE build jobs. The XTTS-v2 neural voice engine | |
| # (`natural`) is deliberately NOT here — it is added as a job-level override on | |
| # build-linux only, which is the one job that installs the CPU torch stack first and | |
| # verifies no CUDA wheels crept in. Adding it globally makes windows/macos resolve | |
| # torch from PyPI, and their bundles then fail the 2 GiB asset guard below. | |
| OPTIONAL_EXTRAS: "tts asr image vision web wakeword" | |
| jobs: | |
| guard: | |
| name: version guard | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.ver.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Whole-tree syntax gate at the BUILD python version | |
| # The v2.1.0 exe shipped broken because one file used py3.12-only | |
| # f-string syntax while builds run py3.11: PyInstaller only WARNS on | |
| # uncompilable modules. Fail the release up front instead. | |
| run: | | |
| python -m compileall -q eli api || { | |
| echo "::error::source does not compile under the release Python ($PYTHON_VERSION) — fix the file above (requires-python is >=3.10, so no 3.12-only syntax)" | |
| exit 1 | |
| } | |
| - name: Read version from pyproject.toml and check the tag matches | |
| id: ver | |
| run: | | |
| VERSION=$(python3 -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "pyproject version: $VERSION" | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| if [[ "$TAG" != "v$VERSION" ]]; then | |
| echo "::error::Tag '$TAG' does not match pyproject.toml version '$VERSION'. Bump [project].version (and eli/kernel/self_upgrade.py + tests/test_pyproject_packaging.py) or re-tag as v$VERSION." | |
| exit 1 | |
| fi | |
| fi | |
| build-windows: | |
| env: | |
| ELI_REQUIRE_VOICES: "1" | |
| ELI_REQUIRE_EMBEDDER: "1" | |
| GH_TOKEN: ${{ github.token }} | |
| name: windows (exe + installer) | |
| needs: guard | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Set SOURCE_DATE_EPOCH (reproducible timestamps) | |
| shell: bash | |
| run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" | |
| - name: Install dependencies (core hard-fails; optional extras degrade loudly) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip wheel | |
| pip install -r requirements-build.txt | |
| pip install ".[$(echo $REQUIRED_EXTRAS | tr ' ' ',')]" | |
| pip install llama-cpp-python --prefer-binary \ | |
| --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \ | |
| || pip install llama-cpp-python | |
| for extra in $OPTIONAL_EXTRAS; do | |
| pip install ".[$extra]" \ | |
| || echo "::warning::optional extra '$extra' has no installable wheels on windows — shipped build degrades cleanly without it" | |
| done | |
| - name: Generate capability manifests (bundled introspection data) | |
| shell: bash | |
| run: | | |
| python -m eli.core.init_data | |
| python tools/bootstrap_claims_artifacts.py | |
| - name: Bundle Piper voices (from the project's local-assets release) | |
| shell: bash | |
| run: | | |
| mkdir -p tts_piper/piper | |
| gh release download local-assets-v2.1 --repo "$GITHUB_REPOSITORY" --clobber \ | |
| --pattern "*.onnx" --pattern "*.onnx.json" --dir tts_piper/piper \ | |
| || { echo "::error::could not download voices from the local-assets-v2.1 release"; exit 1; } | |
| count=$(ls tts_piper/piper/*.onnx 2>/dev/null | wc -l) | |
| echo "voices downloaded: $count" | |
| [ "$count" -ge 1 ] || { echo "::error::no .onnx voices present after download"; exit 1; } | |
| - name: Bundle text embedder (nomic-embed, required for memory/RAG) | |
| shell: bash | |
| run: | | |
| # Ship the ~84 MB embedder so the packaged app has semantic memory | |
| # offline instead of degrading to keyword-only recall. Download straight | |
| # to the repo-relative path ELI.spec bundles from — NOT via model_download, | |
| # whose models_dir() resolves to a runtime data dir that differs per OS. | |
| set -euo pipefail | |
| dest="models/embeddings/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| url="https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| mkdir -p models/embeddings | |
| curl -fL --retry 3 --retry-delay 5 -o "$dest" "$url" \ | |
| || { echo "::error::embedder download failed from $url"; exit 1; } | |
| # Guard against an HTML error page saved as the .gguf (size sanity: >50 MB). | |
| bytes=$(wc -c < "$dest") | |
| [ "$bytes" -gt 52428800 ] \ | |
| || { echo "::error::embedder is only $bytes bytes — download did not return the model"; exit 1; } | |
| echo "embedder: $(( bytes / 1048576 )) MiB at $dest" | |
| - name: PyInstaller build (ELI.spec) | |
| shell: bash | |
| run: pyinstaller --noconfirm --clean ELI.spec | |
| - name: Smoke check — bundle structure + REAL launch selftest | |
| shell: bash | |
| run: | | |
| test -f dist/ELI/ELI.exe || { echo "::error::dist/ELI/ELI.exe missing"; exit 1; } | |
| test -f dist/ELI/ELI-Server.exe || { echo "::error::dist/ELI/ELI-Server.exe missing"; exit 1; } | |
| test -d dist/ELI/_internal/eli || { echo "::error::eli source tree missing from bundle"; exit 1; } | |
| test -f dist/ELI/_internal/config/settings.example.json || { echo "::error::config seeds missing from bundle"; exit 1; } | |
| # Boot the frozen app's import stack for real — catches missing | |
| # hidden imports / syntax errors that only appear at runtime. | |
| if ! ./dist/ELI/ELI.exe --selftest; then | |
| echo "::error::frozen app failed its launch selftest:" | |
| cat eli_selftest_error.log 2>/dev/null || true | |
| exit 1 | |
| fi | |
| - name: Reclaim disk before packaging | |
| shell: bash | |
| run: rm -rf build/ELI tts_piper | |
| - name: Portable zip | |
| shell: bash | |
| run: | | |
| V=${{ needs.guard.outputs.version }} | |
| 7z a -tzip "dist/ELI_v2-$V-windows-x64.zip" ./dist/ELI | |
| - name: Download VC++ redistributable (bundled into installer) | |
| shell: bash | |
| run: | | |
| mkdir -p build | |
| # aka.ms is a redirect to a Microsoft CDN that occasionally 5xx/times out — | |
| # retry so a transient hiccup can't fail an otherwise-green release (it did | |
| # once, skipping publish while Linux/macOS built fine). | |
| curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 30 \ | |
| -o build/vc_redist.x64.exe https://aka.ms/vs/17/release/vc_redist.x64.exe | |
| test -s build/vc_redist.x64.exe || { echo "::error::vc_redist download failed after retries"; exit 1; } | |
| - name: Inno Setup installer | |
| shell: pwsh | |
| run: | | |
| # The licence pane reads a generated UTF-8+BOM copy of LICENSE; Inno | |
| # decodes a BOM-less UTF-8 file as ANSI and mangles it. | |
| python scripts\make_installer_license.py | |
| if ($LASTEXITCODE -ne 0) { Write-Error "could not generate the installer licence file"; exit 1 } | |
| $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" | |
| if (-not (Test-Path $iscc)) { choco install innosetup -y --no-progress; $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" } | |
| if (-not (Test-Path $iscc)) { Write-Error "Inno Setup 6 (ISCC.exe) not found and choco install failed"; exit 1 } | |
| & $iscc packaging\windows\installer.iss /DMyAppVersion=${{ needs.guard.outputs.version }} | |
| if ($LASTEXITCODE -ne 0) { Write-Error "ISCC failed with exit code $LASTEXITCODE"; exit 1 } | |
| - name: Checksums + GitHub 2 GiB asset-size guard | |
| shell: bash | |
| run: | | |
| cd dist | |
| rm -rf ELI | |
| for f in *.zip *.exe; do | |
| [ -e "$f" ] || continue | |
| sha256sum "$f" >> SHA256SUMS-windows.txt | |
| size=$(stat -c%s "$f") | |
| if [ "$size" -ge 2147483648 ]; then | |
| echo "::error::$f is $size bytes — over GitHub's 2 GiB release-asset limit. Trim OPTIONAL_EXTRAS in release.yml." | |
| exit 1 | |
| fi | |
| done | |
| cat SHA256SUMS-windows.txt | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: eli-windows | |
| retention-days: 3 # build intermediates; the publish job consumes them immediately | |
| path: | | |
| dist/*.zip | |
| dist/*.exe | |
| dist/SHA256SUMS-windows.txt | |
| if-no-files-found: error | |
| build-macos: | |
| env: | |
| ELI_REQUIRE_VOICES: "1" | |
| ELI_REQUIRE_EMBEDDER: "1" | |
| GH_TOKEN: ${{ github.token }} | |
| name: macos (app + dmg, arm64) | |
| needs: guard | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Set SOURCE_DATE_EPOCH (reproducible timestamps) | |
| run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" | |
| - name: System build deps (portaudio for PyAudio) | |
| run: brew install portaudio | |
| - name: Install dependencies (core hard-fails; optional extras degrade loudly) | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip wheel | |
| pip install -r requirements-build.txt | |
| pip install ".[$(echo $REQUIRED_EXTRAS | tr ' ' ',')]" | |
| pip install llama-cpp-python --prefer-binary \ | |
| --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/metal \ | |
| || pip install llama-cpp-python | |
| for extra in $OPTIONAL_EXTRAS; do | |
| pip install ".[$extra]" \ | |
| || echo "::warning::optional extra '$extra' has no installable wheels on macos-arm64 — shipped build degrades cleanly without it" | |
| done | |
| - name: Generate capability manifests (bundled introspection data) | |
| run: | | |
| python -m eli.core.init_data | |
| python tools/bootstrap_claims_artifacts.py | |
| - name: Bundle Piper voices (from the project's local-assets release) | |
| run: | | |
| mkdir -p tts_piper/piper | |
| gh release download local-assets-v2.1 --repo "$GITHUB_REPOSITORY" --clobber \ | |
| --pattern "*.onnx" --pattern "*.onnx.json" --dir tts_piper/piper \ | |
| || { echo "::error::could not download voices from the local-assets-v2.1 release"; exit 1; } | |
| count=$(ls tts_piper/piper/*.onnx 2>/dev/null | wc -l) | |
| echo "voices downloaded: $count" | |
| [ "$count" -ge 1 ] || { echo "::error::no .onnx voices present after download"; exit 1; } | |
| - name: Bundle text embedder (nomic-embed, required for memory/RAG) | |
| shell: bash | |
| run: | | |
| # Ship the ~84 MB embedder so the packaged app has semantic memory | |
| # offline instead of degrading to keyword-only recall. Download straight | |
| # to the repo-relative path ELI.spec bundles from — NOT via model_download, | |
| # whose models_dir() resolves to a runtime data dir that differs per OS. | |
| set -euo pipefail | |
| dest="models/embeddings/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| url="https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| mkdir -p models/embeddings | |
| curl -fL --retry 3 --retry-delay 5 -o "$dest" "$url" \ | |
| || { echo "::error::embedder download failed from $url"; exit 1; } | |
| bytes=$(wc -c < "$dest") | |
| [ "$bytes" -gt 52428800 ] \ | |
| || { echo "::error::embedder is only $bytes bytes — download did not return the model"; exit 1; } | |
| echo "embedder: $(( bytes / 1048576 )) MiB at $dest" | |
| - name: PyInstaller build (ELI.spec) | |
| run: pyinstaller --noconfirm --clean ELI.spec | |
| - name: Smoke check — .app structure + REAL launch selftest | |
| run: | | |
| test -x dist/ELI.app/Contents/MacOS/ELI || { echo "::error::ELI.app executable missing"; exit 1; } | |
| test -d dist/ELI.app/Contents/Frameworks/eli -o -d dist/ELI.app/Contents/Resources/eli || { echo "::error::eli source tree missing from .app"; exit 1; } | |
| if ! ./dist/ELI.app/Contents/MacOS/ELI --selftest; then | |
| echo "::error::frozen app failed its launch selftest:" | |
| cat eli_selftest_error.log 2>/dev/null || true | |
| exit 1 | |
| fi | |
| - name: Build .dmg (ad-hoc signed) | |
| run: bash packaging/macos/build-dmg.sh | |
| - name: Checksums + GitHub 2 GiB asset-size guard | |
| run: | | |
| cd dist | |
| rm -rf ELI ELI.app | |
| for f in *.dmg; do | |
| shasum -a 256 "$f" >> SHA256SUMS-macos.txt | |
| size=$(stat -f%z "$f") | |
| if [ "$size" -ge 2147483648 ]; then | |
| echo "::error::$f is $size bytes — over GitHub's 2 GiB release-asset limit. Trim OPTIONAL_EXTRAS in release.yml." | |
| exit 1 | |
| fi | |
| done | |
| cat SHA256SUMS-macos.txt | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: eli-macos | |
| retention-days: 3 # build intermediates; the publish job consumes them immediately | |
| path: | | |
| dist/*.dmg | |
| dist/SHA256SUMS-macos.txt | |
| if-no-files-found: error | |
| build-linux: | |
| env: | |
| ELI_REQUIRE_VOICES: "1" | |
| ELI_REQUIRE_EMBEDDER: "1" | |
| GH_TOKEN: ${{ github.token }} | |
| # The XTTS-v2 neural engine is deliberately NOT bundled. It shipped briefly in | |
| # v2.1.72 and was pulled: the CPU-only torch the bundle must use (a CUDA build | |
| # blows GitHub's 2 GiB asset limit) means XTTS runs on CPU, and on a GPU machine | |
| # the startup picker pinned ELI_XTTS_DEVICE=cuda, so it crashed with "Torch not | |
| # compiled with CUDA enabled" — and even working, it costs GPU/CPU far out of | |
| # proportion to a voice. Piper is the shipped engine. Cloning still works from a | |
| # source install (`pip install -e ".[natural]"`), where torch matches the host. | |
| name: linux (AppImage) | |
| needs: guard | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Set SOURCE_DATE_EPOCH (reproducible timestamps) | |
| run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" | |
| - name: Free runner disk (full dependency stack + staging needs it) | |
| run: | | |
| sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup | |
| df -h / | |
| - name: System build deps (portaudio, espeak, Qt/GL runtime libs) | |
| run: | | |
| sudo apt-get update | |
| # xcb-util family: Qt 6.5+'s xcb platform plugin (libqxcb.so) links the | |
| # whole set at runtime (libxcb-cursor/icccm/image/keysyms/render-util/util). | |
| # PyInstaller only bundles what is present at build time, so these MUST be | |
| # installed or the AppImage fails on lean end-user distros — Qt reports it | |
| # misleadingly as "libxcb-cursor0 is needed" whichever member is missing. | |
| # xvfb: virtual X server for the real-xcb GUI selftest in the smoke check. | |
| sudo apt-get install -y --no-install-recommends \ | |
| portaudio19-dev libasound2-dev libegl1 libgl1 libxkbcommon-x11-0 \ | |
| libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ | |
| libxcb-render-util0 libxcb-util1 espeak-ng xvfb | |
| - name: Install dependencies (core hard-fails; optional extras degrade loudly) | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip wheel | |
| pip install -r requirements-build.txt | |
| # CPU torch stack FIRST — default PyPI torch drags ~6 GB of CUDA libs into the | |
| # bundle. torchaudio is included here for the same reason: the `natural` | |
| # (XTTS-v2) extra requires it, and resolving it from PyPI afterwards pulls the | |
| # CUDA build of torch straight back in. | |
| pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu | |
| pip install ".[$(echo $REQUIRED_EXTRAS | tr ' ' ',')]" | |
| pip install llama-cpp-python --prefer-binary \ | |
| --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \ | |
| || pip install llama-cpp-python | |
| for extra in $OPTIONAL_EXTRAS; do | |
| pip install ".[$extra]" \ | |
| || echo "::warning::optional extra '$extra' has no installable wheels on linux — shipped build degrades cleanly without it" | |
| done | |
| # An extra that quietly re-resolved torch from PyPI would pull the CUDA build | |
| # (~6 GB of nvidia-* wheels) and push the AppImage past GitHub's 2 GB per-asset | |
| # limit — the exact failure that kept `natural` out of the bundle before. Fail | |
| # here, where the cause is obvious, rather than at upload. | |
| if pip list --format=freeze | grep -q '^nvidia-'; then | |
| echo "::error::CUDA wheels were pulled in (nvidia-*) — the torch stack is no longer CPU-only:" | |
| pip list --format=freeze | grep '^nvidia-' | sed 's/^/ /' | |
| exit 1 | |
| fi | |
| python - <<'PY' | |
| import importlib | |
| for mod, why in (("torch", "neural voice engine"), ("TTS", "XTTS-v2 (coqui-tts)")): | |
| try: | |
| importlib.import_module(mod) | |
| print(f" {mod}: present ({why})") | |
| except Exception as e: | |
| print(f"::warning::{mod} unavailable — {why} will degrade to Piper: {e}") | |
| PY | |
| - name: Generate capability manifests (bundled introspection data) | |
| run: | | |
| python -m eli.core.init_data | |
| python tools/bootstrap_claims_artifacts.py | |
| - name: Bundle Piper voices (from the project's local-assets release) | |
| run: | | |
| mkdir -p tts_piper/piper | |
| gh release download local-assets-v2.1 --repo "$GITHUB_REPOSITORY" --clobber \ | |
| --pattern "*.onnx" --pattern "*.onnx.json" --dir tts_piper/piper \ | |
| || { echo "::error::could not download voices from the local-assets-v2.1 release"; exit 1; } | |
| count=$(ls tts_piper/piper/*.onnx 2>/dev/null | wc -l) | |
| echo "voices downloaded: $count" | |
| [ "$count" -ge 1 ] || { echo "::error::no .onnx voices present after download"; exit 1; } | |
| - name: Bundle text embedder (nomic-embed, required for memory/RAG) | |
| shell: bash | |
| run: | | |
| # Ship the ~84 MB embedder so the packaged app has semantic memory | |
| # offline instead of degrading to keyword-only recall. Download straight | |
| # to the repo-relative path ELI.spec bundles from — NOT via model_download, | |
| # whose models_dir() resolves to a runtime data dir that differs per OS. | |
| set -euo pipefail | |
| dest="models/embeddings/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| url="https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q4_K_M.gguf" | |
| mkdir -p models/embeddings | |
| curl -fL --retry 3 --retry-delay 5 -o "$dest" "$url" \ | |
| || { echo "::error::embedder download failed from $url"; exit 1; } | |
| bytes=$(wc -c < "$dest") | |
| [ "$bytes" -gt 52428800 ] \ | |
| || { echo "::error::embedder is only $bytes bytes — download did not return the model"; exit 1; } | |
| echo "embedder: $(( bytes / 1048576 )) MiB at $dest" | |
| - name: PyInstaller build (ELI.spec) | |
| run: | | |
| pyinstaller --noconfirm --clean ELI.spec 2>&1 | tee /tmp/pyi.log | |
| # collect_submodules imports the package in an ISOLATED SUBPROCESS, so a | |
| # package that only imports with an in-process shim fails collection and | |
| # PyInstaller merely WARNS — buried in ~250k log lines. That is how four | |
| # releases shipped an AppImage whose neural engine could not load. | |
| if grep -q "Failed to collect submodules for 'TTS'" /tmp/pyi.log; then | |
| echo "::error::PyInstaller could not collect TTS — the neural engine will not load:" | |
| grep -m1 -A2 "Failed to collect submodules for 'TTS'" /tmp/pyi.log | |
| exit 1 | |
| fi | |
| - name: Smoke check — bundle structure + REAL launch selftest (headless + xcb) | |
| run: | | |
| test -x dist/ELI/ELI || { echo "::error::dist/ELI/ELI missing"; exit 1; } | |
| test -x dist/ELI/ELI-Server || { echo "::error::dist/ELI/ELI-Server missing"; exit 1; } | |
| test -d dist/ELI/_internal/eli || { echo "::error::eli source tree missing from bundle"; exit 1; } | |
| # The neural voice engine must be COLLECTED, not merely pip-installed. | |
| # v2.1.65 and v2.1.66 both shipped coqui_tts-*.dist-info with no TTS/ | |
| # package: the spec's import probe raised on a transformers incompatibility | |
| # and silently skipped collection, so every cloned voice fell back to Piper | |
| # with the extra sitting unused in the bundle. Metadata is not the package. | |
| if ls dist/ELI/_internal/coqui_tts-*.dist-info >/dev/null 2>&1; then | |
| if [ ! -d dist/ELI/_internal/TTS ]; then | |
| echo "::error::coqui-tts is installed but the TTS/ package was NOT collected — voice cloning would silently fall back to Piper" | |
| exit 1 | |
| fi | |
| echo "TTS/ package collected: $(du -sh dist/ELI/_internal/TTS | cut -f1)" | |
| fi | |
| # Self-containment guard: the Qt xcb plugin (libqxcb.so) links the whole | |
| # xcb-util family at runtime and resolves them via its RUNPATH from | |
| # PySide6/Qt/lib. Assert EACH member is bundled IN THAT EXACT DIRECTORY — | |
| # v2.1.20 bundled libxcb-cursor into the bundle root instead, which the | |
| # plugin never searches, so the GUI still failed on lean distros. Checking | |
| # the precise dir (not just "present somewhere") is what closes that gap. | |
| QTLIBDIR=dist/ELI/_internal/PySide6/Qt/lib | |
| for _lib in libxcb-cursor.so libxcb-icccm.so libxcb-image.so \ | |
| libxcb-keysyms.so libxcb-render-util.so libxcb-util.so; do | |
| if ! ls "$QTLIBDIR/$_lib"* >/dev/null 2>&1; then | |
| echo "::error::$_lib is NOT bundled in $QTLIBDIR — Qt xcb GUI will fail on minimal distros" | |
| exit 1 | |
| fi | |
| done | |
| # Headless engine selftest (offscreen Qt platform). | |
| if ! QT_QPA_PLATFORM=offscreen ./dist/ELI/ELI --selftest; then | |
| echo "::error::frozen app failed its headless launch selftest:" | |
| cat eli_selftest_error.log 2>/dev/null || true | |
| exit 1 | |
| fi | |
| # Real-X GUI selftest under a virtual display — exercises the xcb platform | |
| # plugin the offscreen path never touches. THIS is the check that would have | |
| # caught the missing-libxcb-cursor gap; keep it so it can't regress. | |
| if ! xvfb-run -a env QT_QPA_PLATFORM=xcb ./dist/ELI/ELI --selftest; then | |
| echo "::error::frozen app failed the xcb (real X) launch selftest — a Qt GUI dependency is missing from the bundle:" | |
| cat eli_selftest_error.log 2>/dev/null || true | |
| exit 1 | |
| fi | |
| - name: Build AppImage | |
| run: bash packaging/linux/build-appimage-pyinstaller.sh | |
| - name: Classic portable tarball (source + voices — parity with previous releases) | |
| run: | | |
| pip install build | |
| SKIP_TESTS=1 bash scripts/package_desktop_app.sh --with-assets | |
| mv dist/app_packages/ELI_v2-*-linux-portable.tar.gz dist/ | |
| ls -lh dist/*.tar.gz | |
| - name: Checksums + GitHub 2 GiB asset-size guard | |
| run: | | |
| cd dist | |
| rm -rf ELI | |
| for f in *.AppImage *.tar.gz; do | |
| [ -e "$f" ] || continue | |
| sha256sum "$f" >> SHA256SUMS-linux.txt | |
| size=$(stat -c%s "$f") | |
| if [ "$size" -ge 2147483648 ]; then | |
| echo "::error::$f is $size bytes — over GitHub's 2 GiB release-asset limit. Trim OPTIONAL_EXTRAS in release.yml." | |
| exit 1 | |
| fi | |
| done | |
| cat SHA256SUMS-linux.txt | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: eli-linux | |
| retention-days: 3 # build intermediates; the publish job consumes them immediately | |
| path: | | |
| dist/*.AppImage | |
| dist/*.tar.gz | |
| dist/SHA256SUMS-linux.txt | |
| if-no-files-found: error | |
| release: | |
| name: publish GitHub Release | |
| if: startsWith(github.ref, 'refs/tags/') | |
| needs: [guard, build-windows, build-macos, build-linux] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: Merge checksums | |
| run: | | |
| cd dist | |
| cat SHA256SUMS-*.txt > SHA256SUMS.txt | |
| rm SHA256SUMS-*.txt | |
| ls -lh | |
| - name: Create/update GitHub Release and upload artifacts | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| name: ELI v${{ needs.guard.outputs.version }} | |
| generate_release_notes: true | |
| files: | | |
| dist/*.zip | |
| dist/*.exe | |
| dist/*.dmg | |
| dist/*.AppImage | |
| dist/*.tar.gz | |
| dist/SHA256SUMS.txt | |
| fail_on_unmatched_files: true |