Let's dive in advanced Python automation with nox, uv, and GitHub Actions — Part 5 of the Terminus Series

Building on Part 4, where we introduced fast, modern MLOps pipelines using uv, Docker, and CI/CD, this fifth installment focuses on automating your developer workflow using Nox — and why in 2025 it still reigns as one of the most flexible, transparent tools for Python-based projects.

This article walks through:

  • ✅ What exactly Nox is and why it's still relevant in 2025
  • 🧰 How we configure our noxfile in Terminus
  • 🚀 How GitHub Actions can dynamically discover and run your nox sessions
  • ⚖️ A brief but robust comparison of nox, Poe the Poet, and uv CLI
  • 🏗️ Why Nox, despite the rise of CLI runners, still plays a crucial orchestration role

Absolutely! Here's a more comprehensive and pedantic rewrite of the section "Going Deeper with uv CLI Tools", structured to clearly guide the reader, explain motivations and use cases, and gracefully transition into the next section on nox:

Going Deeper with uv CLI Tools — Beyond uv sync

Up to this point, we've mostly used uv as a fast, lockfile-aware dependency resolver — replacing pip, pip-tools, or poetry for installing packages via uv sync. But that’s only scratching the surface.

In practice, uv is becoming a holistic developer toolchain manager — and in projects like Terminus, it eliminates the need for make, bash, or even task runners like poethepoet in many cases.

Let’s explore the full potential of uv as a modern CLI toolchain — both for local development and CI/CD pipelines.

uv tool install and uv tool run: Global CLI without Virtualenv Pain

While uv sync installs project-scoped dependencies into virtual environments, uv tool manages CLI tools globally, with user isolation:

uv tool install nox
uv tool run nox -- -s lint

This:

  • Installs nox (or any CLI tool) into a dedicated global environment at ~/.local/share/uv/tools
  • Keeps these tools separate from your project env or system Python
  • Ensures reproducibility across developer machines and CI — version-pinned via uv.lock if needed
  • Avoids venv activation, making developer onboarding zero-friction

In CI (as in Terminus), this means we don't need to activate a venv, use pipx, or worry about system-wide pip issues. We simply run:

uv tool install nox
uv tool run nox -- -s lint

Caching CLI Tools Between CI Runs

One performance advantage of uv tool is that CLI tools are cached globally in:

~/.local/share/uv/tools

By caching this directory in GitHub Actions:

- uses: actions/cache@v4
  with:
    path: ~/.local/share/uv/tools
    key: uv-tools-${{ runner.os }}-${{ hashFiles('pyproject.toml', '**/uv.lock') }}

You:

  • Avoid repeated downloads or rebuilds of CLI tools (e.g., nox, ruff, pytest)
  • Keep your build minimal and declarative — no surprise installs during jobs
  • Ensure clarity between “project dependencies” (uv sync) and “development tools” (uv tool)

Defining CLI Commands with [tool.uv.cli] in pyproject.toml

Just like poethepoet, uv lets you define custom developer commands directly in pyproject.toml — no extra dependencies needed.

[tool.uv.cli]
lint = "ruff check src"
format = "ruff format src"
test = "pytest tests/"

Run them with:

uv run lint
uv run test

This gives you the ergonomics of a task runner (like make or poe) without adding a new config language or runtime dependency.

You can think of it as task aliases bound to your dependency context, all managed by uv.

Recommendations:

  • Use uv run or [tool.uv.cli] for simple local dev flows: lint, serve, test, etc.
  • Use poethepoet if you need cross-platform make-like chaining with config in pyproject.toml
  • Use nox if you need session orchestration, multi-version testing, and dynamic CI compatibility

Now that we understand how uv and uv tool fit into modern Python projects, let’s look at how we use nox in Terminus to automate linting, testing, and Docker builds — in both local and CI pipelines.

Let’s dive into nox and dynamic CI automation.

What is nox, and Why Use It?

Nox is a Python automation tool designed to run sessions — isolated environments where you install dependencies and run scripts.

Unlike make, poe, or raw shell scripts:

  • Each session is run in a fresh virtual environment (by default)
  • You can target multiple Python versions per session
  • Dependencies can be scoped using a tool like uv, pip, or poetry
  • It’s Python-native: no new DSL, write Python functions

In Terminus, we use nox as the automation layer for tasks like linting and testing and can be extended to build the docs, clean up etc.

This makes Nox ideal for CI workflows — especially when the definition of what to run changes over time (via dynamic session discovery).

Terminus noxfile.py — Dissection

Let’s look at how Terminus’ noxfile.py enforces:

  • Single source of truth for Python version (extracted from pyproject.toml)
  • Group-based dependency installation via uv
  • Dev-friendly modular automation

📄 terminus/noxfile.py

# Parses Python version from pyproject.toml
# Sets uv as the backend
# Defines two nox sessions: lint

@nox.session(python=PYTHON_VERSION)
def lint(session):
    """Run Ruff linter and formatter checks."""
    session.run(
        "uv", "sync", "--group", LINT_GROUP,
        f"--python={session.python}",
        env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
        external=True
    )
    session.run("ruff", "check", CODE_DIR)
    session.run("ruff", "format", "--check", CODE_DIR)

Key takeaways:

  • Uses uv as venv backend for speed and lockfile support.
  • Reads the required Python version directly from pyproject.toml for full alignment across local + CI workflows.
  • Dependency isolation: installs only the lint group, not the full app stack.

Dynamic Matrix CI with GitHub Actions + Nox

Instead of hardcoding jobs, Terminus uses a modern matrix approach:

Step 1: Determine Python version and available nox sessions

jobs:
  generate-matrix:
    outputs:
      sessions: ${{ steps.set-matrix.outputs.sessions }}
      python-version: ${{ steps.get-python-version.outputs.version }}
  • Reads the Python version from pyproject.toml
  • Extracts nox sessions via nox --json -l and filters with jq

All this happens before the test matrix runs, so any added sessions automatically trigger in CI without manual YAML changes.

Step 2: Run each session in parallel

jobs:
  run-nox-sessions:
    strategy:
      matrix:
        session: ${{ fromJson(needs.generate-matrix.outputs.sessions) }}
    steps:
      - run: nox -s "${{ matrix.session }}"

Each session is isolated, fast, and reproducible — and uses uv as the environment and dependency manager.


Side-by-side Comparison: nox vs poe vs uv cli

Feature nox poethepoet uv CLI
Language Python-native Custom pyproject syntax Built-in to uv
Virtual Envs Yes (isolated per session) No (uses current env) Optional via uv venv
CLI Task Syntax nox -s lint poe lint uv run ruff check src
CI Friendly ✅ Matrix + Python version ⚠️ Mostly local convenience ✅ (if you know the commands)
Session Scoping Per-group / per-version Globally defined Command-line scoped
Test Matrix Discovery ✅ (--json)
Modern Lock Support ✅ via uv sync ✅ via uv if integrated

So when to use each?

  • Use poe or uv cli for small, script-oriented workflows and fast local CLI access.
  • Use nox when you:
    • Need multiple Python versions
    • Want true isolation per task
    • Want to share automation across dev + CI reliably

Full Automation: From Code to Container

Once lint and test sessions pass, our CI continues to:

  • Build the image (multi-stage Docker build)
  • Authenticate to Docker Hub
  • Push image tagged by short SHA + latest

Defined here: .github/workflows/ci.yml


Final Thoughts

While new tools like uv and poethepoet offer impressive speed and simplicity, Nox remains an unparalleled orchestration layer in 2025 for teams needing:

  • Python version agility
  • Lockfile-aware sessions
  • Matrix-based CI pipelines

In Terminus, combining Nox with uv, GitHub Actions, and Docker gives us a maintainable and fully automated pipeline — fast, reproducible, and clean.

Codebase

🔗 View Terminus on GitHub

Includes:

  • noxfile.py
  • Dockerfile and docker-compose.yml
  • Full GitHub Actions workflow (ci.yml)
  • Example .env, src/, and tests/ (TODO)