Repository navigation
T1.6c docs: the packed refusal masks resolution's refusals #449
Workflow file for this run
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
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. | |
| # All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # Every gate a change has to pass, run where a contributor can see it. | |
| # | |
| # One gate is not here: the contract check reads the canonical OpenAPI spec | |
| # from where it is maintained, which a fork cannot reach. The generated | |
| # bindings are pinned by SHA-256 in PROVENANCE.json either way, and a drifted | |
| # copy fails the build it ships in. | |
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| # The Rust core migration's epic branch and its phase branches run the | |
| # same gates as main; the reference and archive branches deliberately | |
| # do not, including a lane package's archive branch, | |
| # rust-core-phase-1-archive-*, which keeps an abandoned tip (E43, CI2). | |
| branches: | |
| - main | |
| - rust-core | |
| - 'rust-core-phase-*' | |
| - '!rust-core-phase-1-archive-*' | |
| permissions: | |
| contents: read | |
| # A newer push cancels a topic branch's run in progress. On the phase | |
| # branch and the lane branches a run in progress is not cancelled (E43): a | |
| # landing's runs there are recorded after the fast-forward, so the next | |
| # landing or the lane's next package must not cancel them. A run still | |
| # queued there can be: GitHub keeps one pending run per group, and a newer | |
| # push supersedes it. No landing depends on that run alone; land.py verify | |
| # reports a cancelled run, and passes on another green run of the same tree. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ !(github.ref_name == 'rust-core-phase-1' || startsWith(github.ref_name, 'rust-core-phase-1-lane-')) }} | |
| env: | |
| # Whether this run saves its caches: the Rust builds and pip's downloads. | |
| # GitHub restores a push's caches only from its own branch and the default | |
| # branch, so a cache a topic branch saves serves that branch alone, and | |
| # those filled the repository's 10 GiB. Only the long-lived branches save, | |
| # and the lane branches: each parallel lane of packages pushes to one lane | |
| # branch it reuses, so a package's first run restores the builds the | |
| # lane's previous package saved (decision E43). Every run restores what | |
| # its own branch saved; a run that saves nothing still runs every check. | |
| SAVE_CACHES: ${{ github.event_name == 'push' && (contains(fromJSON('["main", "rust-core", "rust-core-phase-1"]'), github.ref_name) || startsWith(github.ref_name, 'rust-core-phase-1-lane-')) }} | |
| # The manylinux2014 image both wheel builds run in, pinned by digest. | |
| MANYLINUX: quay.io/pypa/manylinux2014_x86_64:2026.09.14-1@sha256:21c37461985655aaa25ed3a923b28e6c9d4dd9e10edc0281eb50385184bddd31 | |
| # The binding's tests run at the E16 floor; DuckDB, at the reference's | |
| # version, is for the DuckDB provider's tests there (T1.11b). | |
| BINDING_FLOOR: pyarrow==14.0.1 pandas==2.2.0 numpy==1.26.4 duckdb==1.5.6 | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - run: pip install --upgrade pip 'ruff==0.16.*' | |
| - run: ruff check . | |
| - run: ruff format --check . | |
| hygiene: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - run: python scripts/check_repo_hygiene.py all | |
| # The Rust core (docs/design/rust-core/phase-1/design.md, section CI). Each | |
| # job installs the toolchain rust/rust-toolchain.toml pins, and caches the | |
| # build keyed on Cargo.lock. Third-party actions are pinned by commit, as | |
| # the organisation's allow list requires. | |
| rust-lint: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| with: | |
| workspaces: rust | |
| save-if: ${{ env.SAVE_CACHES == 'true' }} | |
| - run: cargo fmt --all --check | |
| # sdm-testkit builds the bundled DuckDB, so its clippy runs in rust-test, | |
| # which builds it anyway (H3); the L0 self-tests' clippy fixture needs | |
| # only jiff, and stays here. | |
| - run: cargo clippy --workspace --exclude sdm-testkit --all-targets --locked -- -D warnings | |
| # The binding with its test-only operations, as its tests build it. | |
| - run: cargo clippy -p sdm-py --features test-ops --locked -- -D warnings | |
| # The L0 guards (T1.10c): the crate graph (l0_deps.py), with the | |
| # bridge freeze (bridge_freeze.py, E50), whose baseline ratchets | |
| # against the merge base with origin/rust-core-phase-1, which the full | |
| # checkout above fetches; and the vocabulary, SQL-fragment, skip and | |
| # ignore, clippy-ban and step-variant scans (l0_scan.py). The bans | |
| # themselves are enforced by the clippy runs above, from | |
| # rust/clippy.toml. | |
| - name: L0 graph, bans and scans | |
| run: | | |
| python3 scripts/rust_core/l0_deps.py | |
| python3 scripts/rust_core/l0_scan.py | |
| working-directory: . | |
| # Each guard passes on the tree and fails on a fixture seeded with the | |
| # fault it exists for, and clippy refuses each banned item in a fixture | |
| # crate under rust/clippy.toml. | |
| - name: L0 self-tests | |
| run: python3 scripts/rust_core/selftest.py | |
| working-directory: . | |
| # land.py's checks, each passing a sound landing and refusing a seeded | |
| # fault, against a scripted git and gh; lane.py's lane rules and | |
| # land.py's on a lane, against real temporary git repositories and a | |
| # fake gh; and prune_caches.py's stale-cache rules; with no network | |
| # (decision E43, CI2). | |
| - name: land.py, lane.py and prune_caches.py self-tests | |
| run: | | |
| python3 scripts/rust_core/land_selftest.py | |
| python3 scripts/rust_core/lane_selftest.py | |
| python3 scripts/rust_core/prune_caches.py --selftest | |
| working-directory: . | |
| # The contract schema: regenerated from the types and compared with the | |
| # committed file, locked to the current version; no entry merged into | |
| # rust-core rewritten, and at most one working entry, which is why this | |
| # job checks out every branch (for the merge base with rust-core). | |
| - run: cargo test -p sdm-contract --locked --test schema_lock | |
| - run: python3 scripts/rust_core/contract_lock.py | |
| working-directory: . | |
| # The client wheel's notices for the Rust crates it links, regenerated | |
| # from Cargo.lock and compared with the committed file. | |
| - run: python3 scripts/rust_core/rust_notices.py --check | |
| working-directory: . | |
| rust-deny: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - run: rustup toolchain install | |
| - uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3 # v2.87.16 | |
| with: | |
| tool: cargo-deny | |
| - run: cargo deny --locked check | |
| rust-test: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| # Every branch, for the gate's checkpoint (the merge base with the | |
| # phase branch) and its worktree. | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| with: | |
| workspaces: rust | |
| save-if: ${{ env.SAVE_CACHES == 'true' }} | |
| # The test host's ICU, DuckDB's signed extension for v1.5.6, which it | |
| # installs the first time and checks against its recorded digest before | |
| # every load (decision E37): cached, so a run downloads it once. | |
| # Saved, like every cache here, only where SAVE_CACHES says. | |
| - id: duckdb-extensions | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/.duckdb/extensions/v1.5.6 | |
| key: duckdb-extensions-v1.5.6-${{ runner.os }}-${{ runner.arch }} | |
| # The test kit's clippy, here on the DuckDB build this job makes (H3). | |
| - run: cargo clippy -p sdm-testkit --all-targets --locked -- -D warnings | |
| - run: cargo test --workspace --locked | |
| # The pure-Rust host over the contract cases, its observations compared | |
| # with the cases' expectations and reference observations (T1.10; | |
| # T1.18 moves this to the checkpoint's comparator), from T1.11e under | |
| # contract/compare-policy.yaml, which pyyaml reads: its statements and | |
| # their results against the reference's (H9), its call against the | |
| # reference's request, and base.regression reaching its result. The | |
| # test host links no Python. Its DuckDB loads ICU explicitly, installing | |
| # DuckDB's signed extension for its version the first time, from the | |
| # cache above when it holds it (decision E37). | |
| - run: cargo run -p sdm-testkit --bin sdm-contract-run --locked -- --out ../rust-host.jsonl ../contract/cases | |
| - run: | | |
| cargo tree -e normal -p sdm-testkit --locked > ../testkit-tree.txt | |
| ! grep -w pyo3 ../testkit-tree.txt | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| # On the phase branch and its lanes the gate step below runs this | |
| # comparison with the checkpoint's comparator, each baselined case's | |
| # outcome against its effective expectation (an approved change | |
| # included), so this one runs only elsewhere (T1.14); whether the | |
| # gate runs is the gate step's to decide, failing closed. | |
| - run: | | |
| pip install jsonschema pyyaml | |
| where=$(python scripts/rust_core/ratchet.py where) || exit 1 | |
| if [ -n "$where" ]; then | |
| echo 'the gate step compares rust-host on this branch' | |
| exit 0 | |
| fi | |
| python scripts/rust_core/compare.py --cases contract/cases --reference contract/reference/*.jsonl rust-host.jsonl | |
| working-directory: . | |
| # The gate (T1.14): the candidate judged by its checkpoint's harness, | |
| # from the checkpoint's own worktree (design, "Reference observations | |
| # and the gate"; H2): rust-host's observations made with the | |
| # checkpoint's case loader and observation writer, the checkpoint's | |
| # ratchet (protected paths, reference immutability, cases, register, | |
| # relaxations, KNOWN_DIFFERING, baselines and availability), its | |
| # comparator and its L0 scripts. On a push to the phase branch the | |
| # checkpoint is the push's previous tip, and the push lands exactly | |
| # one package; on a lane or topic branch, the merge base with the | |
| # phase branch's tip, a landing commit judged against its own | |
| # recorded base. Before T1.14 lands, the bootstrap: the candidate's | |
| # harness runs. | |
| # Whether the gate runs fails closed (T1.18), decided in the step | |
| # itself so a local replay runs it too (replay_jobs.py takes no | |
| # conditions): on a push to the phase branch or any branch under it | |
| # the gate must run, from GitHub's own event and ref, not from the | |
| # candidate's ratchet.py, whose `where` then only names the base; an | |
| # error or an empty answer there fails. Elsewhere there is no ratchet. | |
| # tests/test_contract_ci_gate.py runs this step's text. | |
| - name: gate (rust-host) | |
| run: | | |
| where=$(python scripts/rust_core/ratchet.py where) || { echo 'gate: ratchet.py where failed'; exit 1; } | |
| case "${GITHUB_EVENT_NAME:-}:${GITHUB_REF_NAME:-}" in | |
| push:rust-core-phase-1 | push:rust-core-phase-1-*) | |
| [ -n "$where" ] || { echo "gate: ratchet.py where named no base on $GITHUB_REF_NAME, where the gate must run"; exit 1; } ;; | |
| esac | |
| [ -n "$where" ] || { echo 'gate: no ratchet off the phase branch'; exit 0; } | |
| scripts/rust_core/gate.sh $where --run rust-host | |
| working-directory: . | |
| - if: ${{ !cancelled() && env.SAVE_CACHES == 'true' && steps.duckdb-extensions.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ~/.duckdb/extensions/v1.5.6 | |
| key: ${{ steps.duckdb-extensions.outputs.cache-primary-key }} | |
| # The client wheel, which carries the core as sdm_client._native (design, | |
| # "Packaging"). maturin, at the version native_wheel.py pins, builds it in | |
| # a manylinux2014 container run with docker rather than through an | |
| # action: the x86_64 cp310-abi3 wheel that ships. native_wheel.py records | |
| # the build's target flags and fails on target-cpu, and reads the wheel | |
| # with auditwheel show. Every job that installs the client takes it from | |
| # this job's artifact, and this job builds and uploads nothing else, so | |
| # those jobs wait for the shipped wheel only (CI1): the test-only build | |
| # and the binding's tests run beside it, in native-test-ops and binding. | |
| native-wheel: | |
| runs-on: ubuntu-latest | |
| env: | |
| NATIVE_ARGS: --out native | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - run: rustup toolchain install | |
| working-directory: rust | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| with: | |
| workspaces: rust | |
| save-if: ${{ env.SAVE_CACHES == 'true' }} | |
| # As the runner's user, so the cache can read rust/target afterwards, | |
| # in a home the runner made, so that user can write it, holding the | |
| # runner's crate registry, which the cache restores. NATIVE_ARGS says | |
| # which build: native-test-ops runs this same step for its own. | |
| - &manylinux-build | |
| name: Build the wheel in manylinux2014 | |
| run: | | |
| home="$RUNNER_TEMP/manylinux-home" | |
| mkdir -p "$HOME/.cargo/registry" "$HOME/.cargo/git" "$home/.cargo/registry" "$home/.cargo/git" | |
| docker run --rm --user "$(id -u):$(id -g)" \ | |
| -e HOME=/tmp/home -e CARGO_HOME=/tmp/home/.cargo -e RUSTUP_HOME=/tmp/home/.rustup \ | |
| -e NATIVE_ARGS \ | |
| -v "$home:/tmp/home" -v "$PWD:/io" -w /io \ | |
| -v "$HOME/.cargo/registry:/tmp/home/.cargo/registry" \ | |
| -v "$HOME/.cargo/git:/tmp/home/.cargo/git" \ | |
| "$MANYLINUX" bash -euo pipefail -c ' | |
| # rustup-init 1.29.0, checked against its published SHA-256. | |
| curl --proto "=https" --tlsv1.2 -sSf -o /tmp/home/rustup-init \ | |
| https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-gnu/rustup-init | |
| echo "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 /tmp/home/rustup-init" \ | |
| | sha256sum -c - | |
| chmod +x /tmp/home/rustup-init | |
| /tmp/home/rustup-init -y --profile minimal --default-toolchain none --no-modify-path | |
| export PATH="$CARGO_HOME/bin:$PATH" | |
| (cd rust && rustup toolchain install) | |
| /opt/python/cp312-cp312/bin/python -m venv /tmp/home/maturin | |
| /tmp/home/maturin/bin/pip install --quiet maturin==1.15.0 | |
| export PATH="/tmp/home/maturin/bin:$PATH" | |
| python scripts/rust_core/native_wheel.py $NATIVE_ARGS --manylinux | |
| ' | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-wheel | |
| path: native/ | |
| # The binding with its test-only operations, built beside native-wheel by | |
| # the same step and tested at the E16 floor: every file of the binding's | |
| # tests, and the Python host's tests that need those operations, which | |
| # the test job does not collect (E23). Its wheel is its own artifact, | |
| # which wheel-smoke installs in its clean containers and no job that | |
| # builds or ships takes (E23). | |
| native-test-ops: | |
| runs-on: ubuntu-latest | |
| env: | |
| NATIVE_ARGS: --out native-test-ops --test-ops | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - run: rustup toolchain install | |
| working-directory: rust | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 | |
| with: | |
| workspaces: rust | |
| save-if: ${{ env.SAVE_CACHES == 'true' }} | |
| - *manylinux-build | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.10' | |
| # The connectors are for the canary test's journey over DuckDB | |
| # (T1.11d), which tests_test_ops runs here at the floor. | |
| - name: The binding's tests on the test-ops build | |
| run: | | |
| python -m venv /tmp/test-ops | |
| /tmp/test-ops/bin/pip install --quiet pytest $BINDING_FLOOR \ | |
| ./packages/structured-data-models-api ./packages/structured-data-models-pql \ | |
| ./packages/structured-data-models-connectors \ | |
| "$(ls native-test-ops/*.whl)" | |
| /tmp/test-ops/bin/python -m pytest rust/bindings/sdm-py/tests -q \ | |
| -p no:cacheprovider --native-build test-ops | |
| /tmp/test-ops/bin/python -m pytest packages/structured-data-models-client/tests_test_ops -q \ | |
| -p no:cacheprovider | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-wheel-test-ops | |
| path: native-test-ops/ | |
| # The binding's tests on the wheel that ships, at the E16 floor: the file | |
| # any build passes. | |
| binding: | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.10' | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| - name: The binding's tests on the shipped build | |
| run: | | |
| python -m venv /tmp/production | |
| /tmp/production/bin/pip install --quiet pytest $BINDING_FLOOR \ | |
| ./packages/structured-data-models-api ./packages/structured-data-models-pql \ | |
| "$(ls native/*.whl)" | |
| /tmp/production/bin/python -m pytest rust/bindings/sdm-py/tests/test_binding.py -q \ | |
| -p no:cacheprovider --native-build production | |
| # The wheels as a user installs them (T1.17, decision E16), each lane in | |
| # a clean container, both gating: the floor (Python 3.10, pyarrow 14.0.1, | |
| # pandas 2.2.0, NumPy 1.26.4) and the newest (Python 3.14 with the newest | |
| # pandas 3, NumPy 2 and pyarrow, resolved on every run). wheel_smoke.py | |
| # runs itself in the lane's Python image, pinned by digest, the checkout | |
| # mounted read-only; it installs the shipped wheel and the test-ops one, | |
| # as these jobs built them, each in an environment of its own holding | |
| # only wheels, and runs the binding's tests on them from outside the | |
| # checkout (shared and released buffers, admitted types, outputs after | |
| # collection, error and destruction, busy and cancel). Each lane's | |
| # resolved environments, reports and environment.json are its artifact, | |
| # uploaded whether it passed or not. | |
| wheel-smoke: | |
| name: wheel-smoke (${{ matrix.lane }}) | |
| runs-on: ubuntu-latest | |
| needs: [native-wheel, native-test-ops] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| lane: [floor, newest] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel-test-ops | |
| path: native-test-ops | |
| - name: The lane's clean container | |
| run: | | |
| python3 scripts/rust_core/wheel_smoke.py --lane ${{ matrix.lane }} --container \ | |
| --wheel "$(ls native/*.whl)" --test-ops-wheel "$(ls native-test-ops/*.whl)" \ | |
| --out wheel-smoke | |
| - if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: wheel-smoke-${{ matrix.lane }} | |
| path: wheel-smoke/ | |
| typecheck: | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| # The engine needs its runtime dependencies present to be checked at all, | |
| # and a toolchain to install. It ships py.typed exactly like the other | |
| # four, so leaving it out would make the gate narrower than the promise. | |
| # The image's compilers and CMake are that toolchain; the runner image | |
| # carries them, so nothing is installed (CI1). | |
| - run: for tool in gcc g++ make cmake; do command -v "$tool"; done | |
| - run: pip install --upgrade pip mypy | |
| # The client from the native wheel; its stub, _native.pyi, ships in it. | |
| - run: | | |
| pip install -e './packages/structured-data-models-api' -e './packages/structured-data-models-pql' \ | |
| "$(ls native/*.whl)" \ | |
| -e './packages/structured-data-models-connectors[sqlite,duckdb,postgres,snowflake,databricks]' \ | |
| -e './packages/structured-data-models-relational-engine' | |
| # Every session a source takes too, so each driver's types are its own | |
| # rather than Any. pyspark-client is pyspark's Python without its jars. | |
| - run: pip install snowflake-snowpark-python pyspark-client | |
| - run: | | |
| mypy packages/structured-data-models-api/src packages/structured-data-models-pql/src packages/structured-data-models-client/src \ | |
| packages/structured-data-models-connectors/src packages/structured-data-models-relational-engine/src | |
| # What a caller's type checker infers for a source, and what it refuses. | |
| - run: mypy packages/structured-data-models-connectors/tests/typing/check_sources.py | |
| # Every interpreter the four pure-python distributions classify. The | |
| # internal pipeline runs one of them, so a defect specific to any other | |
| # reached a release through a promise nothing checked. | |
| test: | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] | |
| # A real server for the PostgreSQL reads and the PostgreSQL sampler, which | |
| # only a PostgreSQL can say it accepts. SDM_REQUIRE_POSTGRES turns a | |
| # missing DSN or driver into a failure, where a skip would read as a pass. | |
| services: | |
| postgres: | |
| # Docker's official image, from its public ECR mirror: Docker Hub's | |
| # anonymous pull limit failed every attempt of a run. Pinned by the | |
| # digest CI pulled for the tag postgres:17. | |
| image: public.ecr.aws/docker/library/postgres@sha256:2d2b8998d31037bf721cfdf764d76ba74171b4fab3431b7f72c27c56ddbdf9e3 | |
| env: | |
| POSTGRES_DB: sdm_test | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready --health-interval 5s | |
| --health-timeout 5s --health-retries 10 | |
| env: | |
| SDM_POSTGRES_TEST_DSN: postgresql://postgres:postgres@localhost:5432/sdm_test | |
| SDM_REQUIRE_POSTGRES: '1' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| # pip's downloads and built wheels, keyed on the week and on every | |
| # distribution's manifest and restored from the newest of this job's | |
| # otherwise. pip still resolves each requirement against the index, so | |
| # the cache only saves downloads and changes no version (CI1). | |
| - id: week | |
| run: echo "week=$(date -u +%G-%V)" >> "$GITHUB_OUTPUT" | |
| - id: pip-cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/.cache/pip | |
| key: pip-${{ runner.os }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.week.outputs.week }}-${{ hashFiles('packages/*/pyproject.toml') }} | |
| restore-keys: pip-${{ runner.os }}-${{ github.job }}-${{ matrix.python-version }}- | |
| # databricks is here for its driver, not for a server: those tests mock | |
| # the connection. Without the driver, importing the backend raises and | |
| # every test covering it skips. The client is the native wheel's, the | |
| # one abi3 build every interpreter here installs. | |
| - run: | | |
| pip install --upgrade pip 'ruff==0.16.*' "tomli; python_version < '3.11'" | |
| pip install -e './packages/structured-data-models-api[test]' -e './packages/structured-data-models-pql[test]' \ | |
| "$(ls native/*.whl)[test]" \ | |
| -e './packages/structured-data-models-connectors[sqlite,duckdb,postgres,databricks,test]' | |
| - run: | | |
| python -m pytest packages/structured-data-models-api/tests packages/structured-data-models-pql/tests \ | |
| packages/structured-data-models-client/tests packages/structured-data-models-connectors/tests tests -q \ | |
| -m 'not live_nim and not live_warehouse' | |
| - if: ${{ !cancelled() && env.SAVE_CACHES == 'true' && steps.pip-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ steps.pip-cache.outputs.cache-primary-key }} | |
| engine: | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| # A real server for the PostgreSQL reads and the PostgreSQL sampler, which | |
| # only a PostgreSQL can say it accepts. SDM_REQUIRE_POSTGRES turns a | |
| # missing DSN or driver into a failure, where a skip would read as a pass. | |
| services: | |
| postgres: | |
| # Docker's official image, from its public ECR mirror: Docker Hub's | |
| # anonymous pull limit failed every attempt of a run. Pinned by the | |
| # digest CI pulled for the tag postgres:17. | |
| image: public.ecr.aws/docker/library/postgres@sha256:2d2b8998d31037bf721cfdf764d76ba74171b4fab3431b7f72c27c56ddbdf9e3 | |
| env: | |
| POSTGRES_DB: sdm_test | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready --health-interval 5s | |
| --health-timeout 5s --health-retries 10 | |
| env: | |
| SDM_POSTGRES_TEST_DSN: postgresql://postgres:postgres@localhost:5432/sdm_test | |
| SDM_REQUIRE_POSTGRES: '1' | |
| steps: | |
| # Every branch, for the gate's checkpoint and its worktree. | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| # The image's compilers and CMake, which a source build uses; the | |
| # runner image carries them, so nothing is installed (CI1). | |
| - run: for tool in gcc g++ make cmake; do command -v "$tool"; done | |
| # pip's downloads and built wheels, keyed on the week and on every | |
| # distribution's manifest and restored from the newest of this job's | |
| # otherwise. pip still resolves each requirement against the index, so | |
| # the cache only saves downloads and changes no version (CI1). | |
| - id: week | |
| run: echo "week=$(date -u +%G-%V)" >> "$GITHUB_OUTPUT" | |
| - id: pip-cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/.cache/pip | |
| key: pip-${{ runner.os }}-${{ github.job }}-3.12-${{ steps.week.outputs.week }}-${{ hashFiles('packages/*/pyproject.toml') }} | |
| restore-keys: pip-${{ runner.os }}-${{ github.job }}-3.12- | |
| # ruff is here for the root suite, not for linting: the test that | |
| # checks the engine ignore-list note shells out to it and skips | |
| # without it, which would make that check vacuous here. | |
| # The Databricks and Snowflake drivers are for the offline tests of those | |
| # backends, which skip whole modules without them; pyyaml reads the | |
| # semantic-view fixtures, and `explain` carries the SDK the explanation | |
| # tests fake around. jsonschema validates the contract's cases and | |
| # observations, which the root suite checks. | |
| - run: | | |
| pip install --upgrade pip 'ruff==0.16.*' | |
| pip install -e './packages/structured-data-models-api' -e './packages/structured-data-models-pql' \ | |
| -e './packages/structured-data-models-connectors[sqlite,duckdb,databricks,snowflake,postgres]' \ | |
| pyyaml jsonschema | |
| pip install -e './packages/structured-data-models-relational-engine[test,explain]' | |
| # The same marker filter as the internal pipeline. The live NIM and live | |
| # warehouse tests need credentials this job does not have, so they are | |
| # deselected by name rather than left to skip. A skip that remains is a | |
| # test that checked its own environment and stood down, and is reported. | |
| - run: | | |
| cd packages/structured-data-models-relational-engine | |
| python -m pytest tests -q -m 'not live_nim and not live_warehouse' | |
| # The client's suite again, with an engine present. The test job installs | |
| # none, so every client test that needs one skips there and runs nowhere. | |
| - run: | | |
| pip install "$(ls native/*.whl)[test]" | |
| python -m pytest packages/structured-data-models-client/tests -q \ | |
| -m 'not live_nim and not live_warehouse' | |
| # And the root suite, which holds the relational wire snapshots. They | |
| # only exercise anything with an engine installed, so running them in | |
| # the test job would prove nothing. | |
| # The contract cases and the runner's controls (contract/tests) run | |
| # in contract-python, from T1.18. | |
| - run: python -m pytest tests -q | |
| - if: ${{ !cancelled() && env.SAVE_CACHES == 'true' && steps.pip-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ steps.pip-cache.outputs.cache-primary-key }} | |
| # The L2 corpus on the Python runners per runtime cell (H1; design, "CI"; | |
| # T1.18): each cell pinned as run_python.py's CELLS names it, python-core | |
| # and python-old observed with the checkpoint's runner and judged by the | |
| # checkpoint's ratchet and comparator through gate.sh, which also | |
| # validates the register; rust-test runs the L0 scripts. In a cell other | |
| # than the reference's the comparator holds python-core to python-old on | |
| # host fields and reports the reference's (H1). Each observation must name | |
| # the job's cell, so a pin that drifts fails. The runner's controls and | |
| # the route tests (contract/tests) run once, in ref-pandas3, with their | |
| # comparisons on the same effective expectations as the gate. One job per | |
| # cell, sharing its steps: replay_jobs.py substitutes no matrix value but | |
| # the Python version. | |
| contract-python-ref-pandas3: | |
| name: contract-python (ref-pandas3) | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| env: | |
| CELL: ref-pandas3 | |
| CELL_PINS: pandas==3.0.6 pyarrow==25.0.1 numpy==2.5.3 | |
| steps: | |
| - &contract-checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| # Every branch, for the gate's checkpoint and its worktree. | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - &contract-wheel | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| # The cell's pins as constraints, so nothing installed after moves | |
| # them; DuckDB at the reference's version; the engine, which a case's | |
| # materialize needs, built from source with the runner image's | |
| # compilers and CMake. | |
| - &contract-install | |
| name: Install the cell | |
| run: | | |
| printf '%s\n' $CELL_PINS duckdb==1.5.6 > "$RUNNER_TEMP/cell.txt" | |
| pip install --upgrade pip | |
| pip install -c "$RUNNER_TEMP/cell.txt" $CELL_PINS duckdb==1.5.6 pyyaml jsonschema pytest \ | |
| ./packages/structured-data-models-api ./packages/structured-data-models-pql \ | |
| './packages/structured-data-models-connectors[duckdb]' \ | |
| ./packages/structured-data-models-relational-engine "$(ls native/*.whl)[test]" | |
| - name: The runner's controls and the route tests | |
| run: python -m pytest contract/tests -q | |
| # As in rust-test, whether the gate runs fails closed (T1.18); off | |
| # the phase branch, the candidate's own runners and comparator run. | |
| # Every cell gates, the floor's too (T1.18's own run alone reported | |
| # it, E65(c); the waiver went at T1.25b, every checkpoint descending | |
| # from T1.18's landing). tests/test_contract_ci_gate.py runs this | |
| # step's text. | |
| - &contract-gate | |
| name: gate (python-core, python-old) | |
| env: | |
| PYTHON: python | |
| run: | | |
| set -o pipefail | |
| where=$(python scripts/rust_core/ratchet.py where) || { echo 'gate: ratchet.py where failed'; exit 1; } | |
| case "${GITHUB_EVENT_NAME:-}:${GITHUB_REF_NAME:-}" in | |
| push:rust-core-phase-1 | push:rust-core-phase-1-*) | |
| [ -n "$where" ] || { echo "gate: ratchet.py where named no base on $GITHUB_REF_NAME, where the gate must run"; exit 1; } ;; | |
| esac | |
| mkdir -p gate-observations | |
| status=0 | |
| if [ -z "$where" ]; then | |
| echo 'gate: no ratchet off the phase branch: the candidate runners and comparator' | |
| for runner in python-core python-old; do | |
| python -I contract/runners/run_python.py --mode $runner \ | |
| --out gate-observations/$runner.jsonl contract/cases || exit 1 | |
| done | |
| python scripts/rust_core/compare.py --cases contract/cases \ | |
| --reference contract/reference/*.jsonl gate-observations/python-*.jsonl || status=$? | |
| else | |
| scripts/rust_core/gate.sh $where --run python-core,python-old --no-l0 \ | |
| --out gate-observations || status=$? | |
| fi | |
| python - <<'EOF' || exit 1 | |
| import json, os, pathlib, sys | |
| cell = os.environ['CELL'] | |
| for runner in ('python-core', 'python-old'): | |
| path = pathlib.Path('gate-observations') / f'{runner}.jsonl' | |
| lines = path.read_text().splitlines() if path.is_file() else [] | |
| try: | |
| seen = {json.loads(line)['identity']['cell'] for line in lines} | |
| except (ValueError, RecursionError, KeyError, TypeError): | |
| sys.exit(f'{runner}: an observation names no cell') | |
| if seen != {cell}: | |
| sys.exit(f'{runner}: observed in {sorted(map(str, seen))}, not {cell}') | |
| print(f'every observation in {cell}') | |
| EOF | |
| exit $status | |
| contract-python-dbr17-pandas2: | |
| name: contract-python (dbr17-pandas2) | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| env: | |
| CELL: dbr17-pandas2 | |
| CELL_PINS: pandas==2.2.3 pyarrow==19.0.1 numpy==2.1.3 | |
| steps: | |
| - *contract-checkout | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - *contract-wheel | |
| - *contract-install | |
| - *contract-gate | |
| contract-python-floor-pandas2-2: | |
| name: contract-python (floor-pandas2.2) | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| env: | |
| CELL: floor-pandas2.2 | |
| CELL_PINS: pandas==2.2.0 pyarrow==14.0.1 numpy==1.26.4 | |
| steps: | |
| - *contract-checkout | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.10' | |
| - *contract-wheel | |
| - *contract-install | |
| - *contract-gate | |
| build: | |
| runs-on: ubuntu-latest | |
| needs: native-wheel | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| # The image's compilers and CMake, which a source build uses; the | |
| # runner image carries them, so nothing is installed (CI1). | |
| - run: for tool in gcc g++ make cmake; do command -v "$tool"; done | |
| - run: pip install --upgrade pip build | |
| # Each distribution builds in an isolated environment holding only the | |
| # build backend. That catches what no test can, because it fails before a | |
| # test could run: a version resolved by importing the package root cannot | |
| # produce a wheel at all. The client is the exception: it carries the | |
| # core, so it is the wheel native-wheel built, taken as it is. | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: native-wheel | |
| path: native | |
| - run: | | |
| for package in structured-data-models-api structured-data-models-pql structured-data-models-connectors structured-data-models-relational-engine; do | |
| echo "building $package" | |
| ( cd "packages/$package" && python -m build --wheel --outdir "$GITHUB_WORKSPACE/dist" . ) | |
| done | |
| cp native/*.whl dist/ | |
| # Every distribution carries the one version they are released together | |
| # at, read from what was just built rather than written here. | |
| - run: | | |
| python - <<'EOF' | |
| import pathlib, re, sys | |
| names = sorted(p.name for p in pathlib.Path('dist').glob('*.whl')) | |
| versions = {re.match(r'[^-]+-([^-]+)-', n).group(1) for n in names} | |
| print('\n'.join(names)) | |
| if len(names) != 5: | |
| sys.exit(f'expected 5 wheels, built {len(names)}') | |
| if len(versions) != 1: | |
| sys.exit(f'distributions disagree on the version: {sorted(versions)}') | |
| pathlib.Path('dist/VERSION').write_text(versions.pop()) | |
| EOF | |
| # Building each one proves nothing about installing them together. A pin | |
| # only one distribution carries makes the set unsatisfiable, and the | |
| # resolver is the only thing that says so. | |
| - run: | | |
| VERSION=$(cat dist/VERSION) | |
| pip install --dry-run --find-links "$GITHUB_WORKSPACE/dist" \ | |
| "structured-data-models-client[relational,sqlite,duckdb]==$VERSION" | |
| # The wheels only: dist/ also carries VERSION for the jobs downstream, | |
| # and twine refuses anything that is not a distribution. | |
| - run: pip install --upgrade twine && twine check dist/*.whl | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: wheels | |
| path: dist/ | |
| # The suite has only ever run against the source tree. What users install is | |
| # a wheel, and the difference is where packaging bugs live: a module left out | |
| # of `packages.find`, a `py.typed` that never shipped. This runs from a | |
| # directory where the source tree is not importable. | |
| installed-wheel: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: wheels | |
| path: dist | |
| - run: | | |
| pip install --upgrade pip pytest requests-mock | |
| VERSION=$(cat dist/VERSION) | |
| pip install --find-links "$GITHUB_WORKSPACE/dist" \ | |
| "structured-data-models-client[sqlite,duckdb]==$VERSION" | |
| - name: The installed distribution is what gets imported, and it is typed | |
| working-directory: /tmp | |
| run: | | |
| python - <<'EOF' | |
| import pathlib | |
| import predictive_query, sdm_api, sdm_client, sdm_connectors | |
| for module in (sdm_api, sdm_client, sdm_connectors, predictive_query): | |
| location = pathlib.Path(module.__file__).resolve() | |
| assert 'site-packages' in location.parts, ( | |
| f'{module.__name__} was imported from {location}, not from ' | |
| f'the installed wheel, so this job is testing the checkout' | |
| ) | |
| marker = location.parent / 'py.typed' | |
| assert marker.is_file(), ( | |
| f'{module.__name__} ships no py.typed in the built wheel, so ' | |
| f'a downstream type checker ignores its annotations entirely' | |
| ) | |
| print('four distributions imported from site-packages, all typed') | |
| # The core is in the installed client, not merely in the checkout; | |
| # found without importing it, which only a core route does. | |
| import importlib.util | |
| spec = importlib.util.find_spec('sdm_client._native') | |
| assert spec and 'site-packages' in spec.origin, spec | |
| print(f'the core ships in the client: {pathlib.Path(spec.origin).name}') | |
| EOF | |
| - working-directory: /tmp | |
| run: | | |
| python -m pytest "$GITHUB_WORKSPACE/packages/structured-data-models-client/tests" -q \ | |
| -m 'not live_nim and not live_warehouse' -p no:cacheprovider | |
| # The dependency inventory a consumer needs before accepting this into their | |
| # own supply chain, covering every connector extra that resolves without a | |
| # native toolchain. | |
| sbom: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: wheels | |
| path: dist | |
| - run: | | |
| pip install --upgrade pip cyclonedx-bom | |
| VERSION=$(cat dist/VERSION) | |
| pip install --find-links "$GITHUB_WORKSPACE/dist" \ | |
| "structured-data-models-client[sqlite,duckdb,postgres,databricks,snowflake,s3]==$VERSION" | |
| cyclonedx-py environment --of JSON --validate -o dist/sbom.cyclonedx.json | |
| python -c "import json,sys; d=json.load(open('dist/sbom.cyclonedx.json')); names={c['name'] for c in d.get('components', [])}; missing={'structured-data-models-client','structured-data-models-api','structured-data-models-pql','structured-data-models-connectors'} - names; print(f'{len(names)} components'); sys.exit(f'SBOM missing {sorted(missing)}' if missing else 0)" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom | |
| path: dist/sbom.cyclonedx.json |