docs(changelog): record JSS reproducibility hardening + 07_scm fixtur… #453
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
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| release: | |
| types: [ published ] | |
| workflow_dispatch: | |
| inputs: | |
| publish_target: | |
| description: 'Which index to publish to' | |
| type: choice | |
| options: | |
| - none | |
| - testpypi-only | |
| - pypi-only | |
| - both | |
| default: none | |
| # Opt into Node 24 runtime early for all JavaScript actions. | |
| # GitHub's documented migration path (blog post 2025-09-19) before the | |
| # hard cutover on 2026-06-02. Keeps existing action versions intact and | |
| # just swaps the runtime, so no surface-area change in our steps. | |
| # Security: pure runner config, no user input. | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' | |
| jobs: | |
| # Decide the test matrix based on trigger: | |
| # - pull_request: 1 combo (ubuntu-latest × 3.10) — fast PR feedback, keeps Codecov upload | |
| # - push / release / workflow_dispatch: full 13-combo matrix | |
| # Rationale: PR CI was ~44 min wall-clock on the slowest Windows shard; a PR | |
| # rarely needs all 13 shards to catch regressions, and push-to-main still runs | |
| # the full matrix before any release. Fully reversible: delete set-matrix and | |
| # inline the old matrix block to restore. | |
| set-matrix: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.compute.outputs.matrix }} | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| steps: | |
| - id: compute | |
| shell: bash | |
| run: | | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| # Match the Codecov upload combo (ubuntu-latest × 3.10) so PR runs still publish coverage. | |
| echo 'matrix={"include":[{"os":"ubuntu-latest","python-version":"3.10"}]}' >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'matrix={"include":[{"os":"ubuntu-latest","python-version":"3.9"},{"os":"ubuntu-latest","python-version":"3.10"},{"os":"ubuntu-latest","python-version":"3.11"},{"os":"ubuntu-latest","python-version":"3.12"},{"os":"ubuntu-latest","python-version":"3.13"},{"os":"windows-latest","python-version":"3.10"},{"os":"windows-latest","python-version":"3.11"},{"os":"windows-latest","python-version":"3.12"},{"os":"windows-latest","python-version":"3.13"},{"os":"macos-latest","python-version":"3.10"},{"os":"macos-latest","python-version":"3.11"},{"os":"macos-latest","python-version":"3.12"},{"os":"macos-latest","python-version":"3.13"}]}' >> "$GITHUB_OUTPUT" | |
| fi | |
| test: | |
| needs: set-matrix | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.set-matrix.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Cache pip dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Lint with flake8 (hard syntax errors — all matrix entries) | |
| # E9/F63/F7/F82 are real syntax / undefined-name breakage and must be | |
| # zero on every OS × Python combo. | |
| run: | | |
| flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics | |
| - name: flake8 baseline gate (single canonical env) | |
| # The full violation COUNT drifts by Python version (3.12's PEP-701 | |
| # f-string tokenisation makes pyflakes report more on 3.12/3.13), so the | |
| # ``scripts/quality_gate.py`` count ratchet is pinned to one canonical | |
| # interpreter — same pattern as the mypy gate below — to stay stable. | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' | |
| run: | | |
| python scripts/quality_gate.py flake8 | |
| - name: Type baseline with mypy | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' | |
| run: | | |
| python scripts/quality_gate.py mypy | |
| # Coverage instrumentation crashes on numpy 2.0.2 (the last numpy that | |
| # supports Python 3.9): under coverage's tracer numpy is imported twice, | |
| # which breaks the ``np._NoValue`` sentinel identity and makes reductions | |
| # raise ``int()/float() ... not '_NoValueType'``. Fixed upstream in | |
| # numpy >= 2.1 (requires Python >= 3.10). So measure coverage on a single | |
| # modern interpreter — ubuntu + 3.10, present in both the reduced and full | |
| # matrices and the same pin used by the flake8/mypy gates — where numpy is | |
| # >= 2.1 and the bug is absent. Every other matrix entry runs the full | |
| # suite without ``--cov``. Root cause: docs/rfc (E1 analysis). | |
| # NEEDS-CI-VERIFICATION: confirm the 3.10 coverage run is green in CI, then | |
| # raise --cov-fail-under from 15 toward the measured baseline. | |
| - name: Test with pytest (coverage) | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' | |
| run: | | |
| pytest tests/ -v --cov=statspai --cov-report=xml --cov-report=term-missing --cov-fail-under=15 | |
| - name: Test with pytest (no coverage) | |
| if: ${{ !(matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10') }} | |
| run: | | |
| pytest tests/ -v | |
| - name: Build and smoke installed wheel | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' | |
| shell: bash | |
| run: | | |
| python -m build --sdist --wheel | |
| python -m venv .wheel-smoke | |
| .wheel-smoke/bin/python -m pip install --upgrade pip | |
| .wheel-smoke/bin/python -m pip install dist/*.whl | |
| .wheel-smoke/bin/python - <<'PY' | |
| import statspai as sp | |
| from statspai.cli import main | |
| assert sp.__version__ | |
| assert "StatsPAI" in sp.citation("cff") | |
| assert main(["version"]) == 0 | |
| PY | |
| - name: Upload coverage to Codecov | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| file: ./coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| build: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build package | |
| run: python -m build | |
| - name: Check package | |
| run: twine check dist/* | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-packages | |
| path: dist/ | |
| publish-test: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| if: | | |
| (github.event_name == 'release' && github.event.action == 'published') || | |
| (github.event_name == 'workflow_dispatch' && (inputs.publish_target == 'testpypi-only' || inputs.publish_target == 'both')) | |
| environment: testpypi | |
| permissions: | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build package | |
| run: python -m build | |
| - name: Check package | |
| run: twine check dist/* | |
| - name: Publish to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| skip-existing: true | |
| publish-prod: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| if: | | |
| (github.event_name == 'release' && github.event.action == 'published') || | |
| (github.event_name == 'workflow_dispatch' && (inputs.publish_target == 'pypi-only' || inputs.publish_target == 'both')) | |
| environment: pypi | |
| permissions: | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build package | |
| run: python -m build | |
| - name: Check package | |
| run: twine check dist/* | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| skip-existing: true | |
| security: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install safety bandit | |
| - name: Run safety check | |
| run: safety check --json || true | |
| - name: Run bandit security check | |
| run: bandit -r src/ -f json --severity-level medium |