We welcome contributions to Ops! Before you start work on a contribution, please also read CONTRIBUTING.md.
To work in the framework itself you will need Python >= 3.10. Linting, testing,
and docs automation is performed using tox.
First, make sure to install uv, for example:
sudo snap install astral-uv --classicThen install tox with extensions, as well as a range of Python versions:
uv tool install tox --with tox-uv
uv tool update-shellYou can validate that you have a working installation by running:
tox --version
4.26.0 from /home/<your-user>/.local/share/uv/tools/tox/lib/python3.13/site-packages/tox/__init__.py
registered plugins:
tox-uv-1.26.0 at /home/<your-user>/.local/share/uv/tools/tox/lib/python3.13/site-packages/tox_uv/plugin.py with uv==0.7.12For improved performance on the tests, install the library that allows PyYAML to use C speedups:
sudo apt-get install libyaml-devThe following are likely to be useful during development:
# Run linting and unit tests
tox
# Run tests, specifying whole suite or specific files
tox -e unit
tox -e unit -- test/test_charm.py
# Format the code using Ruff
tox -e format
# Generate a local copy of the Sphinx docs in docs/_build
make -C docs html
# Check spelling in the doc source files
make -C docs spelling
# run only tests matching a certain pattern
tox -e unit -- -k <pattern>For more in depth debugging, you can enter the virtualenv so that you can run
pytest or other tools directly:
uv sync --all-groups
source .venv/bin/activate
pytestLikewise, use this virtualenv to enable Python type hints and language server if you use an editor from the console or specify it as interpreter path in an IDE.
The framework has some tests that interact with a real/live Pebble server. To
run these tests, you must have pebble
installed and available in your path. If you have the Go toolchain installed,
you can run go install github.com/canonical/pebble/cmd/pebble@master. This will
install pebble to $GOBIN if it is set or $HOME/go/bin otherwise. Add
$GOBIN to your path (e.g. export PATH=$PATH:$GOBIN or export PATH=$PATH:$HOME/go/bin in your .bashrc) and you are ready to run the real
Pebble tests:
tox -e pebbleTo do this even more manually, you could start the Pebble server yourself:
export PEBBLE=$HOME/pebble
export RUN_REAL_PEBBLE_TESTS=1
pebble run --create-dirs --http=:4000 &>pebble.log &
# Then
tox -e unit -- test/test_real_pebble.py
# or
source .tox/unit/bin/activate
pytest -v test/test_real_pebble.pyWhen making changes to ops, you'll commonly want to try those changes out in
a charm.
If your changes are in a Git branch, you can simply replace your ops version
in requirements.txt (or pyproject.toml) with a reference to the branch, like:
#ops ~= 3.0
git+https://github.com/{your-username}/operator@{your-branch-name}
git is not normally available when charmcraft is packing the charm, so you'll
need to also tell charmcraft that it's required for the build, by adding
something like this to your charmcraft.yaml:
parts:
charm:
build-packages:
- gitIf your changes are only on your local device, you can inject your local ops
into the charm after it has packed, and before you deploy it, by unzipping the
.charm file and replacing the ops folder in the virtualenv. This small
script will handle that for you:
#!/usr/bin/env bash
if [ "$#" -lt 2 ]
then
echo "Inject local copy of Python Operator Framework source into charm"
echo
echo "usage: inject-ops.sh file.charm /path/to/ops/dir" >&2
exit 1
fi
if [ ! -f "$2/framework.py" ]; then
echo "$2/framework.py not found; arg 2 should be path to 'ops' directory"
exit 1
fi
set -ex
mkdir inject-ops-tmp
unzip -q $1 -d inject-ops-tmp
rm -rf inject-ops-tmp/venv/ops
cp -r $2 inject-ops-tmp/venv/ops
cd inject-ops-tmp
zip -q -r ../inject-ops-new.charm .
cd ..
rm -rf inject-ops-tmp
rm $1
mv inject-ops-new.charm $1If your ops change relies on a change in a Juju branch, you'll need to deploy
your charm to a controller using that version of Juju. For example, with microk8s:
- Build Juju and its dependencies
- Run
make microk8s-operator-update - Run
GOBIN=/path/to/your/juju/_build/linux_amd64/bin:$GOBIN /path/to/your/juju bootstrap - Add a model and deploy your charm as normal
We rely on automation to update charm pins of a bunch of charms that use the operator framework. The script can be run locally too.
The documentation uses Canonical styling which is customised on top of the Furo Sphinx theme. The easiest way to pull in Canonical style changes is by using the Canonical documentation starter pack, see docs and repository.
TL;DR:
- Clone the starter pack repository to a local directory:
git clone git@github.com:canonical/sphinx-docs-starter-pack. - Copy the folder
.sphinxunder the starter pack repo to the operator repodocs/.sphinx.
There are two configuration files: docs/conf.py and docs/custom_conf.py, copied and customised from the starter pack repo.
To customise, change the file docs/custom_conf.py only, and theoretically, we should not change docs/conf.py (however, some changes are made to docs/conf.py, such as adding autodoc, PATH, fixing issues, etc.)
The Canonical documentation starter pack uses Make to build the documentation, which will run the script docs/.sphinx/build_requirements.py and generate a requirement file requirements.txt under docs/.sphinx/.
To pull in new dependency changes from the starter pack, change to the starter pack repository directory, and build with the following command. This will create a virtual environment, generate a dependency file, install the software dependencies, and build the documentation:
make htmlThen, compare the generated file .sphinx/requirements.txt and the docs declaration in the dependency-groups section of pyproject.toml and adjust the pyproject.toml file accordingly.
The Python dependencies of ops are kept as minimal as possible, to avoid
bloat and to minimise conflict with the charm's dependencies. The dependencies
are listed in pyproject.toml in the project.dependencies section.
Test environments are managed with tox and executed with pytest, with coverage measured by coverage. Static type checking is done using pyright, and extends the Python 3.10 type hinting support through the typing_extensions package.
Formatting uses Ruff.
All tool configuration is kept in project.toml. The list of
dependencies can be found in the relevant tox.ini environment deps field.
The build backend is setuptools, and the build frontend is build.
As part of the release process, you'll write a summary of the release. The summary appears in the GitHub release notes and in Discourse and Matrix.
In the summary, outline the key improvements from all areas of Ops, including testing, tracing, and the docs. The point here is to encourage people to check out the full notes and to upgrade promptly, so ensure that you entice them with the best that the new versions have to offer.
Avoid using the word "Scenario", preferring "unit testing API" or "state transition testing".
CHANGES.md lists the changes in each release. The changelog is
kept up-to-date by the PR that's created when you run tox -e draft-release
during the release process. You only need to manually edit the changelog if a
commit message needs adjusting (we try to avoid doing this).
There's also a changelog for ops-scenario:
testing/CHANGES.md. Don't add new entries to this file.
We've kept it for historical reference, but we no longer maintain it.
The GitHub release notes include the summary of the release and
the list of changes found in the changelog. A draft release is created when
you run tox -e draft-release duing the release process. You might need to
edit the draft release after a review.
After completing the release process, post to the 'framework' category in Discourse and Charm Development in Matrix.
The Discourse post title should be:
Ops x.y.z released
And the post should resemble this:
The main improvements in this release are ...
Read more in the [full release notes on GitHub](link to the GitHub release).
The Matrix post should be similar.
Before you start, ensure that your environment variable GITHUB_TOKEN is set and that the token has sufficient permissions. The easiest way to set a token is to run gh auth login first, follow the steps to log in, then run export GITHUB_TOKEN=$(gh auth token).
Alternatively, you can also create a personal access token. To do so, go to GitHub -> Settings -> Developer Settings -> Personal access tokens -> Fine-grained tokens, and click "Generate new token" (shortcut: click this link). For "Resource owner", choose "canonical". For "Expiration", choose a desired setting (maximum is 366 days). Under "Repository access", choose "Only select repositories" and select "canonical/operator". Under "Permissions", click "Add permissions", select "Contents" and "Pull requests", then set the access to both of them to "Read and write" (since we need to create draft releases and PRs); note that "Metadata" will be chosen automatically as well. Click "Generate token", then set the environment variable GITHUB_TOKEN with it.
Then, check out the main branch of your forked operator repo and pull upstream to ensure the release automation script is the latest.
-
Draft a release: Run:
tox -e draft-releaseat the root directory of the forked repo.This assumes a draft release on the main branch, and your forked remote name is
origin, and thecanonical/operatorremote name isupstream.If you have different settings, add parameters accordingly. For example, the following command assumes your forked remote name is
mine, andcanonical/operatorremote name isorigin:tox -e draft-release -- --canonical-remote origin --fork-remote mineBy default, the script makes a release on the main branch. If you want to make a release on another branch, for example, on "2.23-maintenance" (you do not need to switch to this branch in your forked repo), run it with the "--branch" parameter:
tox -e draft-release -- --branch 2.23-maintenance -
Follow the steps of the
tox -e draft-releaseoutput. You need to input the release title and an introduction section, which can be multiple paragraphs with empty lines in between. End the introduction section by typing a period sign (.) in a new line, then press enter. -
If drafting the release succeeds, a PR named "chore: update changelog and versions for X.Y.Z release" will be created. Get it reviewed and merged, then wait until the tests pass after merging. It takes around 10 minutes. If the tests don't pass at the tip of the main branch, do not continue.
-
Go to the GitHub releases page, then edit the latest draft release. If you are releasing from the main branch, tick the "set as latest release" box. If you are releasing from a maintenance branch, uncheck the box for "set as latest release". Then, click "Publish release". GitHub will create the additional tag.
Pushing the tags will trigger automatic builds for the Python packages and publish them to PyPI (ops ,ops-scenario, and ops-tracing). Note that it sometimes take a bit of time for the new releases to show up.
See .github/workflows/publish.yaml for details.
You can troubleshoot errors at Actions > Publish.
The Publish workflow includes a job that runs the "SBOM and secscan" workflow.
-
On the summary page of the most recent Publish run, locate the secscan artifacts. There will be two artifacts:
secscan-report-upload-sdistandsecscan-report-upload-wheel.Download both of these, and then upload them to the SSDLC Ops folder in Drive. Open the artifacts and verify that the security scan has not found any vulnerabilities. If you are releasing from the 2.23-maintenance branch, then follow the manual process instead, for both SBOM generation and security scanning.
-
Post release: At the root directory of your forked
canonical/operatorrepo, check out to the main branch to ensure the release automation script is up-to-date, then run:tox -e post-release.This assumes the same defaults as mentioned in step 1.
Add parameters accordingly if your setup differs, for example, if you are releasing from a maintenance branch.
-
Follow the steps of the
tox -e post-releaseoutput. If it succeeds, a PR named "chore: adjust versions after release" will be created. Get it reviewed and merged.
If the release automation script fails, delete the draft release and the newly created branches (release-prep-*, post-release-*) both locally and in the origin, fix issues, and retry.
The Charmcraft kubernetes and machine profiles specify a minimum Ops version in their pyproject.toml templates. If an Ops release includes a major new feature or resolves a dependency issue, open a PR to Charmcraft to increase the minimum Ops version in the profiles and refresh the uv.lock templates.
-
Fork the Charmcraft repo and create a branch for local development. In your branch, run
make setupto create a virtual environment, then runsource .venv/bin/activate.See also: Charmcraft's contributing guide
-
In your Charmcraft development branch, modify the Ops version specifier in:
charmcraft/templates/init-kubernetes/pyproject.toml.j2charmcraft/templates/init-machine/pyproject.toml.j2
-
Clone charmcraft-profile-tools locally.
-
In your clone of charmcraft-profile-tools, run:
CHARMCRAFT_DIR=/path/to/charmcraft just initThis initialises a Kubernetes charm and a machine charm based on your Charmcraft development branch.
If you don't have just installed, use
uvx --from rust-just justinstead. -
Lock the dependencies of the charms and generate
uv.lock.j2files:just lock -
Copy the
uv.lock.j2files to your Charmcraft development branch:cp .templates/init-kubernetes/uv.lock.j2 /path/to/charmcraft/charmcraft/templates/init-kubernetes cp .templates/init-machine/uv.lock.j2 /path/to/charmcraft/charmcraft/templates/init-machine -
In your Charmcraft development branch, commit your changes.
You should have changed these files:
- charmcraft/templates/init-kubernetes/pyproject.toml.j2
- charmcraft/templates/init-kubernetes/uv.lock.j2
- charmcraft/templates/init-machine/pyproject.toml.j2
- charmcraft/templates/init-machine/uv.lock.j2