Skip to content

HyperTools 1.0.0 — release to master (review & documentation; do not merge yet) #204

HyperTools 1.0.0 — release to master (review & documentation; do not merge yet)

HyperTools 1.0.0 — release to master (review & documentation; do not merge yet) #204

Workflow file for this run

name: Tests
on:
push:
branches: [ master, dev, dev-1.0 ]
# a `branches` filter without a `tags` filter means TAG pushes do NOT
# trigger this workflow -- so the release-gate job (which keys on
# refs/tags/) would never run on the v1.0.0 tag. Admit version tags too
# (2026-07 release review).
tags: [ 'v*' ]
pull_request:
# must include the branches PRs actually TARGET, or the PR-target
# workflow never runs and green checks are only push-triggered ones
# (2026-07 release review, blocker #3: PR #272 targets dev-1.0, which
# was missing here). Fork PRs in particular only run via pull_request.
branches: [ master, dev, dev-1.0 ]
# Least privilege: this workflow only reads the repo (it runs tests + builds
# docs, and passes GITHUB_TOKEN to repo-controlled code for dataset API quota).
# Pin the token to read-only so it does not inherit the repo default (2026-07
# release review hardening).
permissions:
contents: read
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
# datawrangler 0.5.0+ supports pandas 3.0 (dw#30/#31) and py3.10-3.13. The pandas<3
# ceiling is lifted, so the resolver naturally picks pandas 3.x on py3.11-3.13 and
# pandas 2.x on py3.10 (pandas 3.0 requires py>=3.11) -- the matrix exercises both.
# A dedicated pinned-pandas-3 gate runs below on ubuntu/py3.12.
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 (pinned to SHA, release review #8)
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.4.0 (pinned to SHA)
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.0 (pinned to SHA)
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- name: Cache hypertools example datasets
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.0 (pinned to SHA)
with:
path: ~/hypertools_data
key: hypertools-example-data-v1
# datasets are immutable; every runner shares one cache entry so CI
# doesn't hammer the download host with 24 concurrent requests
enableCrossOsArchive: true
- name: Install system dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
# fonts-noto-cjk (GH #205): ubuntu-latest ships no CJK-covering font
# at all, so tests/test_multibyte.py's `requires_covering_font`
# skipif would silently skip every CJK-dependent test on this OS.
sudo apt-get install -y ffmpeg fonts-noto-cjk
fc-cache -f
- name: Install system dependencies (macOS)
if: matrix.os == 'macos-latest'
run: |
brew install ffmpeg
- name: Install system dependencies (Windows)
if: matrix.os == 'windows-latest'
run: |
choco install ffmpeg
continue-on-error: true
- name: Install package with dev dependencies
# GH #130 (hyp.io.lsl_stream, tests/test_lsl_streaming.py): pylsl is
# in the [dev] extra (pyproject.toml) with NO separate native liblsl
# install step on any of the three platforms below -- investigated
# during Task 13's implementation, pylsl>=1.16's wheels BUNDLE the
# native liblsl library for every platform this matrix runs on:
# - ubuntu-latest (x86_64): manylinux_2_35_x86_64 wheel -> ships
# pylsl/lib/liblsl.so (ubuntu-latest's glibc satisfies 2.35+)
# - macos-latest (arm64/x86_64): macosx_11_0_universal2 wheel ->
# ships pylsl/lib/liblsl.dylib; verified working end-to-end
# locally (real in-process StreamOutlet -> StreamInlet round trip)
# - windows-latest (x86_64): win_amd64 wheel -> ships
# pylsl/lib/lsl.dll
# so all three CI platforms are "provisioned" and none is excluded;
# tests/test_lsl_streaming.py::test_ci_has_pylsl fails the build (not
# skip) if pylsl is ever unimportable here, e.g. if a future pylsl
# release drops the bundled binary for one of these wheel tags.
run: |
python -m pip install --upgrade pip
# [torch] (also pulled in transitively by [dev]) is installed
# explicitly too so a future change to the dev extra can't
# silently drop torch coverage of the six autoencoder reducers
# (GH #162, tests/test_autoencoders.py). torch>=2.0 ships CPU
# wheels for all three CI platforms/Python versions here.
pip install -e ".[dev,torch]"
- name: Pre-fetch headless Chrome for kaleido (Plotly image/GIF export)
# tests/test_animation_export.py exports Plotly figures to GIF/MP4 via
# kaleido 1.x, which drives a headless Chrome. Pre-fetching it here (into
# choreographer's deps dir, where kaleido discovers it automatically)
# avoids a slow first-use Chrome download racing the test's own timeout
# on some runners (seen as an intermittent TimeoutError on
# windows/py3.13). Best effort: if the fetch fails, kaleido falls back to
# its own on-demand download, so this step never blocks the build.
run: plotly_get_chrome -y
continue-on-error: true
- name: Rebuild matplotlib font cache (Ubuntu only)
# GH #205: matplotlib scans installed fonts once and caches the
# result (~/.cache/matplotlib/fontlist-*.json). On ubuntu-latest
# runners that cache is built (by whatever process first imports
# matplotlib.font_manager -- here, `pip install`'s dependency
# resolution/build step, or a previous cached run) BEFORE
# fonts-noto-cjk is installed above, so `find_covering_font`
# (hypertools/plot/fonts.py) would scan a stale font list missing
# every Noto Sans CJK file and skip every CJK-dependent test in
# tests/test_multibyte.py. Force one rebuild here (expensive -- do
# it once per job, not from inside fonts.py on every call).
if: matrix.os == 'ubuntu-latest'
run: |
python -c "import matplotlib.font_manager as fm; fm._load_fontmanager(try_read_cache=False)"
- name: Run pytest
env:
MPLBACKEND: Agg
# Authenticate the fivethirtyeight loader's GitHub API listing calls
# (tests/test_load_538_kaggle.py). The default GITHUB_TOKEN raises the
# REST API quota from 60 to 5000 requests/hour, so the ~12 concurrent
# matrix jobs sharing a runner IP pool don't exhaust the shared
# unauthenticated limit (403 rate-limit failures). See
# hypertools/io/sources.py::_github_api_headers.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pytest -v --tb=short
- name: Acceptance gate — pandas 3.0 (Ubuntu Python 3.12 only)
# Guarantees a pandas-3 run regardless of resolver choices (dw#30/#31 fix).
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
env:
MPLBACKEND: Agg
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install "pandas>=3.0"
python -c "import pandas as pd; assert pd.__version__.split('.')[0] == '3', pd.__version__; print('pandas', pd.__version__)"
pytest -q
- name: Run pytest with coverage (Ubuntu Python 3.12 only)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
env:
MPLBACKEND: Agg
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pytest --cov=hypertools --cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov (Ubuntu Python 3.12 only)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 (pinned to SHA)
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
- name: Generate verification screenshots (Ubuntu Python 3.12 only)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
env:
MPLBACKEND: Agg
run: |
python scripts/generate_baseline_screenshots.py
- name: Upload screenshots as artifacts (Ubuntu Python 3.12 only)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.0 (pinned to SHA)
with:
name: verification-screenshots
path: tests/screenshots/
retention-days: 30
# Release qualification: build the wheel + sdist ONCE, then install EACH
# artifact into its OWN fresh environment (not the editable source
# checkout) and smoke-test the public API + validate metadata. Both are
# exercised because wheel and sdist use different package-discovery/build
# paths -- a gap can exist in one and not the other (2026-07 release
# review, issues #8 wheel + follow-up sdist). Actions are SHA-pinned like
# the test job above.
wheel-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 (pinned to SHA)
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.4.0 (pinned to SHA)
with:
python-version: '3.12'
- name: Build wheel + sdist
run: |
python -m pip install --upgrade pip build twine
python -m build --outdir dist .
twine check dist/*
- name: Install the built WHEEL into a fresh venv and smoke-test
env:
MPLBACKEND: Agg
run: |
python -m venv /tmp/fresh-wheel
# install the wheel artifact itself -- NOT the source checkout
/tmp/fresh-wheel/bin/pip install --upgrade pip
/tmp/fresh-wheel/bin/pip install dist/*.whl
# run from /tmp (NOT the repo) so an accidental source-tree import
# can't mask a packaging gap; the script itself asserts it imported
# hypertools from site-packages
cd /tmp && /tmp/fresh-wheel/bin/python "$GITHUB_WORKSPACE/scripts/wheel_smoke_test.py"
- name: Install the built SDIST into a fresh venv and smoke-test
env:
MPLBACKEND: Agg
run: |
python -m venv /tmp/fresh-sdist
# build the sdist FROM the sdist (its own build path), not the repo
/tmp/fresh-sdist/bin/pip install --upgrade pip
/tmp/fresh-sdist/bin/pip install dist/*.tar.gz
cd /tmp && /tmp/fresh-sdist/bin/python "$GITHUB_WORKSPACE/scripts/wheel_smoke_test.py"
# Fresh-source docs build (2026-07 release review, blocker #2). Read the Docs
# no longer ships a generated docs/auto_examples/ tree, so every RTD build now
# executes ALL gallery examples and renders the Plotly snapshots via headless
# Chrome from scratch. This job reproduces exactly that contract: it builds
# from a pristine `git archive` (tracked files only), asserts the generated
# gallery is absent, provisions Chrome the same way RTD does, and runs Sphinx
# with -W -E -a (warnings are errors, no cached env, rebuild everything) so a
# missing browser or ANY gallery-execution failure fails the build HERE --
# before it reaches RTD. Mirrors .readthedocs.yaml (Python 3.11 + ffmpeg +
# plotly_get_chrome). Actions SHA-pinned like the jobs above.
docs-clean:
runs-on: ubuntu-latest
timeout-minutes: 75
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 (pinned to SHA)
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.4.0 (pinned to SHA)
with:
python-version: '3.11'
- name: Install system dependencies
run: |
sudo apt-get update
# ffmpeg: mp4 rendering of animated gallery examples (matches RTD).
# fonts-noto-cjk: CJK-covering font for the multibyte gallery text.
# pandoc: nbsphinx renders the hand-authored tutorials/*.ipynb via
# pandoc (Read the Docs' build image bundles pandoc; a bare GH runner
# does not, so declare it here or the notebook stage fails).
sudo apt-get install -y ffmpeg fonts-noto-cjk pandoc
fc-cache -f
- name: Export a pristine source tree (tracked files only)
run: |
mkdir -p /tmp/docs-clean
git archive --format=tar HEAD | tar -x -C /tmp/docs-clean
# the generated gallery must NOT be tracked: a fresh RTD build
# regenerates it, so its presence here would MASK execution failures
if [ -e /tmp/docs-clean/docs/auto_examples ]; then
echo "::error::docs/auto_examples is tracked; it must be generated fresh"
exit 1
fi
- name: Install package + docs requirements + headless Chrome
run: |
python -m pip install --upgrade pip
cd /tmp/docs-clean
pip install .
pip install -r docs/doc_requirements.txt
# headless Chrome for the Plotly gallery scraper (kaleido 1.x); RTD
# provisions it identically via .readthedocs.yaml's post_install job
plotly_get_chrome -y
- name: Build docs from clean source (warnings = errors, cold gallery)
env:
MPLBACKEND: Agg
# raise the fivethirtyeight/kaggle loader's GitHub API quota (some
# gallery examples load hosted datasets during execution)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd /tmp/docs-clean/docs
python -m sphinx -b html -W -E -a . _build/html
- name: Release gate -- generated gallery notebooks install the PyPI package
# docs/auto_examples/*.ipynb are gitignored and GENERATED by the build
# above from docs/conf.py's branch-aware install-cell (so they don't exist
# in a bare checkout and the release-gate job cannot scan them). Here they
# DO exist, so on a release build require every generated gallery notebook
# to carry a VALID PyPI install (hypertools[...], no git+/@<branch>, no
# preview note) -- covering all 68 published notebooks. The check is
# scripts/check_release_notebooks.py, unit-tested in
# tests/test_release_notebook_check.py. 2026-07 release review.
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
run: |
python /tmp/docs-clean/scripts/check_release_notebooks.py \
--min 50 /tmp/docs-clean/docs/auto_examples
# Dataset release gate (2026-07 review, findings #2 + #2-followup). Two
# things a green matrix run does NOT prove: (a) that the hosted-dataset
# checks actually ran -- the matrix SKIPS a dataset it can't download so
# transient outages don't block unrelated PRs; and (b) that the Dropbox/Drive
# URLs are still REACHABLE -- the loader accepts a hash-valid CACHE hit
# without contacting the host. This job forbids both gaps: it uses NO dataset
# cache and clears ~/hypertools_data first, so every artifact is fetched
# FRESH from its host, and HYPERTOOLS_REQUIRE_DATASETS=1 turns any
# download/load failure into a HARD failure (never a skip). So it genuinely
# proves host availability + integrity + baseline match, and reports the
# exact count. Actions SHA-pinned like the jobs above.
dataset-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 (pinned to SHA)
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.4.0 (pinned to SHA)
with:
python-version: '3.12'
- name: Install package
run: |
python -m pip install --upgrade pip
# base install (NOT [dev]) so this gate also proves the hosted datasets
# load with only the base dependency stack -- plus pytest to run them
# and pytest-timeout to honor the suite's 20-min hung-download safety net
pip install . pytest pytest-timeout
- name: Require EVERY hosted dataset to DOWNLOAD FRESH + match the pinned baseline
env:
MPLBACKEND: Agg
HYPERTOOLS_REQUIRE_DATASETS: '1'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# NO cache above + clear any cache dir here => every dataset is fetched
# fresh from its host, so this is a genuine host-availability gate (not
# a cache-validity check). A download/load failure fails the job (it
# cannot skip); -s prints the exact count validated.
rm -rf ~/hypertools_data
python -m pytest -v -s \
tests/test_dataset_compat.py \
tests/test_dataset_integrity.py
# Release readiness gate (2026-07 release review). Several release-facing
# references are deliberately in DEV form on dev branches and MUST flip at
# publish; nothing else proves they were all flipped. This job runs ONLY on
# master / release tags, sets HYPERTOOLS_REQUIRE_RELEASE=1, and turns any
# surviving dev-form reference into a HARD failure (it cannot pass by
# skipping): (a) a `git+`/`@<branch>` hypertools install in a published
# notebook; (b) a commit-SHA-pinned README image URL (must be the v1.0.0
# tag); (c) a `(unreleased)` CHANGELOG heading (must be dated). See
# RELEASE_CHECKLIST.md for the exact publish sequence that makes it green.
# Actions SHA-pinned like the jobs above.
release-gate:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 (pinned to SHA)
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.4.0 (pinned to SHA)
with:
python-version: '3.12'
- name: Install pytest
run: python -m pip install --upgrade pip pytest
- name: Require all release-facing references to be in RELEASE form
env:
HYPERTOOLS_REQUIRE_RELEASE: '1'
# the gallery gate reads the docs-notebooks branch tree via the GitHub
# API; authenticate it (5000/hr vs the 60/hr unauthenticated limit) so
# the gate can't throttle-fail intermittently. Read-only per the
# top-level `permissions: contents: read`.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# any surviving dev-form reference fails the job; -s prints the
# offenders. RELEASE_CHECKLIST.md documents how to make each green.
python -m pytest -v -s \
tests/test_notebook_install_gate.py \
tests/test_release_readiness_gate.py