1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
name: Publish Python package
# Manually-triggered release of the `infino` Python package: validate the
# version, build one binary wheel per platform/arch, then upload to TestPyPI
# (default) or PyPI.
#
# 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:
workflow_dispatch:
inputs:
version:
description: "Release version, SemVer (e.g. 0.1.0, 0.1.0-rc.1)"
required: true
type: string
target:
description: "Index to publish to"
required: true
default: testpypi
type: choice
options:
# One release per index at a time; never cancel a publish in flight.
concurrency:
group: publish-python-${{ 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
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check.outputs.version }}
steps:
# 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="${{ inputs.version }}"
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"
build:
name: wheel ${{ matrix.target }}
needs: validate
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
-
-
-
-
-
-
# 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"
# Restore the dep graph (datafusion etc.) so only infino-python rebuilds.
# Order matters: rust-cache keys on Cargo.lock, which the stamp step
# bumps — cache before stamp to keep the key stable across releases.
- uses: Swatinem/rust-cache@v2
with:
workspaces: infino-python
# Version is dynamic (pyproject `dynamic = ["version"]`) → maturin reads
# it from Cargo.toml. Stamp it into Cargo.toml *and* Cargo.lock so the
# build stays --locked. python3 over sed: one script, every runner,
# Windows included.
- name: Stamp version
shell: bash
env:
VERSION: ${{ needs.validate.outputs.version }}
run: |
python3 - "$VERSION" <<'PY'
import pathlib, re, sys
version = sys.argv[1]
toml = pathlib.Path("infino-python/Cargo.toml")
patched, count = re.subn(
r'(?m)^version = "[^"]*"', f'version = "{version}"',
toml.read_text(), count=1)
assert count == 1, "[package] version not found in Cargo.toml"
toml.write_text(patched)
lock = pathlib.Path("infino-python/Cargo.lock")
patched, count = re.subn(
r'(?ms)(\[\[package\]\]\nname = "infino-python"\nversion = ")[^"]*(")',
rf"\g<1>{version}\g<2>", lock.read_text(), count=1)
assert count == 1, "infino-python entry not found in Cargo.lock"
lock.write_text(patched)
print(f"stamped infino-python -> {version}")
PY
# `manylinux` is honored only for Linux targets and ignored elsewhere;
# musl targets need the musllinux image, every other target builds glibc.
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
command: build
target: ${{ matrix.target }}
manylinux: ${{ contains(matrix.target, 'musl') && 'musllinux_1_2' || 'auto' }}
args: --release --locked --out dist -m infino-python/Cargo.toml
# Install the freshly built wheel into a clean interpreter and run the
# smoke suite, proving it imports and round-trips before publish. Skipped
# for musl wheels, which can't install on the glibc-based runner.
- name: Smoke-test wheel
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
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist/*.whl
if-no-files-found: error
publish:
name: Publish to ${{ inputs.target }}
needs: build
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: ${{ inputs.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 ${{ inputs.target }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
repository-url: ${{ inputs.target == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}