v1.3.3: Release — Collab integration, UI improvements, CLI enhancemen… #1128
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: Code CI | |
| on: | |
| push: | |
| # Fire on every branch so the `gate` job below can decide whether to run. | |
| # GitHub cannot filter on commit message at the event level, so we trigger | |
| # broadly and gate execution: main/dev always run, other branches run only | |
| # when the head commit message contains "[force ci]". | |
| branches: [ '**' ] | |
| tags: [ 'v*' ] | |
| pull_request: | |
| types: [ opened, reopened, ready_for_review, synchronize ] | |
| workflow_dispatch: | |
| jobs: | |
| # Single source of truth for "should the core CI jobs run?". Reproduces the | |
| # previous trigger rules (PR / manual dispatch / main / dev run; tags skip) | |
| # and adds a manual escape hatch for custom branches via "[force ci]". | |
| gate: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run_ci: ${{ steps.decide.outputs.run_ci }} | |
| steps: | |
| - name: Decide whether to run CI | |
| id: decide | |
| env: | |
| # Evaluated by GitHub (not the shell) so the raw commit message is | |
| # never interpolated into bash — avoids quoting/injection issues. | |
| FORCE_CI: ${{ contains(github.event.head_commit.message, '[force ci]') }} | |
| run: | | |
| if [ "${{ github.event_name }}" != "push" ]; then | |
| run_ci=true # PRs and manual dispatch always run | |
| elif [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| run_ci=false # tag pushes are handled by the release workflow | |
| elif [ "${{ github.ref }}" = "refs/heads/main" ] || [ "${{ github.ref }}" = "refs/heads/dev" ] || [[ "${{ github.ref }}" == refs/heads/v* ]]; then | |
| run_ci=true # default branches and release branches (v*) always run | |
| elif [ "${FORCE_CI}" = "true" ]; then | |
| run_ci=true # custom branch opted in via [force ci] | |
| else | |
| run_ci=false # custom branch without [force ci] → skip | |
| fi | |
| echo "run_ci=${run_ci}" >> "$GITHUB_OUTPUT" | |
| echo "Decision: run_ci=${run_ci} (event=${{ github.event_name }}, ref=${{ github.ref }}, force_ci=${FORCE_CI})" | |
| # Required gate: a lint violation here FAILS the CI run. Nothing `needs` this | |
| # job, so it runs in parallel with install/test/build — its failure turns the | |
| # run red but does NOT stop the other jobs from running (GitHub Actions does | |
| # not cancel sibling jobs on failure). | |
| code-quality: | |
| needs: gate | |
| if: ${{ needs.gate.outputs.run_ci == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for comparison | |
| - name: Set up Python | |
| id: setup-python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Get changed Python files | |
| id: changed-files | |
| run: | | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| BASE_SHA=${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA=${{ github.event.pull_request.head.sha }} | |
| else | |
| BASE_SHA=${{ github.event.before }} | |
| HEAD_SHA=${{ github.sha }} | |
| fi | |
| # Get changed Python files in weightslab directory, exclude generated files | |
| CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT $BASE_SHA $HEAD_SHA | \ | |
| grep '^weightslab/.*\.py$' | \ | |
| grep -v '_pb2\.py$' | \ | |
| grep -v '_pb2_grpc\.py$' | \ | |
| grep -v 'ui\.py$' | \ | |
| grep -v 'trainer.*\.py$' || true) | |
| echo "changed_files<<EOF" >> $GITHUB_OUTPUT | |
| echo "$CHANGED_FILES" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| if [ -z "$CHANGED_FILES" ]; then | |
| echo "has_files=false" >> $GITHUB_OUTPUT | |
| echo "No Python files changed in weightslab directory" | |
| else | |
| echo "has_files=true" >> $GITHUB_OUTPUT | |
| echo "Changed files:" | |
| echo "$CHANGED_FILES" | |
| fi | |
| - name: Install linting tools | |
| if: steps.changed-files.outputs.has_files == 'true' | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install ruff pylint vulture | |
| - name: Lint with Ruff (unused, style, bugs) | |
| if: steps.changed-files.outputs.has_files == 'true' | |
| run: | | |
| FILES="${{ steps.changed-files.outputs.changed_files }}" | |
| if [ -n "$FILES" ]; then | |
| # No --fix in CI: report and FAIL on any selected violation (a fix in | |
| # the ephemeral runner would be discarded and silently pass the gate). | |
| echo "$FILES" | xargs ruff check --select F401,F841,E9,F632,F722,F823 --ignore E501 ./weightslab | |
| fi | |
| - name: Pylint (errors only) | |
| if: steps.changed-files.outputs.has_files == 'true' | |
| run: | | |
| FILES="${{ steps.changed-files.outputs.changed_files }}" | |
| if [ -n "$FILES" ]; then | |
| echo "$FILES" | xargs pylint --disable=R0913,E0401,C0301,W0511,C0114,C0115,C0116 --fail-under=7.0 ./weightslab | |
| fi | |
| - name: Vulture (unused code) | |
| if: steps.changed-files.outputs.has_files == 'true' | |
| run: | | |
| FILES="${{ steps.changed-files.outputs.changed_files }}" | |
| if [ -n "$FILES" ]; then | |
| # Advisory only (|| true): vulture at this confidence yields false | |
| # positives on dynamically-referenced code (CLI entry points, gRPC | |
| # handlers, framework hooks). Ruff + Pylint above are the hard gates. | |
| echo "$FILES" | xargs vulture --min-confidence 70 ./weightslab || true | |
| fi | |
| # ── Fast install gate (always runs) ────────────────────────────────────── | |
| # Single-version install on 3.11 — always runs so `test` has a gate to | |
| # depend on regardless of branch. | |
| install: | |
| needs: gate | |
| if: ${{ needs.gate.outputs.run_ci == 'true' }} | |
| runs-on: ubuntu-latest | |
| name: install (Python 3.11) | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install project dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e . --extra-index-url https://download.pytorch.org/whl/cpu | |
| - name: Verify installation | |
| run: | | |
| python -c "import weightslab; print(f'weightslab imported successfully')" | |
| # ── Full Python version matrix (PR and main only) ───────────────────────── | |
| # Verifies that `pip install` + basic import work across all supported Python | |
| # versions. Runs on every PR and every push to main; skipped on dev/other | |
| # branch pushes to avoid unnecessary cost on work-in-progress commits. | |
| install-matrix: | |
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false # keep running other versions if one fails | |
| matrix: | |
| include: | |
| - python-version: '3.10' | |
| - python-version: '3.11' | |
| - python-version: '3.12' | |
| - python-version: '3.13' | |
| - python-version: '3.14' | |
| allow-prereleases: true # 3.14 is pre-release; visible failure but non-blocking | |
| name: install (Python ${{ matrix.python-version }}) | |
| # 3.14 pre-release failures are informational — don't block the pipeline. | |
| continue-on-error: ${{ matrix.allow-prereleases == true }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| allow-prereleases: ${{ matrix.allow-prereleases || false }} | |
| - name: Install project dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e . --extra-index-url https://download.pytorch.org/whl/cpu | |
| - name: Verify installation | |
| run: | | |
| python -c "import weightslab; print(f'weightslab imported successfully on Python ${{ matrix.python-version }}')" | |
| test: | |
| if: ${{ needs.gate.outputs.run_ci == 'true' }} | |
| runs-on: ubuntu-latest | |
| # Depends on the fast 3.11 install gate; `gate` is also a direct need so this | |
| # job can read run_ci (the matrix and main-only jobs run independently). | |
| needs: [ gate, install ] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install project dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| # Install the test extra so pytest, graphviz, torchmetrics, | |
| # pytorch-lightning and tensorboard are available (several test modules | |
| # import pytest / use pytest fixtures and cannot run under bare unittest). | |
| python -m pip install '.[utest]' --extra-index-url https://download.pytorch.org/whl/cpu | |
| python -m pip install pytest-timeout | |
| - name: Run unit tests - General | |
| run: | | |
| export WEIGHTSLAB_LOG_LEVEL="DEBUG" | |
| # A per-test timeout guards against any regression that hangs a test. | |
| python -m pytest ./tests -v --timeout=300 | |
| build-and-publish-dev: | |
| # Only publish to TestPyPI when pushing to main (not on PRs or dev branch pushes). | |
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| name: Build & Publish Dev (TestPyPI) | |
| needs: test | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| published_version: ${{ steps.version.outputs.published_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install build tools | |
| run: python -m pip install --upgrade pip build twine | |
| - name: Compute publish version | |
| id: version | |
| run: | | |
| TS="$(date -u +%Y%m%d%H%M%S)" | |
| SHA_HEX="$(git rev-parse --short=8 HEAD)" | |
| # TestPyPI/PyPI reject local versions (+sha), so encode sha as decimal in devN. | |
| SHA_DEC="$((16#${SHA_HEX}))" | |
| PRETEND="${TS}.dev${SHA_DEC}" | |
| echo "published_version=${PRETEND}" >> $GITHUB_OUTPUT | |
| echo "Publishing version: ${PRETEND} (from sha ${SHA_HEX})" | |
| - name: Clean build artifacts | |
| run: | | |
| rm -rf build dist *.egg-info | |
| - name: Build distributions | |
| env: | |
| # Package-specific override is the most reliable way with setuptools_scm. | |
| SETUPTOOLS_SCM_PRETEND_VERSION_FOR_WEIGHTSLAB: ${{ steps.version.outputs.published_version }} | |
| run: | | |
| echo "Building with version ${SETUPTOOLS_SCM_PRETEND_VERSION_FOR_WEIGHTSLAB}" | |
| python -m build | |
| - name: Verify built artifact version | |
| env: | |
| WL_VERSION: ${{ steps.version.outputs.published_version }} | |
| run: | | |
| echo "Expected version: ${WL_VERSION}" | |
| echo "Files in dist:" | |
| ls -la dist/ | |
| # Check for 0.0.0 artifacts (which indicate version not set) | |
| if ls dist/ | grep -q "0.0.0"; then | |
| echo "ERROR: Build produced 0.0.0 artifacts (version not set properly)" | |
| exit 1 | |
| fi | |
| # Verify at least one artifact has the expected version | |
| if ! ls dist/ | grep -q "${WL_VERSION}"; then | |
| echo "ERROR: Expected version ${WL_VERSION} not found in dist artifacts" | |
| echo "Available files:" | |
| ls -1 dist/ | |
| exit 1 | |
| fi | |
| echo "✓ Version verification passed" | |
| - name: Check distributions | |
| run: python -m twine check dist/* | |
| - name: Publish to TestPyPI | |
| env: | |
| TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} | |
| TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} | |
| run: | | |
| if [ -z "${{ secrets.TEST_PYPI_API_TOKEN }}" ]; then | |
| echo "TEST_PYPI_API_TOKEN not set; skipping TestPyPI upload." | |
| exit 0 | |
| fi | |
| python -m twine upload --repository-url https://test.pypi.org/legacy/ --verbose dist/* | |
| # ── Cross-version install test from TestPyPI ────────────────────────────── | |
| # Installs the just-published dev package from TestPyPI on every supported | |
| # Python version and verifies the import works end-to-end. | |
| # Runs only on main branch pushes (same gate as the publish step above). | |
| test-install-from-pip-dev: | |
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| name: Test Install From Pip Dev (Python ${{ matrix.python-version }}) | |
| needs: build-and-publish-dev | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - python-version: '3.10' | |
| - python-version: '3.11' | |
| - python-version: '3.12' | |
| - python-version: '3.13' | |
| - python-version: '3.14' | |
| allow-prereleases: true | |
| continue-on-error: ${{ matrix.allow-prereleases == true }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| allow-prereleases: ${{ matrix.allow-prereleases || false }} | |
| - name: Create isolated virtual environment | |
| run: | | |
| python -m venv .venv-ci | |
| - name: Install from TestPyPI (wait until indexed) | |
| env: | |
| WL_VERSION: ${{ needs.build-and-publish-dev.outputs.published_version }} | |
| run: | | |
| . .venv-ci/bin/activate | |
| python -m pip install --upgrade pip | |
| if [ -z "${WL_VERSION}" ]; then | |
| echo "Missing published version from upstream job" | |
| exit 1 | |
| fi | |
| # TestPyPI can take a few minutes to expose newly uploaded versions. | |
| for attempt in $(seq 1 60); do | |
| echo "Checking pip visibility for weightslab==${WL_VERSION} (attempt ${attempt}/60)" | |
| if python -m pip download \ | |
| --index-url https://test.pypi.org/simple/ \ | |
| --no-deps \ | |
| "weightslab==${WL_VERSION}" \ | |
| -d /tmp/wl-probe >/dev/null 2>&1; then | |
| echo "Found weightslab==${WL_VERSION} in TestPyPI simple index" | |
| break | |
| fi | |
| if [ "${attempt}" -eq 60 ]; then | |
| echo "Timed out waiting for weightslab==${WL_VERSION} on TestPyPI simple index" | |
| exit 1 | |
| fi | |
| sleep 7 | |
| done | |
| python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple "weightslab==${WL_VERSION}" --no-cache-dir --retries 10 --timeout 60 | |
| echo "weightslab==${WL_VERSION} installed from TestPyPI on Python ${{ matrix.python-version }}" | |
| - name: Verify package import | |
| run: | | |
| . .venv-ci/bin/activate | |
| python -c " | |
| import weightslab, importlib.metadata as m | |
| print('weightslab import OK on Python ${{ matrix.python-version }}') | |
| print('installed version:', m.version('weightslab')) | |
| " |