# How to Set Up CI/CD for Switchyard Projects: A Complete GitHub Actions Guide

> Master CI/CD for Switchyard projects using GitHub Actions. Automate Python linting, Rust builds, and Maturin wheel packaging for robust validation on every push.

- Repository: [NVIDIA-NeMo/Switchyard](https://github.com/NVIDIA-NeMo/Switchyard)
- Tags: how-to-guide
- Published: 2026-08-23

---

**Setting up CI/CD for Switchyard requires orchestrating Python linting and testing, Rust workspace builds, and Maturin wheel packaging in GitHub Actions to validate the hybrid codebase on every push.**

Switchyard, NVIDIA's hybrid Python/Rust routing framework, maintains a native server core alongside Python bindings in `crates/switchyard-py/`. Because the project spans two languages and requires compiled artifacts, implementing robust CI/CD for Switchyard projects demands distinct validation stages for each stack. This guide walks through the exact GitHub Actions configuration used to lint Python code, test the Rust workspace, and publish distributable wheels.

## Understanding the Dual-Stack Architecture

Switchyard is organized as a Rust workspace with PyO3 Python bindings. Key files defining the build and test surface include:

- [`pyproject.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/pyproject.toml): Declares Python dependencies, development groups, and build scripts using `uv`
- [`Cargo.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/Cargo.toml): Root workspace manifest aggregating all Rust crates under `crates/*`
- `crates/switchyard-py/`: Houses the PyO3 integration layer that compiles into the Python wheel
- `tests/`: Contains Python-level unit tests for the public API and CLI
- [`mkdocs.yml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/mkdocs.yml): Configures documentation generation (optional CI step for docs validation)

## Python Stack: Linting and Testing

The Python layer uses `uv` for dependency management and requires three validation steps before packaging.

### Linting with Ruff and MyPy

Static analysis ensures code quality before runtime testing. The development dependency group includes both tools:

```bash

# Install dev dependencies including ruff and mypy

uv sync --group dev

# Lint Python source

uv run ruff check .

# Type-check the switchyard package

uv run mypy switchyard

```

### Running the Python Test Suite

Execute pytest against the `tests/` directory to validate the integration layer:

```bash
uv run pytest tests/ -v

```

## Rust Stack: Workspace Testing

The native server and routing algorithms live in the Rust workspace defined by the root [`Cargo.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/Cargo.toml). Testing requires the stable toolchain and workspace-level execution:

```bash

# Ensure stable Rust is available

rustup toolchain install stable

# Test all crates in the workspace

cargo test --workspace

```

This validates the core logic that the Python bindings expose.

## Packaging Stack: Wheels and Distribution

Switchyard uses **Maturin** to build Python wheels embedding the compiled Rust binaries. This stage should only execute after both Python and Rust tests pass.

### Building the Wheel

Generate a release-grade wheel for distribution:

```bash

# Build manylinux-compatible wheel (disable manylinux container for standard runners)

uv run maturin build --release --manylinux off

```

The output appears in `target/wheels/`.

### Continuous Deployment Options

Extend the pipeline to publish artifacts:

- **PyPI**: Use `twine upload target/wheels/*.whl` after building
- **Docker**: Build and push an image containing the native server binary for containerized deployments

## Orchestrating the Pipeline in GitHub Actions

Combine these stages into [`.github/workflows/ci.yml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/.github/workflows/ci.yml) with strategic job dependencies to maximize parallelism while preventing broken code from reaching packaging:

```yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh
      - name: Install dependencies
        run: uv sync --group dev
      - name: Run ruff
        run: uv run ruff check .
      - name: Run mypy
        run: uv run mypy switchyard

  python-tests:
    runs-on: ubuntu-latest
    needs: lint-typecheck
    steps:
      - uses: actions/checkout@v4
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh
      - name: Sync dependencies
        run: uv sync --group dev
      - name: Run pytest
        run: uv run pytest tests/ -v

  rust-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          override: true
      - name: Cargo test
        run: cargo test --workspace

  build-wheel:
    runs-on: ubuntu-latest
    needs: [python-tests, rust-tests]
    steps:
      - uses: actions/checkout@v4
      - name: Install uv and maturin
        run: |
          curl -LsSf https://astral.sh/uv/install.sh | sh
          uv pip install maturin
      - name: Build wheel
        run: uv run maturin build --release --manylinux off
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: wheel
          path: target/wheels/*.whl

```

### Pipeline Optimization Strategies

- **Parallel Execution**: The **lint-typecheck** and **rust-tests** jobs run concurrently to minimize total CI time. Only **build-wheel** waits for verification.
- **Artifact Persistence**: The `actions/upload-artifact` step preserves wheels for downstream deployment jobs or manual inspection.
- **Isolation**: Each job runs on a fresh `ubuntu-latest` runner, eliminating hidden state between Python and Rust environments.

## Summary

- **Switchyard** requires CI/CD pipelines that handle both Python ([`pyproject.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/pyproject.toml)) and Rust ([`Cargo.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/Cargo.toml)) validation in parallel tracks
- Use `uv sync --group dev` to install development dependencies including **ruff**, **mypy**, and **pytest**
- Run Rust tests with `cargo test --workspace` from the repository root to validate the native core
- Package the hybrid library using `uv run maturin build --release --manylinux off` to embed Rust binaries in Python wheels
- Structure GitHub Actions with separate jobs for Python linting, Python testing, Rust testing, and wheel building to maximize reliability and CI performance

## Frequently Asked Questions

### Does Switchyard require specific uv configuration for CI?

No additional configuration is required beyond the standard `uv sync --group dev` command. The [`pyproject.toml`](https://github.com/NVIDIA-NeMo/Switchyard/blob/main/pyproject.toml) in the repository root declares all development dependencies, including the test runner and build tools, which `uv` resolves automatically in the GitHub Actions runner.

### How do I handle maturin builds in GitHub Actions?

Install `maturin` via `uv pip install maturin` after installing `uv` itself. Execute `uv run maturin build --release --manylinux off` to generate wheels compatible with the target environment. Always run this step after both Python and Rust tests complete to ensure packaging only occurs with verified code.

### Can I run Rust and Python tests in parallel?

Yes. The recommended workflow places **rust-tests** and **lint-typecheck** in separate jobs without cross-dependencies, allowing GitHub Actions to execute them concurrently. Only the **python-tests** job depends on linting, and **build-wheel** depends on both test suites, creating an efficient dependency graph.

### What triggers should I use for the Switchyard CI pipeline?

Configure the workflow to trigger on `push` events to the `main` branch and `pull_request` events targeting `main`. This ensures every proposed change and every merge undergoes full validation across both the Python integration layer and the native Rust core.