Build & release moonshine-server 🛠️ #21
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 & release moonshine-server 🛠️ | |
| # Distribution-only repo — no moonshine fork lives here. | |
| # | |
| # This workflow tracks upstream releases of the `moonshine-voice` PyPI package | |
| # (UsefulSensors' streaming STT library). On a daily schedule it checks PyPI | |
| # for the latest version; if this repo has no release for it yet, it builds | |
| # self-contained moonshine-server bundles and publishes them. | |
| # | |
| # Sources at build time: | |
| # - moonshine-voice: PyPI wheel (never forked, never vendored) | |
| # - main.py wrapper: checked out from lemonade-sdk/lemonade | |
| # (tools/moonshine-server/main.py) | |
| # | |
| # Release tag scheme: moonshine<moonshine-voice version>, e.g. moonshine0.0.62. | |
| # lemonade pins the consumed tag in src/cpp/resources/backend_versions.json | |
| # (moonshine.cpu) — new upstream versions are auto-built here, but lemonade | |
| # only switches when that pin is bumped via PR. | |
| # | |
| # Asset names match MoonshineServer::get_install_params() in lemonade: | |
| # moonshine-server-<tag>-linux-x64.tar.gz | |
| # moonshine-server-<tag>-windows-x64.zip | |
| # moonshine-server-<tag>-macos-arm64.tar.gz | |
| # (moonshine-voice publishes wheels for win x64, linux x64/aarch64, macOS | |
| # arm64 only — no Intel macOS, hence no macos-x64 asset.) | |
| # | |
| # Python is pinned to 3.12: main.py uses the `cgi` module, removed in 3.13. | |
| on: | |
| schedule: | |
| - cron: '17 6 * * *' # daily upstream check | |
| workflow_dispatch: | |
| inputs: | |
| moonshine_voice_version: | |
| description: 'moonshine-voice PyPI version to build (empty = latest)' | |
| required: false | |
| default: '' | |
| force: | |
| description: 'Rebuild even if the release already exists' | |
| type: boolean | |
| required: false | |
| default: false | |
| lemonade_ref: | |
| description: 'lemonade-sdk/lemonade ref to take main.py from' | |
| required: false | |
| default: 'main' | |
| permissions: | |
| contents: write | |
| jobs: | |
| check: | |
| name: Check upstream version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.resolve.outputs.version }} | |
| tag: ${{ steps.resolve.outputs.tag }} | |
| need_build: ${{ steps.resolve.outputs.need_build }} | |
| steps: | |
| - name: Resolve latest moonshine-voice version | |
| id: resolve | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REQUESTED: ${{ github.event.inputs.moonshine_voice_version }} | |
| FORCE: ${{ github.event.inputs.force }} | |
| LEMONADE_REF: ${{ github.event.inputs.lemonade_ref || 'main' }} | |
| run: | | |
| set -euxo pipefail | |
| if [ -n "${REQUESTED:-}" ]; then | |
| VERSION="$REQUESTED" | |
| else | |
| VERSION=$(curl -fsSL https://pypi.org/pypi/moonshine-voice/json | jq -r '.info.version') | |
| fi | |
| TAG="moonshine${VERSION}" | |
| NEED=true | |
| if [ "${FORCE:-false}" != "true" ] && gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists — nothing to do." | |
| NEED=false | |
| fi | |
| # The main.py wrapper must exist at the lemonade ref. Until the | |
| # moonshine backend PR merges to main, scheduled runs would fail | |
| # here — skip gracefully instead of going red every night. | |
| if [ "$NEED" = "true" ] && ! gh api "repos/lemonade-sdk/lemonade/contents/tools/moonshine-server/main.py?ref=${LEMONADE_REF}" --silent >/dev/null 2>&1; then | |
| echo "::notice::tools/moonshine-server/main.py not found at lemonade ref '${LEMONADE_REF}' — skipping build." | |
| NEED=false | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "need_build=$NEED" >> "$GITHUB_OUTPUT" | |
| build: | |
| name: Build (${{ matrix.platform }}) | |
| needs: check | |
| if: needs.check.outputs.need_build == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-22.04 | |
| platform: linux-x64 | |
| archive: tar.gz | |
| - os: ubuntu-22.04-arm | |
| platform: linux-arm64 | |
| archive: tar.gz | |
| - os: windows-latest | |
| platform: windows-x64 | |
| archive: zip | |
| - os: macos-latest | |
| platform: macos-arm64 | |
| archive: tar.gz | |
| env: | |
| VERSION: ${{ needs.check.outputs.version }} | |
| TAG: ${{ needs.check.outputs.tag }} | |
| steps: | |
| - name: Checkout lemonade (main.py wrapper only) | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: lemonade-sdk/lemonade | |
| ref: ${{ github.event.inputs.lemonade_ref || 'main' }} | |
| sparse-checkout: tools/moonshine-server | |
| path: lemonade | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install dependencies | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install "moonshine-voice==${VERSION}" websockets pyinstaller | |
| python -c "import moonshine_voice; print(moonshine_voice.__file__)" | |
| - name: Remove foreign-platform libraries (macOS) | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| # The macOS moonshine-voice wheel ships shared libraries for other | |
| # platforms too (e.g. a Linux ELF libmoonshine.so). PyInstaller's | |
| # Mach-O parser hard-fails on ELF inputs, so drop anything that is | |
| # not Mach-O before freezing. | |
| SITE=$(python -c "import moonshine_voice, os; print(os.path.dirname(moonshine_voice.__file__))") | |
| find "$SITE" \( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \) -print0 | | |
| while IFS= read -r -d '' f; do | |
| if ! file -b "$f" | grep -q 'Mach-O'; then | |
| echo "removing foreign binary: $f" | |
| rm -f "$f" | |
| fi | |
| done | |
| - name: Build self-contained bundle (PyInstaller onedir) | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| # --collect-all pulls in moonshine_voice's native shared libraries and | |
| # data files; websockets is imported lazily so declare it explicitly. | |
| pyinstaller --noconfirm --onedir --name moonshine-server \ | |
| --collect-all moonshine_voice \ | |
| --hidden-import websockets \ | |
| lemonade/tools/moonshine-server/main.py | |
| - name: Smoke test bundle | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| ./dist/moonshine-server/moonshine-server --help | |
| - name: Package asset (name must match get_install_params) | |
| id: package | |
| shell: bash | |
| run: | | |
| set -euxo pipefail | |
| ASSET="moonshine-server-${TAG}-${{ matrix.platform }}.${{ matrix.archive }}" | |
| cd dist | |
| if [ "${{ matrix.archive }}" = "zip" ]; then | |
| 7z a "../$ASSET" moonshine-server | |
| else | |
| tar -czf "../$ASSET" moonshine-server | |
| fi | |
| cd .. | |
| ls -lh "$ASSET" | |
| echo "asset=$ASSET" >> "$GITHUB_OUTPUT" | |
| - name: Publish release asset | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euxo pipefail | |
| ASSET="${{ steps.package.outputs.asset }}" | |
| # Create the release if it does not exist yet ("|| true" — another | |
| # matrix job may create it concurrently), then upload with --clobber | |
| # so re-runs replace the asset. | |
| if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release create "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --title "moonshine-server $TAG" \ | |
| --notes "Self-contained moonshine-server bundles (PyInstaller, embedded Python 3.12 + moonshine-voice ${VERSION} from PyPI). Wrapper: lemonade-sdk/lemonade tools/moonshine-server/main.py." \ | |
| || true | |
| fi | |
| gh release upload "$TAG" "$ASSET" --repo "$GITHUB_REPOSITORY" --clobber |