Participating
TLDR
| Event | What to do |
|---|---|
| Want to contribute | Open a PR |
| Found a bug | File a ticket on Github Issues |
| Found a vulnerability | Report it privately |
| Feature request | Describe what you want on Github Discussions |
| Want to start a discussion | Start one on Github Discussions |
| Be aware of the ongoing development | Take a look at the Github Project and what is being worked on |
Contributing
Feature requests
Since I want DC to be community driven, go to Github discussions, start a discussion about a features you want to see and users will be able to vote for your it. Most requested features will have more attention than others.
Found a bug?
If you find a problem, first search if an issue already exists. If a related issue doesn't exist, you can open a new issue using the issue form.
Found a vulnerability?
Do not open a public issue, discussion or pull request. Report it through GitHub private vulnerability reporting instead; the security policy states the supported branches, the response targets and what is in scope.
General guidelines
You can contribute to the source code with Pull Requests, for example:
- To fix a typo you found on the documentation.
- To propose new documentation sections.
- To fix an existing issue/bug.
- Make sure to add tests.
- To add a new feature.
- Make sure to add tests.
- Make sure to add documentation if it's relevant.
Branches and backports
Development happens on rolling, the repository's default branch: open every PR against
rolling. One branch per ROS 2 distro keeps the release lines:
| Branch | Role |
|---|---|
rolling | development tip — all new work lands here first |
jazzy | ROS 2 Jazzy line, receives rolling backports until its EOL (05/2029) |
lyrical | ROS 2 Lyrical line, receives rolling backports until its EOL (05/2031) |
humble | legacy line (DC 1.x, embedded Fluent Bit), maintained on its own branch, no backports |
To have a merged change land on a distro branch too, label the PR backport:jazzy and/or
backport:lyrical before merging. Small mechanical fixes port cleanly; restructuring
changes usually don't — keep those rolling-only. When the PR merges, the Backport workflow
cherry-picks the PR's commits onto each labeled branch and opens a PR there, linking back
to the original. A cherry-pick that conflicts still opens a PR, with the conflict markers
left in the files and the conflicting files listed in the PR body: resolve them there.
Backport PRs created by the workflow don't start CI on their own — GitHub doesn't run workflows for PRs created with the workflow's own token. Close and reopen one to trigger its checks.
Preview what a merge would backport, creating nothing:
./tools/ci/backport.sh <pr-number> --dry-run
# or the same plan through the workflow:
gh workflow run backport.yml -f pr_number=<pr-number>
Setup environment
ROS
Follow the steps to build your workspace and install dependencies in the setup section
Then install the git hook. .pre-commit-config.yaml is run by
prek, a single-binary reimplementation of pre-commit:
uv tool install prek # or: curl -LsSf https://prek.j178.dev/install.sh | sh
prek install
You are now ready to write some code, commit and follow the standards with the git hook. To run every hook over the whole tree the way CI does:
prek run --all-files --skip build-doc # drop the --skip to check the docs build too
License headers
The repo follows REUSE: every file declares its copyright and
license, and the reuse hook fails if one doesn't. Sources say so in a header:
// SPDX-FileCopyrightText: 2022-2026 David Bensoussan
// SPDX-License-Identifier: MPL-2.0
reuse annotate writes it for you, in whatever comment syntax the file uses:
uvx reuse annotate --copyright "2022-2026 David Bensoussan" --license MPL-2.0 path/to/new_file.cpp
uvx reuse lint # what the hook runs
Files that can't carry a comment — images, meshes, fonts — are covered by path in
REUSE.toml, which is also where third-party assets declare their own upstream license.
Add a new license there and run uvx reuse download --all to fetch its text into
LICENSES/.
Docs
The doc toolchain (mdbook plus its preprocessors — mdbook-admonish, mdbook-mermaid,
mdbook-open-on-gh — and mdbook-linkcheck, all pinned) runs inside a Podman image built
from containers/doc/Containerfile; mdbook's preprocessor ABI isn't stable across minor
versions, so an unpinned cargo install mdbook can silently pull an incompatible set. The
same two scripts CI, the build-doc pre-commit hook, and a local editing loop all use:
./tools/ci/pre-commit/build_doc.sh # one-shot build -> doc/book/html
./tools/ci/pre-commit/serve_doc.sh # live-reloading dev server at http://127.0.0.1:3000
Open the doc folder of the repository and edit the Markdown files you need — serve_doc.sh
reloads the browser on every save. Editing an ADR under docs/adr/ needs one re-run of
tools/ci/pre-commit/generate_adr_pages.py first, since that's what mirrors it into
doc/src/dc/adr/.
Declaring plugin parameters
Measurement and Condition plugins (dc_measurements/plugins/{measurements,conditions}/)
declare their own parameters in onConfigure(). Always do this through
dc_util::get_*_type_param() (dc_util/include/dc_util/node_utils.hpp) — never call
declare_parameter or nav2_util::declare_parameter_if_not_declared directly. One call
both declares and reads the value, and exits with a clear RCLCPP_FATAL if it can't be
retrieved, instead of a hand-rolled declare/get/try-catch block per parameter:
// Mandatory (no default; fatal if not overridden):
cam_name_ = dc_util::get_str_type_param(node, measurement_name_, "cam_name");
// Optional, with a default:
polling_interval_ = dc_util::get_int_type_param(node, measurement_name_, "polling_interval", 1000);
plugin_name/measurement_name_/condition_name_ is the namespace prefix — the helper
declares and reads "<plugin_name>.<param_name>". Available types: str, str_array,
bool, bool_array (mandatory only), int, int_array (mandatory only), double,
double_array (mandatory only). measurement_server.cpp/group_server.py-level
parameters that have no plugin namespace use the equivalent dc_util::get_str_param() /
get_str_array_param() (no plugin_name argument).
This single-source-of-truth convention is deliberate: see
ADR-0008
for why nav2_util stays a dependency and dc_util wraps it rather than replacing it.
dc_group (Python) has no plugins and no equivalent wrapper — group_server.py declares
each of its parameters exactly once via plain self.declare_parameter(...), which is
sufficient there (see the ADR).
Tests
TODO...