Switch to side-by-side view

--- a
+++ b/.github/workflows/bionemo-subpackage-ci.yml
@@ -0,0 +1,226 @@
+name: BioNeMo Sub-Package Workflow
+
+on:
+  # To test or publish sub-packages or adjustments to this workflow that are branched in PR's, manually dispatch this workflow on the PR's branch here: https://github.com/NVIDIA/bionemo-framework/actions/workflows/bionemo-subpackage-ci.yml.
+  workflow_dispatch:
+    inputs:
+      subpackages:
+        description: "BioNeMo sub-packages (comma-separated) to test or publish."
+        required: true
+        type: string
+      test:
+        description: "Test the sub-packages before publishing to PyPI. Strongly recommended for production releases to PyPI. Can be disabled when staging sub-packages on Test PyPI or publishing circular dependencies to PyPI."
+        required: false
+        type: boolean
+        default: true
+      publish:
+        description: "Publish the built package to PyPI. If testing is specified, requires that all sub-package tests succeed based on dependencies published to Test PyPI or PyPI."
+        required: false
+        type: boolean
+        default: false
+      pypi:
+        description: "Publish to PyPI instead of Test PyPI."
+        required: false
+        type: boolean
+        default: false
+      version_overwrite:
+        description: "Overwrite the published version of the sub-package. (Sets skip-existing to False. Requires deleting existing wheels and other artifacts on PyPI.)"
+        required: false
+        type: boolean
+        default: false
+      build_framework:
+        description: "Build framework to use for building and publishing."
+        type: choice
+        options:
+          - "python"
+          - "rust_pyo3_maturin"
+        default: "python"
+        required: true
+      python_version:
+        description: "Python version to use for testing and publishing."
+        required: false
+        type: string
+        default: "3.12"
+      gpu_runner:
+        description: "Specify a GPU runner for testing on NVIDIA GitHub Actions. (For a list of available runners, refer to: https://docs.gha-runners.nvidia.com/runners/)"
+        required: false
+        type: string
+        default: "linux-amd64-gpu-l4-latest-1"
+      cuda_version:
+        description: "NVIDIA CUDA container version to use for testing."
+        required: false
+        type: string
+        default: "nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04"
+
+jobs:
+  configure-workflow-packages:
+    name: "[Configure Workflow Packages] Identify sub-packages for testing and publishing."
+    runs-on: ubuntu-latest
+    outputs:
+      workflow_packages: ${{ steps.parse-dispatch-packages.outputs.dispatch_packages }}
+    steps:
+      - id: parse-dispatch-packages
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        name: Parse the sub-packages specified in the workflow dispatch.
+        run: |
+          # Send the parsed list of sub-packages to the next job.
+          dispatch_packages=$(echo '${{ github.event.inputs.subpackages }}' | jq -R -c 'split(",")')
+          echo "dispatch_packages=$dispatch_packages" >> "$GITHUB_OUTPUT"
+          echo "[BioNeMo Sub-Package CI] Sub-packages to stage: $dispatch_packages"
+
+  install-and-test:
+    needs: configure-workflow-packages
+    # Check if the previous job has any staged packages to test and publish.
+    if: ${{ needs.configure-workflow-packages.outputs.workflow_packages != '[]' }}
+    strategy:
+      matrix:
+        package: ${{ fromJson(needs.configure-workflow-packages.outputs.workflow_packages) }}
+      fail-fast: false  # Prevent all matrix jobs from failing if one fails.
+    name: "[${{ matrix.package }}] Install and test sub-package."
+    # Use GPU runner only when testing, otherwise use a standard runner.
+    runs-on: ${{ github.event.inputs.test == 'true' && github.event.inputs.gpu_runner || 'ubuntu-latest' }}
+    container:
+      # GPU jobs must run in a container. Use a fresh CUDA base container for package installation and testing.
+      # If testing is disabled, use a lightweight container to quickly skip this job.
+      image: ${{ github.event.inputs.test == 'true' && github.event.inputs.cuda_version || 'ubuntu:latest' }}
+    steps:
+      # Silently skip all steps if testing is disabled, which does not block building or publishing.
+      - name: Install git and system dependencies.
+        if: ${{ github.event.inputs.test == 'true' }}
+        run: |
+          apt-get update
+          apt-get install -qyy git curl lsb-release build-essential
+      - uses: actions/checkout@v4
+        if: ${{ github.event.inputs.test == 'true' }}
+        with:
+          fetch-depth: 0
+          submodules: "recursive"
+      - uses: actions/setup-python@v5
+        if: ${{ github.event.inputs.test == 'true' }}
+        with:
+          python-version: ${{ github.event.inputs.python_version }}
+      - id: install-rust
+        if: ${{ github.event.inputs.test == 'true' }}
+        name: Install Rust.
+        run: |
+          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+          . $HOME/.cargo/env
+          rustc --version
+          cargo --version
+          rustup --version
+      - id: install-subpackage-core
+        if: ${{ github.event.inputs.test == 'true' }}
+        name: Install sub-package.
+        run: |
+          # Setup environment, i.e. add Rust to PATH and silence pip root user warnings.
+          . $HOME/.cargo/env
+          # Install sub-package and dependencies.
+          pip install --upgrade pip setuptools uv maturin
+          # Install required core & optional [test] dependencies.
+          uv pip install --no-cache --system pytest sub-packages/${{ matrix.package }}[test]
+      - id: install-subpackage-post
+        if: ${{ github.event.inputs.test == 'true' }}
+        name: Install sub-package dependencies that need to be installed after the core dependencies.
+        run: |
+          # DEV: Post-install dependencies are configured in [project.optional-dependencies].
+          # `uv pip install --extra <optional-dependency> -r <pyproject.toml>` tracks
+          # post-dependencies in the pyproject.toml and avoids installing core dependencies
+          # redundantly, which causes errors with incompatible --config-setting.
+
+          # TransformerEngine
+          uv pip install --no-cache --no-build-isolation --system --extra te -r sub-packages/${{ matrix.package }}/pyproject.toml || echo "[BioNeMo Sub-Package CI] TE will not be installed."
+
+          # # Apex
+          # # NOTE: --cpp_ext and --cuda_ext are required for building fused Apex kernels.
+          # uv pip install --no-cache --no-build-isolation --system --config-setting="--build-option=--cpp_ext" --config-setting="--build-option=--cuda_ext" --extra apex -r sub-packages/${{ matrix.package }}/pyproject.toml || echo "[BioNeMo Sub-Package CI] Apex will not be installed."
+      - id: test-dispatch-subpackage
+        if: ${{ github.event.inputs.test == 'true' }}
+        name: Test sub-package.
+        run: pytest -vv sub-packages/${{ matrix.package }}
+
+  build-pypi:
+    # Build distributions from either the workflow dispatch or PR.
+    # Validate building before merging or publishing.
+    needs: [configure-workflow-packages, install-and-test]
+    if: ${{ needs.configure-workflow-packages.outputs.workflow_packages != '[]' && github.event.inputs.publish == 'true' }}
+    outputs:
+      staged_packages: ${{ needs.configure-workflow-packages.outputs.workflow_packages }}
+    strategy:
+      matrix:
+        package: ${{ fromJson(needs.configure-workflow-packages.outputs.workflow_packages) }}
+      fail-fast: false  # Prevent all matrix jobs from failing if one fails.
+    name: "[${{ matrix.package }}] Build the sub-package."
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+      - uses: actions/setup-python@v5
+        with:
+          python-version: ${{ github.event.inputs.python_version }}
+      - id: build-package
+        name: Build a binary wheel and a source tarball for the sub-package.
+        run: |
+          if [[ "${{ github.event.inputs.test }}" != "true" && "${{ github.event.inputs.version_overwrite }}" != "true" ]]; then
+            # For untested sub-packages, append '-dev' to the version for PyPI.
+            sed -i 's/[[:space:]]*$//' sub-packages/${{ matrix.package }}/VERSION
+            sed -i 's/$/-dev/' sub-packages/${{ matrix.package }}/VERSION
+          fi
+          # Build the sub-package.
+          if [[ "${{ github.event.inputs.build_framework }}" == "python" ]]; then
+            pip install build
+            python -m build sub-packages/${{ matrix.package }}
+          elif [[ "${{ github.event.inputs.build_framework }}" == "rust_pyo3_maturin" ]]; then
+            # Install maturin[zig] to build the Rust sub-package with compatibility for manylinux_X_Y using zig.
+            pip install maturin[zig]
+            maturin build --release --zig -m sub-packages/${{ matrix.package }}/Cargo.toml
+          fi
+      - id: upload-distribution
+        name: Upload distribution packages to the workflow.
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ matrix.package }}-build-artifacts
+          path: ${{ github.event.inputs.build_framework == 'rust_pyo3_maturin' && format('sub-packages/{0}/target/wheels', matrix.package) || format('sub-packages/{0}/dist', matrix.package) }}
+
+  publish-to-pypi:
+    needs: [build-pypi, install-and-test]
+    # Require staged sub-package builds for publishing to PyPI.
+    if: ${{ needs.build-pypi.result == 'success' }}
+    strategy:
+      matrix:
+        package: ${{ fromJson(needs.build-pypi.outputs.staged_packages) }}
+      fail-fast: false  # Prevent all matrix jobs from failing if one fails.
+    name: Publish ${{ matrix.package }} to PyPI.
+    runs-on: ubuntu-latest
+    environment:
+      name: ${{ github.event.inputs.pypi && 'pypi' || 'testpypi' }}
+      url: ${{ github.event.inputs.pypi && format('https://pypi.org/p/{0}', matrix.package) || format('https://test.pypi.org/p/{0}', matrix.package) }}
+    permissions:
+      id-token: write
+    steps:
+      - id: download-distribution
+        name: Download the built distribution.
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ matrix.package }}-build-artifacts
+          path: sub-packages/${{ matrix.package }}/dist
+      - id: publish-to-testpypi
+        name: Publish distribution 📦 to Test PyPI for PR.
+        if: ${{ github.event.inputs.pypi == 'false' }}
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          verbose: true
+          packages-dir: sub-packages/${{ matrix.package }}/dist
+          repository-url: https://test.pypi.org/legacy/
+          skip-existing: ${{ github.event.inputs.version_overwrite }}
+      - id: publish-to-pypi
+        name: Publish distribution 📦 to PyPI for Workflow Dispatch.
+        # To require testing before publishing to PyPI, add: ... && needs.install-and-test.result == 'success'
+        # If testing is run but fails, the workflow will fail and not publish to PyPI (or Test PyPI).
+        # We strongly recommend testing when publishing to production PyPI.
+        if: ${{ github.event.inputs.pypi == 'true' }}
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          verbose: true
+          packages-dir: sub-packages/${{ matrix.package }}/dist
+          skip-existing: ${{ github.event.inputs.version_overwrite }}