Participating

TLDR

EventWhat to do
Want to contributeOpen a PR
Found a bugFile a ticket on Github Issues
Found a vulnerabilityReport it privately
Feature requestDescribe what you want on Github Discussions
Want to start a discussionStart one on Github Discussions
Be aware of the ongoing developmentTake 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:

BranchRole
rollingdevelopment tip — all new work lands here first
jazzyROS 2 Jazzy line, receives rolling backports until its EOL (05/2029)
lyricalROS 2 Lyrical line, receives rolling backports until its EOL (05/2031)
humblelegacy 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...