Set up a charm monorepo
Sets up a charm monorepo following the patterns established by charmed-hpc/slurm-charms. The repository uses uv workspaces for dependency management, a scripts/repository.py CLI tool for build/test/lint orchestration, and a justfile (conforming to UHPC011) as the developer-facing entry point.
repository.py must never be called directly by developers. All interaction goes through the justfile, which delegates to repository.py via uv run.
Process
- Read the repository -- Examine the existing directory structure. Identify charm directories (e.g.
charms/), internal packages (internal/), public packages (pkg/), shared integration tests (tests/integration/), and configuration files (pyproject.toml,uv.lock). - Create
scripts/repository.py-- Place the monorepo CLI tool atscripts/repository.py. Follow the structure and subcommands described in therepository.pyspecification below. Adapt paths and tool configuration to match the target repository. - Create the justfile -- Create a UHPC011-compliant justfile at the repository root that delegates to
scripts/repository.py. Follow the justfile specification below. - Configure
pyproject.toml-- Ensure the rootpyproject.tomlhas the necessary[tool.repository]section for external libraries and binary packages, plus tool configuration forruff,black,codespell,pyright,coverage, andpytest. - Verify -- Run
just helpto confirm the justfile parses. Runjust test unitandjust lintto confirm that the justfile correctly delegates toscripts/repository.py. Fix any issues.
Repository structure
The target repository layout should follow this pattern:
.
├── justfile # UHPC011-compliant, delegates to scripts/repository.py
├── pyproject.toml # Root project config (deps, uv workspace, tool configs)
├── uv.lock
├── scripts/
│ └── repository.py # Monorepo CLI tool (never called directly)
├── charms/ # Individual Juju charms (uv workspace members)
│ └── <charm-name>/
│ ├── charmcraft.yaml
│ ├── pyproject.toml
│ ├── src/
│ ├── tests/unit/
│ └── terraform/
├── internal/ # Private packages (uv workspace members)
│ └── <package-name>/
├── pkg/ # Public packages (uv workspace members)
│ └── <package-name>/
└── tests/
└── integration/ # Shared cross-charm integration tests
scripts/repository.py specification
The repository.py file is a Python CLI tool that orchestrates all build, test, lint, and formatting operations across the monorepo. It must not be executed directly by developers; the justfile is the only entry point.
Required subcommands
| Subcommand | Description |
|---|---|
fmt |
Apply formatting standards. Runs black and ruff check --fix. |
lint |
Check code style. Runs codespell and ruff check. |
typecheck |
Stage charms, then run pyright against charm and package source. |
unit |
Stage charms, then run pytest with coverage for each charm and package. |
integration |
Build all charms, then run pytest against tests/integration/. |
stage |
Copy charm source, libraries, and requirements into _build/. |
build |
Stage charms, then run charmcraft pack for each charm. |
clean |
Remove the _build/ directory and all build artifacts. |
All subcommands accept optional positional target arguments (charm or package names). When no targets are given, the command operates on all charms and packages.
Key design patterns
BuildToolwrapper -- A dataclass that locates a binary onPATHviashutil.whichand provides arun_commandmethod that streams stdout/stderr in real time using threads.Repositoryclass -- Loads monorepo metadata frompyproject.tomlanduv.lockon initialization. Discovers charms, internal/external libraries, and packages. Resolves binary package versions from the lock file.CharmandPackagedataclasses -- Hold metadata, paths, library dependencies, and package dependencies for each charm/package.CharmLibrarydataclass -- Represents a charm library with charm name, library name, and major/minor version. Handles conversion to/fromcharmcraft.yamlformat.- Staging -- Before running unit tests or type checks, charms are staged into
_build/<charm-name>/with their libraries copied in,charmcraft.yamlrewritten (to injectcharm-binary-python-packages), andrequirements.txtexported viauv export. - Coverage -- Unit tests run under
coverage runper charm/package, then results are combined into a single report withcoverage combine,coverage report, andcoverage xml. - uv runner -- Commands are run through
uv run --frozen --extra devto use the development virtual environment.
Path constants
ROOT_DIR = Path(__file__).parent.parent.resolve() # repository root (one level up from scripts/)
BUILD_PATH = ROOT_DIR / "_build"
CHARMS_PATH = ROOT_DIR / "charms"
PUBLIC_PKGS_PATH = ROOT_DIR / "pkg"
PRIVATE_PKGS_PATH = ROOT_DIR / "internal"
LIBS_CHARM_PATH = BUILD_PATH / "libs"
Note: since repository.py lives in scripts/, ROOT_DIR must resolve to the parent of scripts/ (i.e. the repository root).
Configuration in pyproject.toml
The repository.py script reads configuration from the root pyproject.toml:
[tool.repository]
external-libraries = [
{ lib = "<charm>.<library>", version = "<major>.<minor>" },
]
binary-packages = [
"<package-name>",
]
Per-charm pyproject.toml files may declare library dependencies:
[tool.repository]
libraries = [
"<charm>.<library>",
]
Justfile specification
The justfile must conform to UHPC011 (Common justfile format for Charmed HPC repositories). It delegates all substantial work to scripts/repository.py via uv run.
Required variables and settings
uv := require("uv")
export PY_COLORS := "1"
export PYTHONBREAKPOINT := "pdb.set_trace"
uv_run := "uv run --frozen --extra dev"
Required recipes
All UHPC011 required recipes must be present:
[private]
default:
@just help
# Show available recipes
help:
@just --list --unsorted
# Prepare the local environment
setup: env
# Clean project directory
clean:
{{uv_run}} scripts/repository.py clean
# Apply static checks
check: fmt lint typecheck
# Run tests for specified targets, or all tests if none specified
test *targets:
#!/usr/bin/env bash
if [ "{{targets}}" = "" ]; then
just test-all
exit 0
fi
for target in {{targets}}; do
if just --show $target > /dev/null 2>&1; then
echo "Running $target tests..."
just $target
else
echo "$target tests not found, skipping."
exit 1
fi
done
Recommended recipes (delegating to scripts/repository.py)
# Run all test suites
test-all: unit integration
# Run unit tests
unit *args: lock
{{uv_run}} scripts/repository.py unit {{args}}
# Run integration tests
integration *args: lock
{{uv_run}} scripts/repository.py integration {{args}}
# Apply formatting standards
fmt: lock
{{uv_run}} scripts/repository.py fmt
# Check files against style standards
lint: lock
{{uv_run}} scripts/repository.py lint
# Perform type checking
typecheck: lock
{{uv_run}} scripts/repository.py typecheck
# Build specified charms, or all charms if none specified
build *args: lock
{{uv_run}} scripts/repository.py build {{args}}
# Regenerate uv.lock
lock:
uv lock
# Create a uv development environment
env: lock
uv sync --extra dev
# Upgrade uv.lock with the latest dependencies
upgrade:
uv lock --upgrade
License header
The justfile must include the Apache 2.0 license header at the top. Update the year to the current year:
# Copyright 2025 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Verification
After setting up both files, verify that the justfile correctly delegates to scripts/repository.py:
- Parse check -- Run
just --listand confirm all required and recommended recipes appear without errors. - Unit tests -- Run
just test unitand confirm it dispatches to theunitrecipe, which callsscripts/repository.py unit. - Lint -- Run
just lintand confirm it callsscripts/repository.py lint. - Format -- Run
just fmtand confirm it callsscripts/repository.py fmt. - Type check -- Run
just typecheckand confirm it callsscripts/repository.py typecheck. - Clean -- Run
just cleanand confirm it callsscripts/repository.py clean. - Test dispatch -- Run
just test unit integrationand confirm both theunitandintegrationrecipes are invoked sequentially. - Test fallback -- Run
just testwith no arguments and confirm it delegates totest-all, which runs all test suites.
If any command fails, check that:
scripts/repository.pyis importable (no syntax errors, correct Python version).- The
uvdevelopment environment is set up (just setup). - The
pyproject.tomlhas the correct[tool.repository]configuration. - Charm directories exist under
charms/with validpyproject.tomlandcharmcraft.yamlfiles.
Constraints
- DO NOT allow
scripts/repository.pyto be called directly. It must always be invoked through the justfile viauv run. - DO NOT place
repository.pyat the repository root. It must live inscripts/. - DO NOT omit any UHPC011 required recipe from the justfile.
- DO NOT rename required or recommended recipes. Use the exact names specified by UHPC011.
- DO NOT use recipe groups in the justfile. All recipes must be ungrouped.
- DO NOT change the implementation of the
defaultrecipe. It must always be@just help. - DO NOT change the signature of the
testrecipe. It must accept*targets. - DO ensure the
defaultrecipe has the[private]attribute. - DO ensure the justfile is compatible with
justversion 1.48.1. - DO use
require()for any external tools the recipes depend on (e.g.uv). - DO include the Apache 2.0 license header at the top of the justfile.
- DO include a comment above each recipe describing what it does.
- DO set
ROOT_DIRinrepository.pytoPath(__file__).parent.parent.resolve()to account for thescripts/subdirectory.