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 }}

      # Cache the per-target build dir and cargo-zigbuild binary. Keyed by
      # target so matrix legs don't collide. See issue on slow release CI.
      - name: Rust cache
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
        with:
          key: nvim-${{ 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
            npm_package: fff-bin-android-arm64
            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: Rust cache
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
        with:
          key: c-${{ 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
        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
        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: Rust cache
        uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
        with:
          key: mcp-${{ 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:
      # we have to make sure that pushing a commit on this workflow triggers the CI for nightly neovim
      - uses: actions/checkout@v5
        with:
          token: ${{ secrets.GUSTAV_PAT || github.token }}

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