Skip to content

Electron Release

Electron Release #5

name: Electron Release
# Triggered by a version tag push (v*) or a manual run.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v1.0.0)"
required: true
default: "v0.0.1"
binary_version:
description: "acestep.cpp release tag to bundle (e.g. v0.0.1)"
required: false
default: "v0.0.1"
concurrency:
group: electron-release-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
# acestep.cpp binary release to bundle; override via workflow_dispatch input.
BINARY_VERSION: ${{ github.event.inputs.binary_version || 'v0.0.1' }}
# ──────────────────────────────────────────────────────────────────────────────
# Shared setup steps are defined as a reusable composite action inline via
# `run` steps repeated in each job. Each job is self-contained so the CI
# log is easy to read and debug per platform.
#
# Archive layout (flat tarball — all files at ./):
# bin/<binary> ace-qwen3, dit-vae, neural-codec, …
# bin/lib<name>.so Linux shared libraries (unversioned names)
# bin/lib<name>.dylib macOS dylibs (versioned + symlink chain)
# ──────────────────────────────────────────────────────────────────────────────
jobs:
# ────────────────────────────────────────────────────────────────────────────
# macOS — Apple Silicon → ACE-Step UI-*.dmg
# ────────────────────────────────────────────────────────────────────────────
build-mac:
name: Build — macOS arm64
runs-on: macos-14
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm install
- name: Install server dependencies
run: npm install
working-directory: server
- name: Build frontend
run: npm run build
- name: Build server
run: npm run build
working-directory: server
- name: Download & extract acestep.cpp binaries
shell: bash
run: |
ARCHIVE="acestep-macos-arm64-metal.tar.gz"
URL="https://github.com/audiohacking/acestep.cpp/releases/download/${BINARY_VERSION}/${ARCHIVE}"
echo "Downloading ${ARCHIVE} from ${URL} …"
curl -fsSL --retry 3 "${URL}" -o "${ARCHIVE}"
mkdir -p bin
tar -xzf "${ARCHIVE}" -C bin/
echo "bin/ contents:"
ls -lh bin/
- name: Verify macOS dylibs
shell: bash
run: |
# macOS dylibs ship as versioned files + two-level symlink chain:
# lib<name>.0.9.7.dylib → lib<name>.0.dylib → lib<name>.dylib
GGML_VER="0.9.7"
warn=0
for base in libggml libggml-base libggml-metal libggml-cpu libggml-blas; do
for name in "${base}.${GGML_VER}.dylib" "${base}.0.dylib" "${base}.dylib"; do
if [ -e "bin/${name}" ]; then echo "✅ bin/${name}"
else echo "⚠️ bin/${name} — missing"; warn=1; fi
done
done
for bin in ace-qwen3 dit-vae neural-codec; do
if [ -f "bin/${bin}" ] && [ -x "bin/${bin}" ]; then echo "✅ bin/${bin}"
else echo "⚠️ bin/${bin} — not found or not executable"; warn=1; fi
done
[ "$warn" = "0" ] || echo "⚠️ Some files missing — verify BINARY_VERSION=${BINARY_VERSION}"
- name: Rebuild native modules for Electron
run: npx @electron/rebuild --module-dir server --only better-sqlite3
- name: Build Electron app bundle (unpacked)
# Build the unpacked .app directory so we can sign it before packaging.
# electron-builder --dir skips DMG creation; we create the DMG ourselves
# after signing so the delivered image contains a properly signed bundle.
run: npm run electron:build:mac -- --dir
env:
CSC_IDENTITY_AUTO_DISCOVERY: false # we sign manually below
- name: Code sign the app bundle
shell: bash
run: |
APP="$(find release/mac-arm64 -maxdepth 1 -name '*.app' | head -1)"
if [ -z "$APP" ]; then
echo "❌ No .app bundle found in release/mac-arm64/"
ls -lh release/mac-arm64/ || true
exit 1
fi
echo "App bundle: $APP"
chmod +x codesign.sh
./codesign.sh "$APP"
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY || '-' }}
- name: Create DMG
shell: bash
run: |
APP="$(find release/mac-arm64 -maxdepth 1 -name '*.app' | head -1)"
APP_NAME="$(basename "$APP" .app)"
VERSION="$(node -p "require('./package.json').version")"
DMG_NAME="${APP_NAME}-${VERSION}-arm64.dmg"
echo "Creating DMG: ${DMG_NAME}"
mkdir -p dist_dmg
cp -R "$APP" dist_dmg/
# Add Applications symlink for drag-and-drop install
ln -sf /Applications dist_dmg/Applications
hdiutil create \
-volname "${APP_NAME}" \
-srcfolder dist_dmg \
-ov \
-format UDZO \
"release/${DMG_NAME}"
echo "✅ DMG: release/${DMG_NAME}"
ls -lh "release/${DMG_NAME}"
- name: Upload macOS artifact
uses: actions/upload-artifact@v4
with:
name: electron-macos-arm64
path: release/**/*.dmg
if-no-files-found: warn
retention-days: 7
# ────────────────────────────────────────────────────────────────────────────
# Linux — x86_64 → ACE-Step UI-*.AppImage + ACE-Step UI-*.snap
#
# The Linux ELFs have a hardcoded RUNPATH pointing to the CI build tree.
# At runtime electron/main.js prepends BIN_DIR to LD_LIBRARY_PATH so the
# bundled shared libraries are found regardless.
#
# The archive ships unversioned .so names (libggml.so) but ELFs link against
# versioned sonames (libggml.so.0). We create the missing symlinks before
# packaging so electron-builder includes them in extraResources.
# ────────────────────────────────────────────────────────────────────────────
build-linux:
name: Build — Linux x64
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm install
- name: Install server dependencies
run: npm install
working-directory: server
- name: Build frontend
run: npm run build
- name: Build server
run: npm run build
working-directory: server
- name: Download & extract acestep.cpp binaries
shell: bash
run: |
ARCHIVE="acestep-linux-x64.tar.gz"
URL="https://github.com/audiohacking/acestep.cpp/releases/download/${BINARY_VERSION}/${ARCHIVE}"
echo "Downloading ${ARCHIVE} from ${URL} …"
curl -fsSL --retry 3 "${URL}" -o "${ARCHIVE}"
mkdir -p bin
tar -xzf "${ARCHIVE}" -C bin/
echo "bin/ contents:"
ls -lh bin/
- name: Create versioned soname symlinks
shell: bash
run: |
# ELFs link against libggml.so.0 / libggml-base.so.0 (sonames) but
# the archive ships the unversioned names. Create the missing links.
cd bin
for pair in "libggml.so:libggml.so.0" "libggml-base.so:libggml-base.so.0"; do
real="${pair%%:*}"
soname="${pair##*:}"
if [ -f "$real" ] && [ ! -e "$soname" ]; then
ln -sv "$real" "$soname"
fi
done
echo "Symlinks:"
ls -la | grep " -> " || echo "(none)"
- name: Verify Linux binaries & libraries
shell: bash
run: |
warn=0
for bin in ace-qwen3 dit-vae neural-codec; do
if [ -f "bin/${bin}" ] && [ -x "bin/${bin}" ]; then echo "✅ bin/${bin}"
else echo "⚠️ bin/${bin} — not found or not executable"; warn=1; fi
done
for lib in libggml.so libggml-base.so libggml.so.0 libggml-base.so.0; do
if [ -e "bin/${lib}" ]; then echo "✅ bin/${lib}"
else echo "⚠️ bin/${lib} — missing"; warn=1; fi
done
[ "$warn" = "0" ] || echo "⚠️ Some files missing — verify BINARY_VERSION=${BINARY_VERSION}"
- name: Rebuild native modules for Electron
run: npx @electron/rebuild --module-dir server --only better-sqlite3
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Build Electron package (Linux)
run: npm run electron:build:linux -- --publish=never
env:
SNAPCRAFT_STORE_CREDENTIALS: "" # offline / no store upload
- name: Upload Linux artifacts
uses: actions/upload-artifact@v4
with:
name: electron-linux-x64
path: |
release/**/*.AppImage
release/**/*.snap
if-no-files-found: warn
retention-days: 7
# ────────────────────────────────────────────────────────────────────────────
# Publish a GitHub Release with all platform artifacts attached.
# ────────────────────────────────────────────────────────────────────────────
publish:
name: Publish GitHub Release
needs: [build-mac, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
pattern: electron-*
path: release-artifacts
merge-multiple: true
- name: List release artifacts
run: find release-artifacts -type f | sort
- name: Resolve release tag
id: tag
shell: bash
run: |
TAG="${GITHUB_REF_NAME:-}"
[[ "$TAG" == v* ]] || TAG="${{ github.event.inputs.tag || 'v0.0.1-electron' }}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Publish GitHub Release
# Uses the built-in GITHUB_TOKEN (contents: write granted above).
# No custom PAT/GH_TOKEN secret is required.
uses: softprops/action-gh-release@v2
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: "ACE-Step UI ${{ steps.tag.outputs.tag }} — Desktop App"
draft: false
prerelease: ${{ contains(steps.tag.outputs.tag, '-') }}
body: |
## ACE-Step UI ${{ steps.tag.outputs.tag }} — Electron Desktop App
Native desktop application with embedded server and precompiled
[acestep.cpp](https://github.com/audiohacking/acestep.cpp) binaries bundled — no compiler or build step required.
### Downloads
| File | Platform |
|------|----------|
| `*.dmg` | macOS — Apple Silicon (arm64) |
| `*.AppImage` | Linux — x86_64 (portable, any distro) |
| `*.snap` | Linux — x86_64 (Snap Store / snapd) |
### First run
On first launch the app will offer to download the default Q8_0 model set
(~8 GB total) from HuggingFace automatically. You can also skip and
copy models manually to:
- **macOS** `~/Library/Application Support/ACE-Step UI/models/`
- **Linux** `~/.config/ACE-Step UI/models/`
Generated audio is saved to `~/Music/ACEStep/`.
### macOS notes
The DMG is ad-hoc signed (no Apple notarisation required for dev builds).
If macOS still shows a "damaged or incomplete" warning, run once:
```
xattr -cr "/Applications/ACE-Step UI.app"
```
### Linux notes
**AppImage** — make executable then run:
```
chmod +x ACE-Step*.AppImage && ./ACE-Step*.AppImage
```
**Snap** — install locally (classic confinement required):
```
sudo snap install *.snap --dangerous --classic
```
files: release-artifacts/**/*
fail_on_unmatched_files: false