macrame-db 0.13.0

A Bitemporal Graph Ledger on libSQL · Embedded knowledge database
Documentation
name: Wheels

# Builds the Python distribution: one wheel per platform, plus an sdist.
#
# Separate from `ci.yml` because it is a different question. CI asks whether the
# code is correct; this asks whether it *packages*, which is a property of the
# build matrix and the target toolchains rather than of the source. It runs on
# tags and by hand, not on every push — the matrix is not free (see the timings
# below) and nothing on a feature branch depends on the answer.
#
# ## abi3 is why this matrix is small
#
# `pyo3` is built with `abi3-py310`, so one wheel serves CPython 3.10 through
# whatever ships next: the extension links only the stable ABI. Without it this
# file would be a 4-platform × 5-version matrix of 20 builds of a crate that
# compiles the SQLite amalgamation each time. That trade — measured at ~35 µs
# per 768-dim embedding for the buffer protocol abi3 costs us — is recorded in
# §14.5 of the architecture set, and this is the other half of it.
#
# ## Measured (probe P5-a, 2026-08-01, Windows x86_64 native)
#
#   cold build, cargo clean first ... 54-62 s, 197 crates, libsql-ffi included
#   wheel .......................... 4.3 MiB compressed, 11.0 MiB unpacked
#   sdist .......................... 748 KiB, 124 members
#   pip install from sdist ......... 183 s in a fresh venv, source only
#
# **Only the native target has been timed.** The plan's stated risk is aarch64
# under QEMU emulation, where a 1-minute native build is typically 5-15x. That
# is affordable but it is not measured, and the first run of this workflow is
# what measures it. If emulated aarch64 turns out to exceed the runner budget,
# the fallback is a native arm64 runner rather than dropping the target.

on:
  push:
    tags: ["v*"]
  workflow_dispatch:
    inputs:
      publish:
        description: "Upload to PyPI (otherwise build and keep artifacts only)"
        type: boolean
        default: false

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  wheels:
    name: wheel (${{ matrix.name }})
    runs-on: ${{ matrix.os }}
    strategy:
      # One platform failing should not hide whether the others build: a matrix
      # that stops at the first red tells you about one target per run.
      fail-fast: false
      matrix:
        include:
          - name: linux-x86_64
            os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            manylinux: "2_28"
            # The builder can run what it built.
            smoke: true
          - name: linux-aarch64
            os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            manylinux: "2_28"
            # Cross-built under emulation; the runner is x86_64 and cannot
            # import an aarch64 extension. Asserted by `maturin` producing the
            # wheel and by the auditwheel tag, not by running it. An arm64
            # runner would close this, and is the same change that would fix a
            # build-time problem here.
            smoke: false
          - name: macos-universal2
            os: macos-latest
            target: universal2-apple-darwin
            manylinux: "auto"
            smoke: true
          - name: windows-x86_64
            os: windows-latest
            target: x86_64-pc-windows-msvc
            manylinux: "auto"
            smoke: true

    steps:
      - uses: actions/checkout@v5

      - name: build wheel
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.target }}
          manylinux: ${{ matrix.manylinux }}
          # `--release` because a debug-built libSQL is slow enough to be
          # misleading, and this is the artifact people install.
          args: --release --out dist
          sccache: "true"

      # The one check that catches the failure this whole file exists to
      # prevent: a wheel that builds, installs, and has no engine in it. P0's
      # `engine_linked()` is exactly that assertion, and it is why the function
      # was kept past P1.
      - name: smoke test the built wheel
        if: matrix.smoke
        shell: bash
        run: |
          python -m pip install --upgrade pip
          python -m pip install --no-index --find-links dist macrame-db
          python - <<'PY'
          import macrame
          assert macrame.engine_linked(), "wheel has no libSQL engine linked in"
          import tempfile, pathlib
          T0 = "2026-01-01T00:00:00.000000Z"
          path = pathlib.Path(tempfile.mkdtemp()) / "kb.db"
          with macrame.Database.open(path, snapshot_every_entries=None) as db:
              db.write_concepts([macrame.ConceptUpsert("a", "A", valid_from=T0)])
              db.assert_edge(macrame.EdgeAssertion("a", "a", "SELF", valid_from=T0))
              graph = db.load_subgraph("a", 1, 1 << 20)
              assert len(graph) == 1, graph
              # D-093: the wheel carries `--features metrics`, so this is a real
              # counter rather than a zero-sized type answering zero. A wheel
              # that lost the feature is a wheel whose `metrics()` lies.
              assert db.metrics().turns > 0, "wheel was built without --features metrics"
          print("ok:", macrame.__version__)
          PY

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

  sdist:
    name: sdist
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: build sdist
        uses: PyO3/maturin-action@v1
        with:
          command: sdist
          args: --out dist

      # An sdist is the fallback for every platform this matrix does not cover,
      # so "it was produced" is not the property worth checking — "it builds
      # from source, with no wheel in reach" is. `--no-binary :all:` is what
      # forces that path; without it pip would happily install the wheel this
      # workflow just built.
      - name: install from source only
        shell: bash
        run: |
          python -m pip install --upgrade pip
          python -m pip install --no-binary :all: dist/*.tar.gz
          python -c "import macrame; assert macrame.engine_linked(); print('sdist ok', macrame.__version__)"

      - uses: actions/upload-artifact@v6
        with:
          name: sdist
          path: dist/*.tar.gz
          if-no-files-found: error

  # The full Python suite, reused rather than reimplemented — the same argument
  # `release.yml` makes for calling `ci.yml`. Before P7 this file could take a
  # tag, build four wheels, pass a six-line smoke test and upload to PyPI
  # **without the 353-test suite having run at all**, on an artifact whose
  # version number cannot be spent twice. The smoke test answers "is the engine
  # in there"; it was never meant to answer "does it work".
  suite:
    name: python suite
    uses: ./.github/workflows/python.yml

  publish:
    name: publish to PyPI
    needs: [wheels, sdist, suite]
    runs-on: ubuntu-latest
    # A tag publishes; a manual run publishes only when asked. Same shape as
    # `release.yml`, and for the same reason: a PyPI version number cannot be
    # reused once taken, even after a delete, so a failed upload spends it.
    if: |
      startsWith(github.ref, 'refs/tags/v') ||
      inputs.publish == true
    environment: pypi # add a required reviewer here to gate uploads
    permissions:
      # Trusted Publishing (OIDC). **There is deliberately no API token in this
      # workflow.** PyPI mints a short-lived credential for this repository and
      # this workflow, so there is no long-lived secret to leak, rotate, or hand
      # to anyone. It has to be configured once, by the project owner, at
      # https://pypi.org/manage/project/macrame-db/settings/publishing/ —
      # naming this repository, this workflow file, and the `pypi` environment.
      id-token: write
    steps:
      - uses: actions/download-artifact@v7
        with:
          path: dist
          merge-multiple: true
      - run: ls -la dist
      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          # Re-running a tag must not be a hard failure (0.11.0).
          #
          # `v0.10.0` was re-pointed and force-pushed to carry a documentation
          # fix, because `release.yml`'s crates.io publish gates on the whole of
          # `ci.yml` and a broken intra-doc link was holding it (D-144). This
          # workflow gates on `python.yml` instead, so the wheel had already
          # published — and the re-pushed tag re-ran this job against a version
          # PyPI already had. Without this, that is a red X on a tag whose real
          # outcome is "nothing needed doing".
          #
          # **The cost is real and is accepted deliberately.** A genuine upload
          # failure caused by the version already existing — the classic case
          # being a bump that was forgotten, so the build silently republishes
          # the previous release's number — now reports success instead of
          # stopping. What makes that acceptable here is that the failure has
          # another detector that runs *first*: `release.yml`'s `check-version`
          # compares the tag against `Cargo.toml`, and `tests/packaging_tests.rs`
          # pins the wheel's `__version__` to the same manifest. A forgotten bump
          # is caught by the tag, not by PyPI's rejection.
          #
          # It does not weaken the "a version number cannot be reused" rule this
          # job's `if:` condition is written around. It changes what happens when
          # the number is not being spent at all.
          skip-existing: true