From 7c3c38b22ce5f00ec08aec5a0970bb3d9ba3bc21 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 10:13:06 -0500 Subject: [PATCH 1/7] Fix the release wheel build and test it on packaging PRs The publish workflow could not cut a release: - macos-14 has been retired, so the macOS job never got a runner. Move to macos-15. - cibuildwheel's test step inherited PYTHONPATH=$GITHUB_WORKSPACE, which put the checkout ahead of the installed wheel. The tests imported the checkout's singlestoredb and the extension `pip install -e` had built in place for the 64-bit host Python, which the 32-bit Windows test interpreter cannot load. Drop it so the tests exercise the wheel. The xdist requirement only existed for the workspace pyproject's addopts, which pytest was not reading there anyway. Neither showed up until release time, so also run the build and test (never the publish job) on pull requests that touch packaging, and weekly, since runner retirements break the workflow with no change to the repo. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 37 ++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 26fe992bc..259279d59 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,22 @@ on: - 'v*-beta*' release: types: [published] + # Build and test, but never publish, when packaging changes, so a broken + # release pipeline is caught in review rather than at release time. + pull_request: + paths: + - '.github/workflows/publish.yml' + - 'accel.c' + - 'setup.py' + - 'setup.cfg' + - 'pyproject.toml' + - 'MANIFEST.in' + - 'resources/create_test_cluster.py' + - 'resources/drop_db.py' + # Runner images and cibuildwheel's toolchains are retired out from under the + # workflow with no change to this repo, so build and test weekly as well. + schedule: + - cron: '0 6 * * 1' workflow_dispatch: inputs: publish_pypi: @@ -36,6 +52,9 @@ on: jobs: setup-database: + # Pull requests from forks get no secrets, so they cannot deploy the test + # cluster. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest steps: @@ -77,7 +96,7 @@ jobs: matrix: os: - ubuntu-24.04 - - macos-14 + - macos-15 - windows-2022 steps: @@ -135,15 +154,16 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014 CIBW_TEST_COMMAND: "pytest -v --pyargs singlestoredb.tests.test_basics" - # xdist because pyproject.toml's addopts sets -n/--dist, and PYTHONPATH - # points --pyargs at the workspace, so that pyproject is the inifile - # here. Without the plugin pytest exits on the unknown arguments. - CIBW_TEST_REQUIRES: "pytest pytest-xdist" + CIBW_TEST_REQUIRES: "pytest" # CLUSTER_PASSWORD has to survive both the userinfo half of the URL # and the single-quoted shell word cibuildwheel evaluates, so keep the # secret alphanumeric: no ':', '@', '/', '%' or quote characters. CIBW_ENVIRONMENT: "SINGLESTOREDB_URL='mysql://admin:${{ secrets.CLUSTER_PASSWORD }}@${{ needs.setup-database.outputs.cluster-host }}:3306/${{ needs.setup-database.outputs.cluster-database }}?pure_python=0'" - PYTHONPATH: ${{ github.workspace }} + # No PYTHONPATH here: it would put the checkout ahead of the installed + # wheel, so the tests would import the checkout's singlestoredb and the + # extension that `pip install -e` built in place for the host Python, + # not the wheel under test. On Windows the 64-bit host's extension + # then fails to load in the 32-bit test interpreter. # - name: Build conda # if: ${{ matrix.os != 'windows-latest' }} @@ -186,6 +206,7 @@ jobs: publish: needs: build-and-test + if: ${{ github.event_name != 'pull_request' && github.event_name != 'schedule' }} runs-on: ubuntu-latest @@ -258,7 +279,9 @@ jobs: shutdown-database: needs: [setup-database, build-and-test] - if: ${{ always() }} + # Not when setup-database was skipped (a fork's pull request): there is + # no cluster, and the empty ID would fail the shutdown step. + if: ${{ always() && needs.setup-database.result != 'skipped' }} runs-on: ubuntu-latest steps: From c794c269a799cc1ce9978e77d767e2573303acb4 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 10:16:26 -0500 Subject: [PATCH 2/7] Don't run the wheel build for cluster script changes smoke-test.yml already drives both scripts nightly. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 259279d59..9ad15e443 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,8 +25,6 @@ on: - 'setup.cfg' - 'pyproject.toml' - 'MANIFEST.in' - - 'resources/create_test_cluster.py' - - 'resources/drop_db.py' # Runner images and cibuildwheel's toolchains are retired out from under the # workflow with no change to this repo, so build and test weekly as well. schedule: From a2dbc44dd2aee3fb05551b42e3c5b42bf0fc8b54 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 10:25:09 -0500 Subject: [PATCH 3/7] Let the shipped test suite start without pytest-xdist conftest.py implements pytest_testnodedown, which only xdist defines, so pytest refused to start without the plugin ("unknown hook"). The release build tests the wheel's own copy of the suite with just pytest installed, and now that it no longer imports the checkout, it hit this on macOS. Mark the hook optional. Co-Authored-By: Claude Opus 5.5 --- singlestoredb/tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/singlestoredb/tests/conftest.py b/singlestoredb/tests/conftest.py index 0de122150..0987d8615 100644 --- a/singlestoredb/tests/conftest.py +++ b/singlestoredb/tests/conftest.py @@ -330,6 +330,9 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: pass +# An xdist hook. Optional so the suite still starts where xdist is not +# installed, as when the release build tests the wheel's own copy of it. +@pytest.hookimpl(optionalhook=True) def pytest_testnodedown(node: Any, error: Any) -> None: """ Report, on the controller, what a worker could not terminate. From 67892a7c095289b47b483262e4b78168c0c7fbee Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Wed, 11 Nov 2026 15:18:53 -0500 Subject: [PATCH 4/7] Use an SPDX license string and setuptools' own bdist_wheel setuptools warns that `project.license` as a TOML table is deprecated and must be gone by 2027-02-18, after which newer setuptools releases may refuse to build the project. Isolated builds (pip, `build`, cibuildwheel) always pull the latest setuptools, so that date is a hard deadline for building wheels and installing from the sdist. Switch to the SPDX string form, and name LICENSE in `license-files` so the wheel carries the same single license file it does today. `wheel.bdist_wheel` is now just a deprecated alias for `setuptools.command.bdist_wheel`, so import that directly and drop `wheel` from build-system.requires; nothing else in the build uses it. Both forms need setuptools 77, so raise the floor from 61 in build-system.requires and the `build` extra. 77.0.0 was never uploaded, so this resolves to 77.0.1 or later, which requires Python 3.9 -- the same floor as requires-python. The only change in the built metadata is `License: Apache-2.0` becoming `License-Expression: Apache-2.0` (Metadata-Version stays 2.4). Wheel tags and file lists are unchanged. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 7 ++++--- setup.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7a140b75..0acbbc149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["build", "setuptools>=61.0", "wheel"] +requires = ["build", "setuptools>=77.0"] build-backend = "setuptools.build_meta" [project] @@ -7,7 +7,8 @@ name = "singlestoredb" version = "1.18.2" description = "Interface to the SingleStoreDB database and workspace management APIs" readme = {file = "README.md", content-type = "text/markdown"} -license = {text = "Apache-2.0"} +license = "Apache-2.0" +license-files = ["LICENSE"] authors = [ {name = "SingleStore", email = "support@singlestore.com"} ] @@ -69,7 +70,7 @@ docs = [ ] build = [ "build", - "setuptools>=61.0", + "setuptools>=77.0", "wheel", ] dev = [ diff --git a/setup.py b/setup.py index 9ace49851..44458593a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import Extension from setuptools import setup -from wheel.bdist_wheel import bdist_wheel +from setuptools.command.bdist_wheel import bdist_wheel py_limited_api = '0x03080000' From 5065a43aac51f6da924f04d6866c7fa3e87b47e1 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 10:55:33 -0500 Subject: [PATCH 5/7] Build the wheels on fork pull requests too A fork's pull request skips setup-database for want of secrets, and build-and-test, which needs it, was skipped with it, so fork packaging PRs got no build at all. Run build-and-test when setup-database was skipped (still not when it failed), and on those runs swap the cluster tests for an import check of each wheel's extension. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ad15e443..ae3f69c9b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -86,6 +86,10 @@ jobs: build-and-test: needs: setup-database + # setup-database is skipped only for a fork's pull request, which has no + # secrets. Those runs still build every wheel and check that its extension + # loads; only the cluster tests are left out. + if: ${{ !cancelled() && (needs.setup-database.result == 'success' || needs.setup-database.result == 'skipped') }} name: Build and test wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -151,7 +155,7 @@ jobs: # the pure-Python wheel without any warning. CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014 - CIBW_TEST_COMMAND: "pytest -v --pyargs singlestoredb.tests.test_basics" + CIBW_TEST_COMMAND: ${{ needs.setup-database.result == 'success' && 'pytest -v --pyargs singlestoredb.tests.test_basics' || 'python -c "import _singlestoredb_accel, singlestoredb"' }} CIBW_TEST_REQUIRES: "pytest" # CLUSTER_PASSWORD has to survive both the userinfo half of the URL # and the single-quoted shell word cibuildwheel evaluates, so keep the From 8cae81a48669e5ad5b1a1b7db4528744c880b101 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 11:28:13 -0500 Subject: [PATCH 6/7] Keep the build/ tree out of the Linux wheels The Linux publish job builds the pure-Python wheel first, which leaves build/lib/ in the checkout, and cibuildwheel then builds the extension wheel from that checkout. Package discovery from pyproject includes namespace packages, so build/lib/singlestoredb and build/lib/sqlx were packaged too and installed as site-packages/build/lib/. v1.18.2's Linux wheels carry the same 258 extra files. Exclude build*. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0acbbc149..e62d7daa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,9 @@ singlestoredb = "singlestoredb.pytest" include-package-data = true [tool.setuptools.packages.find] -exclude = ["docs*", "resources*", "examples*", "licenses*"] +# build* because discovery includes namespace packages, so an earlier build's +# build/lib/ tree would otherwise be packaged as a package of its own. +exclude = ["build*", "docs*", "resources*", "examples*", "licenses*"] [tool.setuptools.package-data] "*" = ["*.typed", "*.sql", "*.csv", "*.ipynb"] From 2465f830c0a0498320babb226a8700451b55be44 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 12 Nov 2026 12:11:11 -0500 Subject: [PATCH 7/7] Clear the publish workflow's run annotations - Drop pp* from CIBW_SKIP. cibuildwheel v3+ builds no PyPy unless it is enabled, so the selector matched nothing and warned on every build job; CIBW_BUILD already limits the build to cp39 and cp315. - Pin the ubuntu-latest jobs to ubuntu-24.04. The label is moving to Ubuntu 26.04, and these jobs run on the image's own Python, so the move would change it under them. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ae3f69c9b..21d3d06ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -53,7 +53,11 @@ jobs: # Pull requests from forks get no secrets, so they cannot deploy the test # cluster. if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest + # Pinned, not ubuntu-latest: that label is moving to Ubuntu 26.04, and + # this job, publish and shutdown-database run on the image's own Python + # rather than one from setup-python, so the move would change it under + # them. The weekly scheduled run flags this image's retirement. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 @@ -148,7 +152,7 @@ jobs: # only reruns the tests there, so the oldest and newest supported # interpreters both load the extension. CIBW_BUILD: "cp39-* cp315-*" - CIBW_SKIP: "pp* *-musllinux* *-manylinux_i686" + CIBW_SKIP: "*-musllinux* *-manylinux_i686" # cibuildwheel v3+ builds on manylinux_2_28 (glibc 2.28) by default. # Pin manylinux2014 (glibc 2.17) so CentOS/RHEL 7, Amazon Linux 2 and # Ubuntu 18.04 still get the C extension instead of falling back to @@ -210,7 +214,8 @@ jobs: needs: build-and-test if: ${{ github.event_name != 'pull_request' && github.event_name != 'schedule' }} - runs-on: ubuntu-latest + # Pinned for the same reason as setup-database. + runs-on: ubuntu-24.04 permissions: id-token: write # Required for OIDC trusted publishing and actions/attest @@ -284,7 +289,8 @@ jobs: # Not when setup-database was skipped (a fork's pull request): there is # no cluster, and the empty ID would fail the shutdown step. if: ${{ always() && needs.setup-database.result != 'skipped' }} - runs-on: ubuntu-latest + # Pinned for the same reason as setup-database. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7