roschema 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {roschema-0.2.0 → roschema-0.2.1}/CHANGELOG.md +13 -0
- roschema-0.2.1/CONTRIBUTING.md +56 -0
- roschema-0.2.1/PKG-INFO +475 -0
- roschema-0.2.1/README.md +441 -0
- {roschema-0.2.0 → roschema-0.2.1}/action/action.yml +1 -1
- {roschema-0.2.0 → roschema-0.2.1}/docs/rules.md +85 -0
- {roschema-0.2.0 → roschema-0.2.1}/pyproject.toml +1 -1
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/gendocs.py +48 -15
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl001_field_snake_case.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl002_constant_upper_snake.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl003_name_pascal_case.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl004_missing_doc.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl005_discouraged_types.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl006_unbounded.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/rsl010_srv_shadow.py +2 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/model.py +3 -0
- roschema-0.2.0/PKG-INFO +0 -354
- roschema-0.2.0/README.md +0 -320
- {roschema-0.2.0 → roschema-0.2.1}/.github/workflows/ci.yml +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/.github/workflows/release.yml +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/.gitignore +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/LICENSE +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/cli.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/config.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/deep.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/differ.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/discovery/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/discovery/discover.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/discovery/model.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/discovery/resolve.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/formatter.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/freeze.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/ir.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/loader.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/mcap_fs.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/parser/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/parser/core.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/parser/errors.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/py.typed +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/report/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/report/github.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/report/json_report.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/report/summary.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/report/text.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs001_field_removed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs002_field_added.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs003_field_type_changed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs004_fields_reordered.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs005_field_renamed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs006_array_kind_changed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs007_bound_tightened.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs008_bound_loosened.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs020_constant_removed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs021_constant_value_changed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs022_constant_type_changed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs030_default_changed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs040_interface_removed.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs060_message_version.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/breaking/rs110_transitive.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/__init__.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/lint/_common.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/rules/registry.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/src/roschema/vfs.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/_helpers.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/conftest.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/AMENT_IGNORE +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/COLCON_IGNORE +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/golden_check.json +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/action/Move.action +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/package.xml +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_new/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/action/Move.action +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Removed.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/package.xml +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/fixtures/ws_old/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_action.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_check_cli.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_cli_ls.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_cli_version.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_config.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_deep.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_differ.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_discovery.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_fmt.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_freeze.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_gendocs.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_golden.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_lint.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_mcap.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_parser.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_report_formats.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_rules.py +0 -0
- {roschema-0.2.0 → roschema-0.2.1}/tests/test_vfs.py +0 -0
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
The format follows [Keep a Changelog](https://keepachangelog.com/), and the
|
|
4
4
|
project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.2.1] - 2026-11-03
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `docs/rules.md` opens with a one-line overview of every rule, and lint rules
|
|
11
|
+
show flagged and preferred examples.
|
|
12
|
+
- `CONTRIBUTING.md` with the development checks and release process.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- README restructured for new users: levels and severities, target resolution,
|
|
17
|
+
suppression syntax, exit codes, Action inputs and troubleshooting.
|
|
18
|
+
|
|
6
19
|
## [0.2.0] - 2026-11-03
|
|
7
20
|
|
|
8
21
|
### Added
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
roschema uses [uv](https://docs.astral.sh/uv/):
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv sync --extra dev --extra bag
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Checks
|
|
12
|
+
|
|
13
|
+
CI runs these on every pull request; run them before pushing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run pytest
|
|
17
|
+
uv run ruff check .
|
|
18
|
+
uv run ruff format --check .
|
|
19
|
+
uv run mypy --strict src
|
|
20
|
+
uv run python -m roschema.rules.gendocs --check
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The tests do not need ROS and work in a shell with a ROS environment sourced:
|
|
24
|
+
`pyproject.toml` disables the ROS launch pytest plugins, which cannot be
|
|
25
|
+
imported in the roschema virtualenv.
|
|
26
|
+
|
|
27
|
+
Every fix or feature should come with a test that fails without it.
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
Each rule lives in its own module under `src/roschema/rules/breaking/` (check
|
|
32
|
+
rules, `RS***`) or `src/roschema/rules/lint/` (lint rules, `RSL***`) and is
|
|
33
|
+
registered in that package's `__init__.py`. Rule IDs are permanent: never
|
|
34
|
+
renumber or reuse one.
|
|
35
|
+
|
|
36
|
+
`docs/rules.md` is generated from the rule metadata. After changing a rule,
|
|
37
|
+
regenerate it:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv run python -m roschema.rules.gendocs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Releases
|
|
44
|
+
|
|
45
|
+
1. Bump `version` in `pyproject.toml` and turn the `[Unreleased]` section of
|
|
46
|
+
`CHANGELOG.md` into a dated `[X.Y.Z]` section.
|
|
47
|
+
2. Merge to `main`.
|
|
48
|
+
3. Tag the merge commit and push the tag:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git tag -a vX.Y.Z -m "Release X.Y.Z"
|
|
52
|
+
git push origin vX.Y.Z
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The release workflow then checks that the tag matches the package version, runs
|
|
56
|
+
the checks, builds the sdist and wheel, and publishes them to PyPI.
|
roschema-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: roschema
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Breaking-change detection, linting and formatting for ROS 2 interface definitions
|
|
5
|
+
Project-URL: Homepage, https://github.com/justagist/roschema
|
|
6
|
+
Project-URL: Documentation, https://github.com/justagist/roschema#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/justagist/roschema
|
|
8
|
+
Project-URL: Issues, https://github.com/justagist/roschema/issues
|
|
9
|
+
Author-email: Saif Sidhik <saif.sidhik@helloleap.ai>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: action,breaking-change,interface,lint,msg,ros,ros2,srv
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: rich>=13.0
|
|
24
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
25
|
+
Requires-Dist: typer>=0.12
|
|
26
|
+
Provides-Extra: bag
|
|
27
|
+
Requires-Dist: mcap>=1.1; extra == 'bag'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
32
|
+
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# roschema
|
|
36
|
+
|
|
37
|
+
[](https://github.com/justagist/roschema/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/roschema/)
|
|
39
|
+
[](https://pypi.org/project/roschema/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
[](https://github.com/astral-sh/ruff)
|
|
42
|
+
[](https://mypy-lang.org/)
|
|
43
|
+
|
|
44
|
+
Catch breaking ROS 2 interface changes in the PR, not on the robot.
|
|
45
|
+
|
|
46
|
+
`roschema` detects breaking changes, lints and formats ROS 2 interface
|
|
47
|
+
definitions (`.msg`, `.srv`, `.action`) with zero ROS dependencies, so it runs
|
|
48
|
+
in a bare CI container.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
From the root of your workspace or repository, compare the working tree against
|
|
55
|
+
the base branch (needs [`uv`](https://docs.astral.sh/uv/); nothing else to
|
|
56
|
+
install):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uvx roschema check --against origin/main
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
roschema finds every interface package below the current directory, compares
|
|
63
|
+
it with the same packages at `origin/main`, and lists each breaking change with
|
|
64
|
+
its rule ID, location and a suggested semver bump. It exits `1` when an error is
|
|
65
|
+
found, so it works as a CI gate as is; to run it on every pull request, use the
|
|
66
|
+
[GitHub Action](#github-action).
|
|
67
|
+
|
|
68
|
+
Next steps:
|
|
69
|
+
|
|
70
|
+
- [What gets reported](#what-gets-reported) explains levels and severities.
|
|
71
|
+
- The [rule reference](docs/rules.md) lists every check (`RS***`) and lint
|
|
72
|
+
(`RSL***`) rule, starting with a one-line overview of each.
|
|
73
|
+
- The [roschema-demo](https://github.com/justagist/roschema-demo) repo has a
|
|
74
|
+
`robot_msgs` package with a passing and a failing branch to compare, plus a
|
|
75
|
+
ready-to-copy workflow.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
Requires Python 3.10+; a ROS installation is **not** needed.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv tool install roschema # or: pipx install roschema
|
|
83
|
+
uv tool install 'roschema[bag]' # with MCAP bag support, for bag-check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Inside a virtualenv, `pip install roschema` works too. Outside one, Ubuntu 24.04
|
|
87
|
+
(the Jazzy platform) refuses `pip install` into the system Python
|
|
88
|
+
("externally-managed-environment", PEP 668), so prefer `uv tool` or `pipx`.
|
|
89
|
+
|
|
90
|
+
To run it without installing anything, use `uvx roschema ...` as in the
|
|
91
|
+
quickstart. The [GitHub Action](#github-action) needs no installation either.
|
|
92
|
+
|
|
93
|
+
## What gets reported
|
|
94
|
+
|
|
95
|
+
CDR, the ROS 2 wire format, is positional: fields are serialised in declaration
|
|
96
|
+
order with no field tags. Adding, removing, reordering or retyping a field
|
|
97
|
+
therefore breaks the wire format even when your code still compiles, so
|
|
98
|
+
roschema compares interfaces structurally rather than by text.
|
|
99
|
+
|
|
100
|
+
Every rule belongs to one or more **compatibility levels**. `--level` picks a
|
|
101
|
+
cumulative bundle of levels to report; the default is `source`.
|
|
102
|
+
|
|
103
|
+
| Level | Reports | Example |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `wire` | CDR byte-stream compatibility only | a field added (RS002) |
|
|
106
|
+
| `source` | wire + generated C++/Python API | a constant removed (RS020) |
|
|
107
|
+
| `introspection` | source + name-based tooling (bags, echo, YAML) | a field renamed (RS005) |
|
|
108
|
+
| `behavior` | introspection + semantics (defaults, constant values) | a default changed (RS030) |
|
|
109
|
+
|
|
110
|
+
Levels decide **what is reported**; each rule's **severity** (`error`, `warn`
|
|
111
|
+
or `off`) decides whether a reported finding fails the run. Only errors fail;
|
|
112
|
+
warnings are printed but exit `0`. Rules that are off by default (such as RS060)
|
|
113
|
+
are reported at every level once you enable them.
|
|
114
|
+
|
|
115
|
+
The [rule overview](docs/rules.md#overview) shows each rule's levels and default
|
|
116
|
+
severity in one table.
|
|
117
|
+
|
|
118
|
+
## Common usage
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# breaking-change gate in a PR (compare against the base branch)
|
|
122
|
+
roschema check --against origin/main
|
|
123
|
+
|
|
124
|
+
# check before pushing, against the last commit
|
|
125
|
+
roschema check --against HEAD
|
|
126
|
+
|
|
127
|
+
# scope to one package, or a subtree with a glob (and exclude vendored packages)
|
|
128
|
+
roschema check src/my_msgs --against origin/main
|
|
129
|
+
roschema check 'src/**' --against origin/main --exclude 'vendor_*'
|
|
130
|
+
|
|
131
|
+
# only fail on wire-level breaks (loosest gate)
|
|
132
|
+
roschema check --against origin/main --level wire
|
|
133
|
+
|
|
134
|
+
# machine-readable output for tooling
|
|
135
|
+
roschema check --against origin/main --format json
|
|
136
|
+
|
|
137
|
+
# style/naming check, and a formatting gate for CI
|
|
138
|
+
roschema lint
|
|
139
|
+
roschema fmt --check
|
|
140
|
+
|
|
141
|
+
# pin a release baseline, then check against it later (no git needed)
|
|
142
|
+
roschema freeze -o roschema.lock
|
|
143
|
+
roschema check --against roschema.lock
|
|
144
|
+
|
|
145
|
+
# is this recorded bag still compatible with the installed interfaces?
|
|
146
|
+
roschema bag-check recording.mcap
|
|
147
|
+
|
|
148
|
+
# what would be checked? (discovered packages and interfaces)
|
|
149
|
+
roschema ls
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Checking
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
roschema check [TARGET] --against BASELINE [--level LEVEL] [--format text|json|github]
|
|
156
|
+
[--deep | --no-deep] [--exit-zero] [--config PATH] [--exclude GLOB]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`TARGET` selects what to check; omit it to check every interface package below
|
|
160
|
+
the current directory.
|
|
161
|
+
|
|
162
|
+
- **A path** checks the packages below that directory.
|
|
163
|
+
- **A package name** (its `<name>` in `package.xml`) checks that package's
|
|
164
|
+
source below the current directory. If there is no such source package, the
|
|
165
|
+
installed copy from the ament index (`AMENT_PREFIX_PATH`) is used, with a
|
|
166
|
+
warning.
|
|
167
|
+
- **A glob** such as `'src/**'` checks every package below the matching
|
|
168
|
+
directories, including packages that exist only in a Git or directory
|
|
169
|
+
baseline (so deleting a package is reported).
|
|
170
|
+
|
|
171
|
+
Discovery skips trees marked with `COLCON_IGNORE` or `AMENT_IGNORE`, which
|
|
172
|
+
includes colcon's `build/`, `install/` and `log/` directories.
|
|
173
|
+
|
|
174
|
+
`--against` accepts a git ref (validated with `git rev-parse`), a directory, an
|
|
175
|
+
MCAP bag (`.mcap`) or a lockfile (`roschema.lock`); the type is detected
|
|
176
|
+
automatically. Initialised git submodules are compared at the commit the
|
|
177
|
+
baseline records for them.
|
|
178
|
+
|
|
179
|
+
Limitations:
|
|
180
|
+
|
|
181
|
+
- When a git ref and a directory share a name, the git ref wins; prefix the
|
|
182
|
+
directory with `./` (`--against ./main`) to use it instead.
|
|
183
|
+
- Lockfile and MCAP baselines have no real paths, so use a workspace or package
|
|
184
|
+
target with them rather than a path glob.
|
|
185
|
+
- A submodule whose recorded commit is not available locally (for example after
|
|
186
|
+
a shallow fetch) is skipped with a warning.
|
|
187
|
+
|
|
188
|
+
### Deep checking
|
|
189
|
+
|
|
190
|
+
Without `--deep`, an embedded type is compared by name only. `check --deep`
|
|
191
|
+
resolves nested types through the workspace and the ament index and reports
|
|
192
|
+
transitive breakage (RS110): an interface that embeds, directly or through other
|
|
193
|
+
types, a type whose wire layout changed is flagged even when its own definition
|
|
194
|
+
did not change, with the chain that connects them. RS110 takes the severity of
|
|
195
|
+
the change it traces back to, and suppressed or allowlisted changes are not
|
|
196
|
+
followed.
|
|
197
|
+
|
|
198
|
+
## Linting and formatting
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
roschema lint [TARGET] [--format text|json|github] [--rules RULE=SEV] [--exit-zero]
|
|
202
|
+
[--config PATH] [--exclude GLOB]
|
|
203
|
+
roschema fmt [TARGET] [--check] [--exclude GLOB]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`lint` checks a single tree, with no baseline. The naming rules follow rosidl:
|
|
207
|
+
field names in snake_case (RSL001), constants in UPPER_SNAKE_CASE (RSL002) and
|
|
208
|
+
interface names in PascalCase (RSL003). It also flags ambiguous `char` and
|
|
209
|
+
scalar `byte` types (RSL005) and fields shared by a service's request and
|
|
210
|
+
response (RSL010). RSL004 (missing documentation) and RSL006 (unbounded
|
|
211
|
+
fields) are off by default. Enable rules in `[lint.rules]`, or per run with
|
|
212
|
+
`--rules`, e.g. `--rules RSL004=warn --rules RSL001=off` (a bare `--rules RSL004`
|
|
213
|
+
means `=warn`). `--rules` is repeatable and overrides configuration.
|
|
214
|
+
|
|
215
|
+
`fmt` formats interface files canonically: a single space between type and
|
|
216
|
+
name, no spaces around a constant's `=`, two spaces before a trailing comment
|
|
217
|
+
and one trailing newline. Comments, blank lines and section separators are kept,
|
|
218
|
+
and so are inline `# roschema: ignore=` suppressions. Formatting is idempotent
|
|
219
|
+
and never changes what `check` reports; `fmt --check` exits `1` if any file
|
|
220
|
+
would change.
|
|
221
|
+
|
|
222
|
+
## Freeze (baselines without git)
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
roschema freeze [TARGET] -o roschema.lock
|
|
226
|
+
roschema check [TARGET] --against roschema.lock
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`freeze` writes a lockfile with each interface's canonical (formatted) text and
|
|
230
|
+
its sha256, so a release tarball can carry its own baseline. `check --against
|
|
231
|
+
roschema.lock` verifies the hashes and compares against it.
|
|
232
|
+
|
|
233
|
+
The lockfile also records the roschema `tool_version`, so upgrading roschema
|
|
234
|
+
changes that one line on the next `freeze` even when the interfaces are the
|
|
235
|
+
same. Comparison ignores it.
|
|
236
|
+
|
|
237
|
+
## Checking against a recorded bag
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
roschema bag-check BAG.mcap [TARGETS...] [--level LEVEL] [--config PATH]
|
|
241
|
+
[--format text|json|github] [--exit-zero] [--exclude GLOB]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Needs the `bag` extra (`uv tool install 'roschema[bag]'`).
|
|
245
|
+
|
|
246
|
+
`bag-check` uses the interface definitions recorded in an MCAP bag as the
|
|
247
|
+
baseline and asks whether **current code can still decode the recording**. It
|
|
248
|
+
therefore reports at the `wire` level by default; pass `--level` (or set
|
|
249
|
+
`[check] level`) to include source-level changes such as removed constants.
|
|
250
|
+
|
|
251
|
+
The bag's types are compared with the packages installed in the sourced
|
|
252
|
+
environment (the ament index), so system packages such as `sensor_msgs` are
|
|
253
|
+
compared with their installed definitions. Optional `TARGETS` (paths or package
|
|
254
|
+
names) replace the installed copy of their packages with your source: a package
|
|
255
|
+
you are editing is checked as it is now, and interfaces you deleted from it are
|
|
256
|
+
reported as removed. If two targets provide the same package, the last one
|
|
257
|
+
wins. A bag type found neither in the environment nor in a target is reported
|
|
258
|
+
as removed (RS040).
|
|
259
|
+
|
|
260
|
+
Limitations:
|
|
261
|
+
|
|
262
|
+
- `ros2msg` schemas are fully supported, including the concatenated definitions
|
|
263
|
+
of dependent types and the `SRV:` blocks recorded for service event topics.
|
|
264
|
+
- `ros2idl` schemas are converted on a best-effort basis for the subset that has
|
|
265
|
+
a `.msg` equivalent. Other encodings and IDL constructs (enums, unions,
|
|
266
|
+
typedefs, annotations, inheritance, constants outside structs, `wchar`) are a
|
|
267
|
+
tool error (exit `2`).
|
|
268
|
+
- Topics recorded without a definition (rosbag2 leaves them empty when it cannot
|
|
269
|
+
find one) are skipped and named in a note.
|
|
270
|
+
- Two different definitions of the same type in one bag are a tool error.
|
|
271
|
+
Definitions that differ only in formatting or comments are accepted.
|
|
272
|
+
|
|
273
|
+
## Configuration
|
|
274
|
+
|
|
275
|
+
Settings come from `roschema.toml`, or from `[tool.roschema]` in
|
|
276
|
+
`pyproject.toml`. A file at the workspace root applies to every package, and a
|
|
277
|
+
file in a package directory overrides it for that package. Precedence is CLI >
|
|
278
|
+
package > workspace > defaults. roschema looks for the workspace file in the
|
|
279
|
+
target directory and its parents, up to the git repository root (or the
|
|
280
|
+
filesystem root outside git). In each directory, `roschema.toml` takes
|
|
281
|
+
precedence over `pyproject.toml`.
|
|
282
|
+
|
|
283
|
+
```toml
|
|
284
|
+
[check]
|
|
285
|
+
level = "source" # wire | source | introspection | behavior
|
|
286
|
+
deep = false # --deep / --no-deep overrides this
|
|
287
|
+
|
|
288
|
+
[check.rules]
|
|
289
|
+
RS008 = "off" # off | warn | error
|
|
290
|
+
|
|
291
|
+
[check.allow]
|
|
292
|
+
# an intentional break: a fully-qualified target and a mandatory reason
|
|
293
|
+
"my_msgs/msg/State.old_field" = { rules = ["RS001"], reason = "removed in 2.0, see #142" }
|
|
294
|
+
|
|
295
|
+
[lint.rules]
|
|
296
|
+
RSL004 = "warn"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Unknown keys, unknown rule IDs (or IDs in the wrong section) and invalid values
|
|
300
|
+
are configuration errors (exit `2`), so a misspelt setting cannot silently do
|
|
301
|
+
nothing. Setting a rule's severity does not change which levels are reported,
|
|
302
|
+
except for rules that are off by default (see
|
|
303
|
+
[What gets reported](#what-gets-reported)).
|
|
304
|
+
|
|
305
|
+
### Suppressing findings
|
|
306
|
+
|
|
307
|
+
Suppressed findings are counted in the summary but never fail the run.
|
|
308
|
+
|
|
309
|
+
**Inline:** a trailing comment on the member line in the new version, e.g.
|
|
310
|
+
`float64 theta # roschema: ignore=RS003`. Separate several IDs with commas.
|
|
311
|
+
This works for lint rules too.
|
|
312
|
+
|
|
313
|
+
**Allowlist:** an entry in `[check.allow]`, keyed by the finding's target:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
<package>/<kind>/<Name>[.<section>][.<member>]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- `<kind>` is `msg`, `srv` or `action`.
|
|
320
|
+
- `<section>` is given for services (`request`, `response`) and actions (`goal`,
|
|
321
|
+
`result`, `feedback`) only, e.g. `my_srvs/srv/SetMode.request.mode`.
|
|
322
|
+
- Findings about a whole interface or section (RS040 interface removed, RS110,
|
|
323
|
+
RS004 fields reordered) have no `<member>`, e.g. `my_msgs/msg/Old`.
|
|
324
|
+
|
|
325
|
+
`reason` is required. `rules` limits the entry to those rule IDs; omit it to
|
|
326
|
+
allow every rule for that target.
|
|
327
|
+
|
|
328
|
+
## Output
|
|
329
|
+
|
|
330
|
+
**Text** (the default) groups findings by package and ends with a summary and a
|
|
331
|
+
semver advice per package:
|
|
332
|
+
|
|
333
|
+
- **MAJOR** when the package has a wire- or source-level error;
|
|
334
|
+
- **MINOR** when its only notable change is an added field (RS002) whose
|
|
335
|
+
severity you lowered to `warn`;
|
|
336
|
+
- **PATCH** otherwise.
|
|
337
|
+
|
|
338
|
+
The advice only covers reported findings. Additions such as a new interface or
|
|
339
|
+
constant are not findings, so they never raise the advice to MINOR on their own.
|
|
340
|
+
|
|
341
|
+
**`--format github`** emits `::error`/`::warning` workflow commands, which GitHub
|
|
342
|
+
shows as annotations on the PR diff.
|
|
343
|
+
|
|
344
|
+
**`--format json`** emits a stable document (`version: 1`):
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"version": 1,
|
|
349
|
+
"findings": [
|
|
350
|
+
{
|
|
351
|
+
"rule": "RS003",
|
|
352
|
+
"severity": "error",
|
|
353
|
+
"level": "wire",
|
|
354
|
+
"package": "my_msgs",
|
|
355
|
+
"interface": "my_msgs/msg/Pose",
|
|
356
|
+
"section": "message",
|
|
357
|
+
"target": "theta",
|
|
358
|
+
"message": "type changed float32 -> float64",
|
|
359
|
+
"old": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
360
|
+
"new": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
361
|
+
"suppressed": false
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
"summary": {
|
|
365
|
+
"total": 1,
|
|
366
|
+
"errors": 1,
|
|
367
|
+
"warnings": 0,
|
|
368
|
+
"suppressed": 0,
|
|
369
|
+
"advisories": { "my_msgs": "MAJOR" }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`old`/`new` are `null` when there is no corresponding side (a removed interface
|
|
375
|
+
has no `new`). `lint` output has no `advisories`.
|
|
376
|
+
|
|
377
|
+
## Exit codes
|
|
378
|
+
|
|
379
|
+
| Code | `check`, `bag-check`, `lint` | `fmt --check` | Other commands |
|
|
380
|
+
|---|---|---|---|
|
|
381
|
+
| `0` | no unsuppressed errors (warnings allowed) | every file formatted | success |
|
|
382
|
+
| `1` | at least one unsuppressed error | a file would change | n/a |
|
|
383
|
+
| `2` | tool or usage error (bad target, baseline, configuration or file) | tool or usage error | tool or usage error |
|
|
384
|
+
|
|
385
|
+
`--exit-zero` turns `1` into `0` for `check`, `bag-check` and `lint`; tool
|
|
386
|
+
errors still exit `2`.
|
|
387
|
+
|
|
388
|
+
## GitHub Action
|
|
389
|
+
|
|
390
|
+
A composite Action runs `roschema check` on the runner (no setup needed) and
|
|
391
|
+
posts the findings as annotations on the PR diff:
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
# .github/workflows/interfaces.yml
|
|
395
|
+
on: pull_request
|
|
396
|
+
jobs:
|
|
397
|
+
interfaces:
|
|
398
|
+
runs-on: ubuntu-latest
|
|
399
|
+
steps:
|
|
400
|
+
- uses: actions/checkout@v4
|
|
401
|
+
with:
|
|
402
|
+
fetch-depth: 0 # needed for the git-ref baseline
|
|
403
|
+
- uses: justagist/roschema/action@main
|
|
404
|
+
with:
|
|
405
|
+
against: origin/${{ github.base_ref }}
|
|
406
|
+
# version: "roschema==0.2.1" # pin a release for reproducible results
|
|
407
|
+
# level: source
|
|
408
|
+
# deep: "true"
|
|
409
|
+
# exclude: "vendor_msgs* legacy_msgs/msg/Old"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
| Input | Default | Meaning |
|
|
413
|
+
|---|---|---|
|
|
414
|
+
| `target` | `.` | path, package name or glob, as for the CLI |
|
|
415
|
+
| `against` | `origin/main` | git ref, directory, `.mcap` bag or `roschema.lock` |
|
|
416
|
+
| `level` | (empty) | reporting level; empty uses the configured level, else `source` |
|
|
417
|
+
| `format` | `github` | `text`, `json` or `github` |
|
|
418
|
+
| `deep` | `"false"` | `"true"` adds `--deep` |
|
|
419
|
+
| `exclude` | (empty) | whitespace-separated globs passed as `--exclude` |
|
|
420
|
+
| `version` | `roschema` | the roschema requirement installed with `uvx` |
|
|
421
|
+
|
|
422
|
+
By default the Action installs the latest roschema release on every run, so new
|
|
423
|
+
rules can change results without any change on your side. Set `version` to pin
|
|
424
|
+
a release instead. A lockfile or directory baseline needs no `fetch-depth: 0`,
|
|
425
|
+
because it is part of the checkout.
|
|
426
|
+
|
|
427
|
+
## Troubleshooting
|
|
428
|
+
|
|
429
|
+
**"no breaking changes found", but you expected some.** Run `roschema ls` with
|
|
430
|
+
the same target to see which packages and interfaces are discovered, then check
|
|
431
|
+
that:
|
|
432
|
+
|
|
433
|
+
- the packages are not below a `COLCON_IGNORE` or `AMENT_IGNORE` marker;
|
|
434
|
+
- a package-name target did not fall back to the installed copy (the warning
|
|
435
|
+
says so); run from the directory that contains the source, or pass its path;
|
|
436
|
+
- packages in git submodules were not skipped (the warning names any submodule
|
|
437
|
+
whose baseline commit is missing; fetch it with `git submodule update`);
|
|
438
|
+
- the change's level is included: a constant change is not reported under
|
|
439
|
+
`--level wire`, and `bag-check` defaults to `wire`.
|
|
440
|
+
|
|
441
|
+
**A configuration error names an unknown key.** Each table accepts only the keys
|
|
442
|
+
shown in [Configuration](#configuration); the error suggests the closest one.
|
|
443
|
+
|
|
444
|
+
## Rule reference
|
|
445
|
+
|
|
446
|
+
Every rule, its levels, default severity, rationale and an example is
|
|
447
|
+
documented in [docs/rules.md](docs/rules.md), generated from the rule metadata.
|
|
448
|
+
|
|
449
|
+
## Non-goals
|
|
450
|
+
|
|
451
|
+
roschema deliberately does **not**:
|
|
452
|
+
|
|
453
|
+
- generate migration or translation rules;
|
|
454
|
+
- parse ROS 1 `.msg` (md5 semantics differ);
|
|
455
|
+
- parse hand-written `.idl` beyond the MCAP-embedded subset;
|
|
456
|
+
- provide any registry or server component;
|
|
457
|
+
- parse `CMakeLists.txt` for discovery (directory convention + ament index only).
|
|
458
|
+
|
|
459
|
+
## Relationship to REP-2011 and type hashes
|
|
460
|
+
|
|
461
|
+
These are complementary layers, not alternatives:
|
|
462
|
+
|
|
463
|
+
- **roschema** prevents *accidental* breaks at PR time, before anything ships.
|
|
464
|
+
- **RIHS type hashes** (REP-2011) detect a pub/sub *mismatch at runtime* -- after
|
|
465
|
+
the fact, on the robot.
|
|
466
|
+
- **REP-2011 type-description distribution / translation** mitigates *intentional*
|
|
467
|
+
evolution by carrying enough information to translate between versions.
|
|
468
|
+
|
|
469
|
+
roschema is the earliest gate: catch the break in review so the later layers
|
|
470
|
+
never have to.
|
|
471
|
+
|
|
472
|
+
## Contributing
|
|
473
|
+
|
|
474
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, checks and
|
|
475
|
+
release process.
|