infino 0.1.8

A fast retrieval engine that stores data on object storage and runs SQL, full-text search, and vector search over it from a single system — search-on-Parquet.
Documentation
name: Publish Python package

# Release of the `infino` Python package: validate the version, build one binary
# wheel per platform/arch, then upload. The version is the one committed in
# `infino-python/Cargo.toml` (stamped by `make release-prep`). A `v<version>`
# tag publishes to real PyPI after asserting the tag matches the tree — but
# ONLY for a coordinated minor/major release (patch == 0, e.g. `v0.2.0`); a
# crate-only patch tag (e.g. `v0.1.1`) is skipped, since patch is independent
# per package. A manual run (workflow_dispatch) — the path for an independent
# Python patch — publishes the tree's version to the PyPI (default) / TestPyPI
# target.
#
# Two facts shape the whole pipeline:
#   - abi3 (pyo3 `abi3-py39`) → a single wheel per platform/arch covers all
#     CPython >= 3.9, so the matrix is platform × arch only, never × Python.
#   - The bindings depend on the core crate by path (`infino = { path = ".." }`),
#     so an sdist can't build outside this repo. Distribution is wheels-only,
#     and the matrix is kept wide enough that pip never falls back to source.

on:
  push:
    tags: ["v[0-9]*"]
  workflow_dispatch:
    inputs:
      target:
        description: "Index to publish to"
        required: true
        default: pypi
        type: choice
        options: [pypi, testpypi]

# One release per index at a time; never cancel a publish in flight. A tag push
# always targets PyPI (see the validate step), so derive the index from the
# event — `inputs.target` is empty on a push and would otherwise split tag
# releases into a separate group from manual PyPI runs.
concurrency:
  group: publish-python-${{ github.event_name == 'push' && 'pypi' || inputs.target }}
  cancel-in-progress: false

# Least privilege by default; the publish job widens to `id-token: write`
# for PyPI Trusted Publishing (OIDC). No long-lived credentials.
permissions:
  contents: read

env:
  # The bindings pull the core crate as an ordinary dependency; a transitive
  # deprecation warning must not fail a release build.
  RUSTFLAGS: ""

jobs:
  validate:
    name: Validate version
    # Coordinated-release gate: on a tag push, run only when the tag is a
    # minor/major (patch == 0, tag ends in `.0`). A crate-only patch tag skips
    # the bindings (build + publish need this job, so they skip too). A manual
    # run always proceeds (independent Python patch).
    if: ${{ github.event_name == 'workflow_dispatch' || endsWith(github.ref_name, '.0') }}
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.check.outputs.version }}
      target: ${{ steps.check.outputs.target }}
    steps:
      - uses: actions/checkout@v5
      # The version is the one committed in infino-python/Cargo.toml (stamped
      # by `make release-prep`). On a tag push, assert the tag matches it — a
      # coordinated release can't ship an unstamped tree. Reject a non-SemVer
      # version before the matrix spins up: Cargo's `version` requires SemVer
      # (it rejects PEP 440 like `0.1.0rc1`), and maturin renders SemVer to
      # the wheel's PEP 440 (`0.1.0-rc.1` → `0.1.0rc1`).
      - id: check
        run: |
          version="$(sed -n 's/^version = "\(.*\)"/\1/p' infino-python/Cargo.toml | head -1)"
          if [ "${{ github.event_name }}" = "push" ]; then
            tag="${GITHUB_REF_NAME#v}"
            if [ "$tag" != "$version" ]; then
              echo "::error::tag v$tag does not match infino-python/Cargo.toml version $version"
              exit 1
            fi
            target="pypi"
          else
            target="${{ inputs.target }}"
          fi
          if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?(\+[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then
            echo "::error::invalid release version (must be SemVer): $version"
            exit 1
          fi
          echo "version=$version" >> "$GITHUB_OUTPUT"
          echo "target=$target" >> "$GITHUB_OUTPUT"
      # The version about to publish must sit on the crate's major.minor
      # release line (docs/versioning.md), and the whole tree must be
      # version-consistent, before the wheel matrix spins up.
      - name: Check version against the crate's release line
        run: python3 scripts/check_version_sync.py --release-version "${{ steps.check.outputs.version }}"

  build:
    name: wheel ${{ matrix.target }}
    needs: validate
    runs-on: ${{ matrix.os }}
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        include:
          - { os: ubuntu-latest,    target: x86_64-unknown-linux-gnu }
          - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
          - { os: ubuntu-latest,    target: x86_64-unknown-linux-musl }
          - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-musl }
          - { os: macos-14,         target: aarch64-apple-darwin }
          - { os: macos-15-intel,   target: x86_64-apple-darwin }
          # No Windows: the core crate uses Unix-only APIs (std::os::unix
          # positional writes, memmap2 madvise) that don't compile on MSVC.
    steps:
      - uses: actions/checkout@v5

      # The core dep graph (datafusion + its sub-crates, parquet, arrow) plus
      # the build target dir overflows the ~14 GB free on a stock Linux runner;
      # reclaim ~30 GB before compiling.
      - name: Free disk space
        if: runner.os == 'Linux'
        uses: jlumbroso/free-disk-space@v1.3.1
        with:
          tool-cache: false
          android: true
          dotnet: true
          haskell: true
          large-packages: true
          docker-images: true
          swap-storage: true

      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"

      # Version is dynamic (pyproject `dynamic = ["version"]`) → maturin reads
      # it straight from the committed infino-python/Cargo.toml; `make
      # release-prep` stamps Cargo.toml and Cargo.lock together, so the build
      # stays --locked with no CI-side rewriting.
      # `manylinux` is honored only for Linux targets and ignored elsewhere;
      # musl targets need the musllinux image, every other target builds glibc.
      # `sccache` caches inside the container, avoiding a host cache whose glibc mismatches it.
      - name: Build wheel
        uses: PyO3/maturin-action@v1
        with:
          command: build
          target: ${{ matrix.target }}
          manylinux: ${{ contains(matrix.target, 'musl') && 'musllinux_1_2' || 'auto' }}
          sccache: "true"
          args: --release --locked --out dist -m infino-python/Cargo.toml

      # Smoke-test the wheel before publish; musl runs in Alpine (can't install on glibc).
      - name: Smoke-test wheel (glibc)
        if: ${{ !contains(matrix.target, 'musl') }}
        shell: bash
        run: |
          python3 -m pip install --upgrade pip
          python3 -m pip install --force-reinstall dist/*.whl pytest
          python3 -m pytest infino-python/tests -v

      - name: Smoke-test wheel (musl)
        if: ${{ contains(matrix.target, 'musl') }}
        shell: bash
        run: |
          docker run --rm -v "$PWD:/w" -w /w python:3.12-alpine sh -c '
            pip install --force-reinstall dist/*.whl pytest &&
            python -m pytest infino-python/tests -v'

      - uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.target }}
          path: dist/*.whl
          if-no-files-found: error

  publish:
    name: Publish to ${{ needs.validate.outputs.target }}
    needs: [build, validate]
    runs-on: ubuntu-latest
    # `environment` is load-bearing, not decoration: the PyPI/TestPyPI trusted
    # publisher is keyed to it (plus repo + workflow file), and it can gate
    # `pypi` behind required reviewers. `id-token: write` lets the upload
    # authenticate over OIDC instead of a stored token.
    permissions:
      id-token: write
      contents: read
    environment: ${{ needs.validate.outputs.target }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: wheels-*
          merge-multiple: true
          path: dist
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - name: Validate distributions
        run: |
          python3 -m pip install --upgrade twine
          python3 -m twine check dist/*
      # Trusted Publishing: no `password` — the action exchanges the OIDC
      # token with the index. `repository-url` selects PyPI vs TestPyPI.
      - name: Upload to ${{ needs.validate.outputs.target }}
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: dist
          repository-url: ${{ needs.validate.outputs.target == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}