From 0946c9e8f847ae1e5bfd046a2a087ed369213efe Mon Sep 17 00:00:00 2001 From: stewjb Date: Sat, 4 Apr 2026 11:56:10 -0500 Subject: [PATCH 1/5] CI: add ASan job, bump actions to Node.js 24, add concurrency group, optional test imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump actions/checkout v4→v5, setup-python v5→v6, upload-artifact v4→v7, download-artifact v4→v8 across build.yaml, pylint.yaml, sbom.yaml. - Add AddressSanitizer job to build.yaml (pinned to Python 3.13, LD_PRELOAD injection, limited to html-testRunner/polars/pillow to avoid pybind11 crashes). - Add concurrency group to cancel superseded runs on push. - Make geopandas/matplotlib/seaborn imports optional in test_sbdf.py so the module loads in environments where those packages are absent; add @skipIf guards to test_read_write_geodata, test_image_matplot, test_image_seaborn. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build.yaml | 65 +++++++++++++++++++++++++++-------- .github/workflows/pylint.yaml | 4 +-- .github/workflows/sbom.yaml | 22 ++++++------ spotfire/test/test_sbdf.py | 19 ++++++++-- 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04da882..2bd24ef 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,15 +1,20 @@ name: Build and Test Package on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build-sdist: name: Build Source Dist runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install Tools @@ -18,11 +23,11 @@ jobs: - name: Source Packaging run: | python -m build --sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: sdist path: 'dist/spotfire-*.tar.gz' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: test-files path: | @@ -50,12 +55,12 @@ jobs: operating-system: ['ubuntu-latest', 'windows-latest'] fail-fast: false steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: sdist path: dist - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Build Requirements @@ -81,7 +86,7 @@ jobs: python -m build --wheel # Move wheel out of build dir into top-level dist dir mv dist\*.whl ..\dist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: wheel-${{ matrix.python-version }}-${{ matrix.operating-system }} path: 'dist/spotfire-*.whl' @@ -96,16 +101,16 @@ jobs: test-environment: ${{ fromJson(needs.build-sdist.outputs.test-environments) }} fail-fast: false steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: wheel-${{ matrix.python-version }}-${{ matrix.operating-system }} path: dist - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: test-files path: test-files - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies (Linux) @@ -122,7 +127,7 @@ jobs: env: TEST_FILES_DIR: ${{ github.workspace }}/test-files/spotfire/test/files TEST_ENVIRONMENT: ${{ matrix.test-environment }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: ${{ always() }} with: name: test-results-${{ matrix.python-version }}-${{ matrix.operating-system }}-${{ matrix.test-environment }} @@ -138,14 +143,14 @@ jobs: echo -n "python-version=" >> $GITHUB_OUTPUT echo '${{ needs.build-sdist.outputs.python-versions }}' | sed -e 's/[^"]*"//' -e 's/".*//' >> $GITHUB_OUTPUT - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ steps.version.outputs.python-version }} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: sdist path: dist - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: wheel-${{ steps.version.outputs.python-version }}-ubuntu-latest path: dist @@ -163,3 +168,35 @@ jobs: mypy spotfire cython-lint spotfire vendor find spotfire -name '*_helpers.[ch]' | xargs cpplint --repository=. + asan: + name: AddressSanitizer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Install dependencies + run: | + pip install setuptools Cython "numpy>=2.0.0rc1" + pip install ".[polars]" + pip install html-testRunner polars pillow + - name: Rebuild extension with AddressSanitizer + env: + CFLAGS: "-fsanitize=address -fno-omit-frame-pointer -g" + LDFLAGS: "-fsanitize=address" + run: python setup.py build_ext --inplace + - name: Run tests under AddressSanitizer + run: | + LIBASAN=$(gcc -print-file-name=libasan.so) + LD_PRELOAD="$LIBASAN" PYTHONMALLOC=malloc python -m spotfire.test + env: + ASAN_OPTIONS: "detect_leaks=0:allocator_may_return_null=1:intercept_cxx_exceptions=0" + TEST_ENVIRONMENT: asan + - uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results-asan + path: build/test-results/*.html diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml index 58911b8..3f03d70 100644 --- a/.github/workflows/pylint.yaml +++ b/.github/workflows/pylint.yaml @@ -7,11 +7,11 @@ jobs: name: Check Linters runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install Tools diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 72094c3..45cd37e 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -35,7 +35,7 @@ jobs: outputs: python-versions: ${{ steps.dynamic.outputs.pythons }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Read python-versions id: dynamic run: | @@ -48,14 +48,14 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive # needed for vendor/sbdf-c when building/installing sdist # workflow_run: reuse artifact from build.yaml — no rebuild - name: Download sdist (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sdist path: dist @@ -64,7 +64,7 @@ jobs: # push / release / workflow_dispatch: build fresh - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Build sdist @@ -118,7 +118,7 @@ jobs: --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sbom-sdist path: spotfire-sdist.sbom.spdx.json @@ -133,14 +133,14 @@ jobs: python-version: ${{ fromJson(needs.setup.outputs.python-versions) }} fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive # needed for vendor/sbdf-c when building wheel fresh # workflow_run: reuse the ubuntu wheel artifact from build.yaml — no rebuild - name: Download wheel (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: wheel-${{ matrix.python-version }}-ubuntu-latest path: dist @@ -150,7 +150,7 @@ jobs: # Also download the sdist so scan-env can install from it (wheel is platform-specific) - name: Download sdist (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sdist path: dist @@ -160,7 +160,7 @@ jobs: # push / release / workflow_dispatch: build fresh on Linux - name: Set Up Python if: github.event_name != 'workflow_run' - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Build wheel @@ -221,7 +221,7 @@ jobs: --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sbom-wheel-${{ matrix.python-version }} path: spotfire-wheel-${{ matrix.python-version }}.sbom.spdx.json @@ -234,7 +234,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all SBOM artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: sbom-* path: all-sboms diff --git a/spotfire/test/test_sbdf.py b/spotfire/test/test_sbdf.py index de89774..8803d71 100644 --- a/spotfire/test/test_sbdf.py +++ b/spotfire/test/test_sbdf.py @@ -11,9 +11,19 @@ import pandas as pd import pandas.testing as pdtest import numpy as np -import geopandas as gpd -import matplotlib.pyplot -import seaborn +try: + import geopandas as gpd # type: ignore[import-not-found] +except ImportError: + gpd = None # type: ignore[assignment] +try: + import matplotlib # type: ignore[import-not-found] + import matplotlib.pyplot +except ImportError: + matplotlib = None # type: ignore[assignment] +try: + import seaborn # type: ignore[import-not-found] +except ImportError: + seaborn = None # type: ignore[assignment] import PIL.Image from packaging import version @@ -137,6 +147,7 @@ def test_read_10001(self): self.assertEqual(dataframe.at[10000, "String"], "kiwis") self.assertEqual(dataframe.at[10000, "Binary"], b"\x7c\x7d\x7e\x7f") + @unittest.skipIf(gpd is None, "geopandas not installed") def test_read_write_geodata(self): """Test that geo-encoded data is properly converted to/from ``GeoDataFrame``.""" gdf = sbdf.import_data(utils.get_test_data_file("sbdf/NACountries.sbdf")) @@ -468,6 +479,7 @@ def test_numpy_timedelta_resolution(self): val = df2.at[1, 'x'] self.assertEqual(val, target) + @unittest.skipIf(matplotlib is None, "matplotlib not installed") def test_image_matplot(self): """Verify Matplotlib figures export properly.""" matplotlib.pyplot.clf() @@ -480,6 +492,7 @@ def test_image_matplot(self): else: self.fail(f"Expected PNG bytes, got {type(image)}: {image!r}") + @unittest.skipIf(seaborn is None, "seaborn not installed") def test_image_seaborn(self): """Verify Seaborn grids export properly.""" matplotlib.pyplot.clf() From af0e70ebee13587eaf58f4bf325b6e4ce55ee694 Mon Sep 17 00:00:00 2001 From: stewjb Date: Sat, 4 Apr 2026 17:57:21 -0500 Subject: [PATCH 2/5] CI: add UBSan to sanitizer job; add Dependabot for action version tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extend ASan job to also run UBSan (-fsanitize=address,undefined). UBSan shares the libasan.so LD_PRELOAD runtime so no extra preload is needed. Catches signed integer overflow, null pointer dereference, misaligned access, and other C UB in the Cython extension. UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 makes CI fail clearly on the first finding with a full stack trace. - Rename job 'asan' → 'sanitizers' and artifact 'test-results-asan' → 'test-results-sanitizers' to reflect the combined coverage. - Add .github/dependabot.yml to auto-PR GitHub Actions version bumps weekly, preventing future Node.js deprecation warnings from going unnoticed. Co-Authored-By: Claude Sonnet 4.6 --- .github/dependabot.yml | 8 ++++++++ .github/workflows/build.yaml | 15 ++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9e80c11 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "ci" \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2bd24ef..a349741 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -168,8 +168,8 @@ jobs: mypy spotfire cython-lint spotfire vendor find spotfire -name '*_helpers.[ch]' | xargs cpplint --repository=. - asan: - name: AddressSanitizer + sanitizers: + name: AddressSanitizer + UBSan runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -183,20 +183,21 @@ jobs: pip install setuptools Cython "numpy>=2.0.0rc1" pip install ".[polars]" pip install html-testRunner polars pillow - - name: Rebuild extension with AddressSanitizer + - name: Rebuild extension with AddressSanitizer + UBSan env: - CFLAGS: "-fsanitize=address -fno-omit-frame-pointer -g" - LDFLAGS: "-fsanitize=address" + CFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g" + LDFLAGS: "-fsanitize=address,undefined" run: python setup.py build_ext --inplace - - name: Run tests under AddressSanitizer + - name: Run tests under AddressSanitizer + UBSan run: | LIBASAN=$(gcc -print-file-name=libasan.so) LD_PRELOAD="$LIBASAN" PYTHONMALLOC=malloc python -m spotfire.test env: ASAN_OPTIONS: "detect_leaks=0:allocator_may_return_null=1:intercept_cxx_exceptions=0" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" TEST_ENVIRONMENT: asan - uses: actions/upload-artifact@v7 if: always() with: - name: test-results-asan + name: test-results-sanitizers path: build/test-results/*.html From 450a9f375a50e342b3df7dbe8705923a538e7793 Mon Sep 17 00:00:00 2001 From: stewjb Date: Sat, 4 Apr 2026 17:58:51 -0500 Subject: [PATCH 3/5] CI: change Dependabot schedule from weekly to monthly Co-Authored-By: Claude Sonnet 4.6 --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9e80c11..3a05d97 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,6 +3,6 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" labels: - "ci" \ No newline at end of file From bd33bd22ed5acd90da30000428d7c411fe61f96a Mon Sep 17 00:00:00 2001 From: stewjb Date: Sat, 4 Apr 2026 18:00:49 -0500 Subject: [PATCH 4/5] CI: add _FORTIFY_SOURCE=2 and stack-protector-strong to sanitizer build Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a349741..cb8ac6b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -185,7 +185,7 @@ jobs: pip install html-testRunner polars pillow - name: Rebuild extension with AddressSanitizer + UBSan env: - CFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g" + CFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g -D_FORTIFY_SOURCE=2 -fstack-protector-strong" LDFLAGS: "-fsanitize=address,undefined" run: python setup.py build_ext --inplace - name: Run tests under AddressSanitizer + UBSan From 348595c300efdf040bf4f872edbd4733d820b9d7 Mon Sep 17 00:00:00 2001 From: stewjb Date: Sat, 4 Apr 2026 18:12:50 -0500 Subject: [PATCH 5/5] CI: remove redundant FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 env var All actions have been bumped to versions that natively use Node.js 24. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cb8ac6b..15ca0f0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,8 +3,6 @@ on: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build-sdist: name: Build Source Dist