Skip to main content

A small, opinionated uv-native Python monorepo framework.

Project description

Joist

A tiny, opinionated Python monorepo framework built around uv.

Joist borrows a few proven ideas from Nx and Lerna without trying to become their Python clone:

  • a project graph built from workspace packages
  • named targets such as test, lint, and build
  • dependency-aware task ordering
  • affected-project runs based on git diff
  • a small local task cache
  • fixed-version releases for public packages

Quickstart

uvx joist init demo
cd demo
uvx joist new lib core
uvx joist new app api --depends-on core
uvx joist graph
uvx joist run test --all
uvx joist affected test --base main
uvx joist version patch --dry-run

This creates a uv workspace with one library, one app, an editable internal workspace dependency from api to core, and graph-aware Joist targets.

If Joist is installed in the current environment, the console command is available directly:

joist list
joist affected test --base origin/main --head HEAD

Python support

Joist supports Python 3.11 through 3.14. The minimum is Python 3.11 because Joist uses the standard-library tomllib module for TOML parsing.

Opinionated layout

joist init creates this shape:

.
|-- apps/
|-- packages/
|-- joist.toml
`-- pyproject.toml

Projects live in apps/* or packages/*, each with its own pyproject.toml. The root pyproject.toml owns the uv workspace; Joist treats that as the package source of truth whenever possible. Joist uses uv workspace membership to discover packages, uv workspace sources to keep internal dependencies editable, and uv commands to sync, test, build, and publish. Joist only adds graph-aware task orchestration on top:

[tool.uv]
package = false

[tool.uv.workspace]
members = ["packages/*", "apps/*"]

Configuration

joist.toml defines project globs and target defaults:

[workspace]
projects = ["packages/*", "apps/*"]
default_base = "main"
cache_dir = ".joist/cache"

[target_defaults.test]
command = "uv run --package {package_name} pytest {project_root}/tests"
cache = true
inputs = ["{project_root}/src/**/*.py", "{project_root}/tests/**/*.py", "{project_root}/pyproject.toml", "pyproject.toml", "uv.lock"]

[target_defaults.build]
command = "uv build --package {package_name}"
cache = false
depends_on = ["^build"]

Target command templates support:

  • {workspace_root}
  • {project_root}
  • {project_name}
  • {package_name}

Commands are split with Python's shlex and executed without a shell. Use an explicit shell command such as sh -c "..." only when shell syntax is required.

Each project can override or add targets in its own pyproject.toml:

[tool.joist]
name = "api"
type = "app"
depends_on = ["core"]

[tool.joist.targets.serve]
command = "uv run --package api python -m api"
cache = false

Internal dependencies are inferred from [project].dependencies when a dependency name matches another workspace package. In uv, workspace-member dependencies should also be declared with tool.uv.sources:

[project]
dependencies = ["core==0.1.0"]

[tool.uv.sources]
core = { workspace = true }

Explicit depends_on is available for task-ordering edges that are not package dependencies.

Affected run flow

joist affected follows the same broad shape as Nx affected runs: Git decides which files changed, Joist maps those files onto workspace projects, then the project graph pulls in dependents that also need validation.

flowchart TD
    A["git diff chooses changed files"] --> B["Map files to project roots"]
    B --> C{"Root coordination file changed?"}
    C -- "yes" --> D["Select every project"]
    C -- "no" --> E["Select directly changed projects"]
    E --> F["Add reverse dependents"]
    D --> G["Topologically sort selected projects"]
    F --> G
    G --> H["Expand target pipelines, such as ^build"]
    H --> I{"Cache hit?"}
    I -- "yes" --> J["Replay cached output"]
    I -- "no" --> K["Run rendered uv command"]
    K --> L["Record successful output in local cache"]

Built-in root coordination files are joist.toml, root pyproject.toml, and uv.lock. Add repo-specific shared files with workspace.affects_all:

[workspace]
affects_all = [
  ".github/workflows/**",
  "Makefile",
  "requirements*.txt",
  "ruff.toml",
  "scripts/**",
]

Commands

uv run joist list
uv run joist graph --format dot
uv run joist run build api
uv run joist run lint --all --dry-run
uv run joist affected --list --base origin/main --head HEAD --json
uv run joist affected --list api --base origin/main --head HEAD
uv run joist affected test --base origin/main --head HEAD
uv run joist cache clear
uv run joist version minor

With --list, project names filter the affected set instead of naming a target.

depends_on = ["^build"] means "run the build target for dependency projects before this project." This is the one Nx-style target pipeline rule Joist supports.

The cache is deliberately simple. It hashes configured input files, the rendered command, and extra CLI args, then replays cached terminal output for successful runs. It does not implement remote cache or artifact restoration, so generated build targets are intentionally uncached by default.

Build system integration

Treat joist.toml targets as the contract between the monorepo and your build system. Keep the target commands uv-native, then have CI or local automation call Joist to decide which projects need each target.

For pull requests, fetch enough Git history for the base comparison and run only affected targets:

name: ci

on:
  pull_request:

jobs:
  affected:
    runs-on: ubuntu-latest
    env:
      BASE_REF: ${{ github.base_ref }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
        with:
          fetch-depth: 0
      - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
      - run: uv sync --locked
      - run: uv run joist affected --list --base "origin/${BASE_REF}" --head HEAD --json
      - run: uv run joist affected lint --base "origin/${BASE_REF}" --head HEAD
      - run: uv run joist affected test --base "origin/${BASE_REF}" --head HEAD
      - run: uv run joist affected build --base "origin/${BASE_REF}" --head HEAD

For main-branch validation or nightly builds, run the complete target set:

name: full-build

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
      - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
      - run: uv sync --locked
      - run: uv run joist run lint --all
      - run: uv run joist run test --all
      - run: uv run joist run build --all

For Make-based workflows, delegate the selection logic to Joist instead of duplicating package lists:

.PHONY: lint test build affected-test clean-cache

lint:
	uv run joist run lint --all

test:
	uv run joist run test --all

build:
	uv run joist run build --all

affected-test:
	uv run joist affected test --base origin/main

clean-cache:
	uv run joist cache clear

For release pipelines, keep versioning explicit and build every public package after the bump:

uv sync --locked
uv run joist version patch
uv lock
uv run joist run build --all --no-cache

Before uploading, run the local PyPI readiness checks:

uv run pytest
uv run ruff check .
uv build
uv run twine check dist/*
uv publish --dry-run --trusted-publishing never

Joist does not publish packages for you. Keep publishing as a separate, auditable step using the uv command and credentials your release system already controls.

For public PyPI publishing, verify the distribution name immediately before the first upload.

Release model

Joist uses one Lerna-inspired release mode: fixed versions. joist version patch bumps every non-private workspace project to the same version and writes a root VERSION file. Internal dependency pins that point at public workspace packages are updated too, including pins in private projects.

Mark private projects with:

[tool.joist]
private = true

Design boundaries

Joist is intentionally not a plugin ecosystem, not a remote cache service, and not a Python package publisher. uv owns workspace membership, locking, syncing, editable internal dependencies, building, and publishing. Joist is a small task orchestration layer over uv, git, and project-local commands.

License

Joist is licensed under the Universal Permissive License 1.0 (UPL-1.0).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

joist-0.2.0.tar.gz (17.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

joist-0.2.0-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

File details

Details for the file joist-0.2.0.tar.gz.

File metadata

  • Download URL: joist-0.2.0.tar.gz
  • Upload date:
  • Size: 17.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for joist-0.2.0.tar.gz
Algorithm Hash digest
SHA256 7a05a04bfdb505cb4a1895524e2d0fae7899bb7541c9bcf01df923293af571e8
MD5 055d27abee926a7dd298d9da2f1592c6
BLAKE2b-256 9a0e55be1dfdb3b717e1508146280fc04b29ab2b8b375753a2d0719c882341a8

See more details on using hashes here.

File details

Details for the file joist-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: joist-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 21.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for joist-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f14c9647ffa2647f301f405c0128ddb7b4a05a483913305a7f01279375f0f579
MD5 60fde238656a5bdbaef55bcb068bea2e
BLAKE2b-256 2b760fa995bb72c1bed4e66f2196beec89f529fc8a33cd57f8ad516d6a2248cf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page