name: Build & Publish on: push: branches: [main, fix/use-trusted-publishing] tags: - "v*" pull_request: workflow_dispatch: inputs: publish_pypi: description: "Manually build and publish Python wheels to PyPI" required: false default: false type: boolean env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build-nvim: name: Build Neovim ${{ matrix.target }} runs-on: ${{ matrix.os }} permissions: contents: read id-token: write strategy: matrix: include: ## Linux builds (using cargo-zigbuild) # Glibc 2.31 (Ubuntu 20.04, Debian 11, RHEL 9). # Rust 1.91+ requires glibc >= 2.31 for std::sys::random::getrandom, # copy_file_range, and statx; earlier targets (2.17) no longer link. - os: ubuntu-latest target: x86_64-unknown-linux-gnu zigbuild_target: x86_64-unknown-linux-gnu.2.31 artifact_name: target/x86_64-unknown-linux-gnu/ci/libfff_nvim.so ext: so - os: ubuntu-latest target: aarch64-unknown-linux-gnu zigbuild_target: aarch64-unknown-linux-gnu.2.31 artifact_name: target/aarch64-unknown-linux-gnu/ci/libfff_nvim.so ext: so # Musl (statically linked) - os: ubuntu-latest target: x86_64-unknown-linux-musl artifact_name: target/x86_64-unknown-linux-musl/ci/libfff_nvim.so ext: so - os: ubuntu-latest target: aarch64-unknown-linux-musl artifact_name: target/aarch64-unknown-linux-musl/ci/libfff_nvim.so ext: so ## Android (Termux) - os: ubuntu-latest target: aarch64-linux-android artifact_name: target/aarch64-linux-android/ci/libfff_nvim.so ext: so ## macOS builds - os: macos-latest target: x86_64-apple-darwin artifact_name: target/x86_64-apple-darwin/ci/libfff_nvim.dylib ext: dylib - os: macos-latest target: aarch64-apple-darwin artifact_name: target/aarch64-apple-darwin/ci/libfff_nvim.dylib ext: dylib - os: windows-latest target: x86_64-pc-windows-msvc artifact_name: target/x86_64-pc-windows-msvc/ci/fff_nvim.dll ext: dll - os: windows-latest target: aarch64-pc-windows-msvc artifact_name: target/aarch64-pc-windows-msvc/ci/fff_nvim.dll ext: dll steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Install Rust run: rustup target add ${{ matrix.target }} - name: Install Zig uses: mlugg/setup-zig@v2 with: version: 0.16.0 - name: Install cargo-zigbuild if: contains(matrix.os, 'ubuntu') run: cargo install cargo-zigbuild - name: Build for Linux if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android') run: | cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-nvim --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - name: Build for Android (Termux) if: contains(matrix.target, 'android') run: | NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" # NDK clang for C deps (libgit2, lmdb, blake3) that need Bionic sysroot headers export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++" export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang" cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - name: Build for macOS if: contains(matrix.os, 'macos') run: | MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - name: Ad-hoc sign macOS binary if: contains(matrix.os, 'macos') run: codesign --force --sign - "${{ matrix.target }}.${{ matrix.ext }}" - name: Build for Windows if: contains(matrix.os, 'windows') shell: bash run: | cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: nvim-${{ matrix.target }} path: ${{ matrix.target }}.* build-c: name: Build C FFI ${{ matrix.target }} runs-on: ${{ matrix.os }} permissions: contents: read strategy: matrix: include: ## Linux builds - os: ubuntu-latest target: x86_64-unknown-linux-gnu zigbuild_target: x86_64-unknown-linux-gnu.2.31 artifact_name: target/x86_64-unknown-linux-gnu/ci/libfff_c.so npm_package: fff-bin-linux-x64-gnu lib_filename: libfff_c.so ext: so - os: ubuntu-latest target: aarch64-unknown-linux-gnu zigbuild_target: aarch64-unknown-linux-gnu.2.31 artifact_name: target/aarch64-unknown-linux-gnu/ci/libfff_c.so npm_package: fff-bin-linux-arm64-gnu lib_filename: libfff_c.so ext: so - os: ubuntu-latest target: x86_64-unknown-linux-musl artifact_name: target/x86_64-unknown-linux-musl/ci/libfff_c.so npm_package: fff-bin-linux-x64-musl lib_filename: libfff_c.so ext: so - os: ubuntu-latest target: aarch64-unknown-linux-musl artifact_name: target/aarch64-unknown-linux-musl/ci/libfff_c.so npm_package: fff-bin-linux-arm64-musl lib_filename: libfff_c.so ext: so ## Android (Termux) - os: ubuntu-latest target: aarch64-linux-android artifact_name: target/aarch64-linux-android/ci/libfff_c.so lib_filename: libfff_c.so ext: so ## macOS builds - os: macos-latest target: x86_64-apple-darwin artifact_name: target/x86_64-apple-darwin/ci/libfff_c.dylib npm_package: fff-bin-darwin-x64 lib_filename: libfff_c.dylib ext: dylib - os: macos-latest target: aarch64-apple-darwin artifact_name: target/aarch64-apple-darwin/ci/libfff_c.dylib npm_package: fff-bin-darwin-arm64 lib_filename: libfff_c.dylib ext: dylib ## Windows builds - os: windows-latest target: x86_64-pc-windows-msvc artifact_name: target/x86_64-pc-windows-msvc/ci/fff_c.dll npm_package: fff-bin-win32-x64 lib_filename: fff_c.dll ext: dll - os: windows-latest target: aarch64-pc-windows-msvc artifact_name: target/aarch64-pc-windows-msvc/ci/fff_c.dll npm_package: fff-bin-win32-arm64 lib_filename: fff_c.dll ext: dll steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Install Rust run: rustup target add ${{ matrix.target }} - name: Install Zig uses: mlugg/setup-zig@v2 with: version: 0.16.0 - name: Install cargo-zigbuild if: contains(matrix.os, 'ubuntu') run: cargo install cargo-zigbuild - name: Build for Linux if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android') run: | cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-c --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - name: Build for Android (Termux) if: contains(matrix.target, 'android') run: | NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++" export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang" cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - name: Build for macOS if: contains(matrix.os, 'macos') run: | MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - name: Ad-hoc sign macOS binary if: contains(matrix.os, 'macos') run: codesign --force --sign - "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - name: Build for Windows if: contains(matrix.os, 'windows') shell: bash run: | cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - name: Prepare npm package if: "!contains(matrix.target, 'android')" shell: bash run: | # Copy the built binary into the platform npm package directory cp "c-lib-${{ matrix.target }}.${{ matrix.ext }}" "packages/${{ matrix.npm_package }}/${{ matrix.lib_filename }}" - name: Upload C library artifact uses: actions/upload-artifact@v4 with: name: c-lib-${{ matrix.target }} path: c-lib-${{ matrix.target }}.* - name: Upload npm package artifact if: "!contains(matrix.target, 'android')" uses: actions/upload-artifact@v4 with: name: npm-${{ matrix.npm_package }} path: packages/${{ matrix.npm_package }}/ build-mcp: name: Build MCP ${{ matrix.target }} runs-on: ${{ matrix.os }} permissions: contents: read strategy: matrix: include: ## Linux builds (using cargo-zigbuild) - os: ubuntu-latest target: x86_64-unknown-linux-gnu zigbuild_target: x86_64-unknown-linux-gnu.2.31 artifact_name: target/x86_64-unknown-linux-gnu/ci/fff-mcp - os: ubuntu-latest target: aarch64-unknown-linux-gnu zigbuild_target: aarch64-unknown-linux-gnu.2.31 artifact_name: target/aarch64-unknown-linux-gnu/ci/fff-mcp - os: ubuntu-latest target: x86_64-unknown-linux-musl artifact_name: target/x86_64-unknown-linux-musl/ci/fff-mcp - os: ubuntu-latest target: aarch64-unknown-linux-musl artifact_name: target/aarch64-unknown-linux-musl/ci/fff-mcp ## macOS builds - os: macos-latest target: x86_64-apple-darwin artifact_name: target/x86_64-apple-darwin/ci/fff-mcp - os: macos-latest target: aarch64-apple-darwin artifact_name: target/aarch64-apple-darwin/ci/fff-mcp ## Windows builds - os: windows-latest target: x86_64-pc-windows-msvc artifact_name: target/x86_64-pc-windows-msvc/ci/fff-mcp.exe - os: windows-latest target: aarch64-pc-windows-msvc artifact_name: target/aarch64-pc-windows-msvc/ci/fff-mcp.exe steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Install Rust run: rustup target add ${{ matrix.target }} - name: Install Zig uses: mlugg/setup-zig@v2 with: version: 0.16.0 - name: Install cargo-zigbuild if: contains(matrix.os, 'ubuntu') run: cargo install cargo-zigbuild - name: Build for Linux if: contains(matrix.os, 'ubuntu') run: | cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-mcp --no-default-features --features zlob cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}" - name: Build for macOS if: contains(matrix.os, 'macos') run: | MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-mcp --no-default-features --features zlob cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}" - name: Ad-hoc sign macOS binary if: contains(matrix.os, 'macos') run: codesign --force --sign - "fff-mcp-${{ matrix.target }}" - name: Build for Windows if: contains(matrix.os, 'windows') shell: bash run: | cargo build --profile ci --target ${{ matrix.target }} -p fff-mcp --no-default-features --features zlob cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}.exe" - name: Upload artifact uses: actions/upload-artifact@v4 with: name: mcp-${{ matrix.target }} path: fff-mcp-${{ matrix.target }}* build-python: name: Build Python wheels ${{ matrix.target }} (${{ matrix.os }}) # Wheels are release artifacts; PR validation uses the develop build in # python.yml, so skip the cross-compile matrix on pull requests. if: github.event_name != 'pull_request' runs-on: ${{ matrix.os }} permissions: contents: read strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64 container: "off" - os: ubuntu-latest target: aarch64 container: "off" - os: macos-latest target: x86_64 - os: macos-latest target: aarch64 - os: windows-latest target: x86_64 steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install Zig uses: mlugg/setup-zig@v2 with: version: 0.16.0 - name: Install cargo-zigbuild if: contains(matrix.os, 'ubuntu') run: cargo install cargo-zigbuild - name: Install aarch64 cross compiler if: matrix.target == 'aarch64' && contains(matrix.os, 'ubuntu') run: | sudo apt-get update -qq sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - name: Build wheels uses: PyO3/maturin-action@v1 env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++ AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar with: target: ${{ matrix.target }} args: --release --out dist --no-default-features --features zlob sccache: "true" working-directory: packages/fff-python container: ${{ matrix.container || '' }} - name: Upload wheels uses: actions/upload-artifact@v4 with: name: python-wheels-${{ matrix.os }}-${{ matrix.target }} path: packages/fff-python/dist/ build-python-sdist: name: Build Python sdist if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Build sdist uses: PyO3/maturin-action@v1 with: command: sdist args: --out dist working-directory: packages/fff-python - name: Upload sdist uses: actions/upload-artifact@v4 with: name: python-sdist path: packages/fff-python/dist/ release: name: Release needs: [build-nvim, build-c, build-mcp, build-python, build-python-sdist] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) permissions: contents: write steps: - uses: actions/checkout@v5 - name: Install Lua uses: leafo/gh-actions-lua@v12 - name: Download artifacts uses: actions/download-artifact@v4 with: path: ./binaries - name: Flatten and rename Neovim artifacts working-directory: ./binaries run: | # Move nvim artifacts to root level with original naming for dir in nvim-*/; do target="${dir#nvim-}" target="${target%/}" for file in "$dir"*; do if [ -f "$file" ]; then filename=$(basename "$file") mv "$file" "./$filename" fi done rmdir "$dir" 2>/dev/null || true done - name: Flatten C library artifacts working-directory: ./binaries run: | # Move c-lib artifacts to root level for dir in c-lib-*/; do for file in "$dir"*; do if [ -f "$file" ]; then filename=$(basename "$file") mv "$file" "./$filename" fi done rmdir "$dir" 2>/dev/null || true done - name: Flatten MCP artifacts working-directory: ./binaries run: | for dir in mcp-*/; do for file in "$dir"*; do if [ -f "$file" ]; then filename=$(basename "$file") mv "$file" "./$filename" fi done rmdir "$dir" 2>/dev/null || true done - name: Move Python wheels to release directory working-directory: ./binaries run: | mkdir -p python for dir in python-wheels-*/ python-sdist/; do [ -d "$dir" ] || continue for file in "$dir"*; do if [ -f "$file" ]; then mv "$file" "python/$(basename "$file")" fi done rmdir "$dir" 2>/dev/null || true done - name: Remove npm package artifacts from release binaries working-directory: ./binaries run: | rm -rf npm-* - name: Generate checksums working-directory: ./binaries run: | ls -la for file in * python/*; do if [ -f "$file" ] && [[ ! "$file" == *.sha256 ]]; then sha256sum "$file" > "${file}.sha256" fi done - name: Determine version id: version run: lua scripts/determine-version.lua # Nightlies publish to a permanent per-sha tag (release_tag == version) so # pinned/stale installs always fetch the binary built for their own commit. # The rolling `nightly` tag is also moved to HEAD for "give me latest" tooling. - name: Move rolling nightly tag to current commit if: steps.version.outputs.is_release != 'true' run: | git tag -f nightly "${{ github.sha }}" git push -f origin refs/tags/nightly - name: Upload Release Assets uses: softprops/action-gh-release@v2 with: name: "${{ steps.version.outputs.version }}" tag_name: "${{ steps.version.outputs.release_tag }}" token: ${{ github.token }} files: | ./binaries/* ./binaries/python/* draft: false prerelease: ${{ steps.version.outputs.is_release != 'true' }} generate_release_notes: ${{ steps.version.outputs.is_release == 'true' }} body: | ${{ steps.version.outputs.is_release == 'true' && format('Release {0}', steps.version.outputs.version) || format('Nightly release from commit: {0}', github.sha) }} npm packages, rust crates and python wheels are available under this version ${{ steps.version.outputs.version }} ## Neovim Plugin - `{target}.so` / `.dylib` / `.dll` - Lua module for Neovim ## C FFI Library (for Bun/Node/Python) - `c-lib-{target}.so` / `.dylib` / `.dll` - C FFI library ## MCP Server - `fff-mcp-{target}` - MCP server binary ## Python Package - `python/*.whl` / `python/*.tar.gz` - Python wheels and sdist - Install from PyPI: `pip install fff-search` (when published) Update mcp via: ```sh curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.sh | bash ``` - name: Bump Homebrew formula (uses local checksums) if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' run: make bump-homebrew-formula VERSION="${{ steps.version.outputs.version }}" BINARIES_DIR=./binaries - name: Pin SHAs in install-mcp.sh (uses local checksums) if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' run: make bump-install-mcp-sh VERSION="${{ steps.version.outputs.version }}" BINARIES_DIR=./binaries - name: Commit formula + installer bump to main # Uses the default GITHUB_TOKEN configured by actions/checkout above. # Requires github-actions[bot] in the main branch-protection bypass list. if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' uses: stefanzweifel/git-auto-commit-action@v5 with: # Workflow runs on detached HEAD at the v* tag — explicit target needed. branch: main commit_message: "chore: bump fff-mcp release artifacts to v${{ steps.version.outputs.version }}" file_pattern: "Formula/fff-mcp.rb install-mcp.sh" commit_user_name: github-actions[bot] commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com pypi-publish: name: Publish Python wheels to PyPI needs: [build-python, build-python-sdist] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) environment: name: pypi url: https://pypi.org/p/fff-search permissions: contents: read id-token: write steps: - name: Download Python wheels and sdist uses: actions/download-artifact@v4 with: pattern: python-* path: dist merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ skip-existing: true crates-publish: name: Publish Rust crates needs: [build-nvim, build-c, build-mcp] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) permissions: contents: read id-token: write steps: - uses: actions/checkout@v5 - uses: rust-lang/crates-io-auth-action@v1 id: auth - name: Install Lua uses: leafo/gh-actions-lua@v12 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install cargo-edit run: cargo install cargo-edit --force --locked - name: Determine version id: version run: lua scripts/determine-version.lua - name: Publish crates env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} run: make publish-crates V="${{ steps.version.outputs.version }}" npm-publish: name: Publish npm packages needs: [build-c] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) permissions: contents: read id-token: write steps: - uses: actions/checkout@v5 - name: Install Lua uses: leafo/gh-actions-lua@v12 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "25" registry-url: "https://registry.npmjs.org" - name: Determine version id: version run: lua scripts/determine-version.lua - name: Download npm package artifacts uses: actions/download-artifact@v4 with: pattern: npm-* path: ./npm-packages - name: Publish platform packages run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.npm_tag }}" for pkg_dir in ./npm-packages/npm-*/; do if [ -d "$pkg_dir" ]; then pkg_name=$(node -p "require('${pkg_dir}package.json').name") echo "Publishing ${pkg_name}@${VERSION} with tag ${TAG}..." make set-npm-version PKG="$pkg_dir" VERSION="$VERSION" cd "$pkg_dir" npm publish --tag "$TAG" --access public --provenance cd - fi done - name: Publish bun package run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.npm_tag }}" echo "Publishing @ff-labs/fff-bun@${VERSION} with tag ${TAG}..." make set-npm-version PKG=packages/fff-bun VERSION="$VERSION" cd packages/fff-bun npm publish --tag "$TAG" --access public --provenance - name: Publish Node.js package run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.npm_tag }}" echo "Publishing @ff-labs/fff-node@${VERSION} with tag ${TAG}..." make set-npm-version PKG=packages/fff-node VERSION="$VERSION" cd packages/fff-node npm install npm run build npm publish --tag "$TAG" --access public --provenance - name: Publish pi-fff package run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.npm_tag }}" echo "Publishing @ff-labs/pi-fff@${VERSION} with tag ${TAG}..." make set-npm-version PKG=packages/pi-fff VERSION="$VERSION" cd packages/pi-fff npm publish --tag "$TAG" --access public --provenance