Skip to main content

roschema

CI PyPI Python versions License: Apache-2.0 Ruff Checked with mypy

Catch breaking ROS 2 interface changes in the PR, not on the robot.

roschema detects breaking changes, lints and formats ROS 2 interface definitions (.msg, .srv, .action) with zero ROS dependencies, so it runs in a bare CI container.

demo

Quickstart

# in a pull request, compare the working tree against the base branch
uvx roschema check --against origin/main

See the full rule reference for every check (RS***) and lint (RSL***) rule.

Checking

roschema check [TARGET] --against REF [--level LEVEL] [--format text|json|github]
               [--deep] [--exit-zero] [--config PATH] [--exclude GLOB]

TARGET is a path, a package name, or a glob ('src/**') that expands to every package underneath; omit it for the current workspace.

--against accepts a git ref (validated with git rev-parse), a directory, an MCAP bag (.mcap) or a lockfile (roschema.lock); the type is auto-detected. A git ref is preferred when a directory of the same name also exists -- prefix the argument with ./ (e.g. --against ./main) to force the directory. Exit codes: 0 clean, 1 breaking changes found, 2 tool/usage error. --exit-zero forces 0. Warnings alone do not fail (exit 0).

CDR is positional -- fields are serialised in declaration order with no field tags -- so adding, removing, reordering or retyping a field is wire-breaking even when your code still compiles. That is why the taxonomy is structural, and why the default source level already reports wire-level changes.

Freeze (non-git baselines)

roschema freeze [TARGET] -o roschema.lock
roschema check  [TARGET] --against roschema.lock

freeze writes a lockfile holding each interface's canonical (post-fmt) text plus a sha256, so a release tarball can carry its own baseline without git. check --against roschema.lock reads it back (sha256-verified) and compares.

The lockfile records the roschema tool_version for provenance; upgrading roschema therefore changes that one line on the next freeze even when the interfaces are identical. It is not read during comparison -- check diffs only the interface text -- so the churn is cosmetic.

Deep checking

check --deep resolves nested complex types through the workspace and ament index and reports transitive breakage (RS110): an interface that embeds - directly or through nested types - another whose wire layout changed is flagged even when its own definition is unchanged, with the causal chain shown. Without --deep, nested types are compared by name only.

Linting and formatting

roschema lint [TARGET] [--format text|json|github] [--rules RULE=SEV] [--exit-zero]
roschema fmt  [TARGET] [--check]

lint checks a single tree (no baseline). Naming rules follow rosidl: field names snake_case (RSL001), constants UPPER_SNAKE (RSL002), interface names PascalCase (RSL003); it also flags deprecated char/scalar-byte (RSL005) and request/response field shadowing in services (RSL010). RSL004 (missing doc) and RSL006 (unbounded) are off by default; enable via [lint.rules] or per run with --rules (e.g. --rules RSL004=warn --rules RSL001=off; a bare --rules RSL004 means =warn). --rules is repeatable and beats config.

fmt canonically formats interface files (single space between type and name, = without spaces for constants, one trailing newline) while preserving comments, blank lines and section separators. It is idempotent and never changes what check reports; --check exits 1 if any file would change. Inline # roschema: ignore= suppressions survive formatting.

Checking against a recorded bag

pip install 'roschema[bag]'
roschema bag-check recording.mcap [TARGETS...] [--format ...]

bag-check reads the interface definitions embedded in an MCAP bag and checks whether they are still compatible with your current environment - the bag is the baseline, so it flags changes that would stop current code from reading the recorded data. By default the bag's types are resolved from the ament index (AMENT_PREFIX_PATH), i.e. the packages installed in the sourced environment, so system packages like sensor_msgs are compared against their installed definitions rather than reported as missing.

Optional TARGETS (paths or package names) are overlaid on top of the environment, so a package you are editing can be checked from source instead of its installed copy. A bag type found in neither the environment nor any target is reported as removed (RS040). ros2msg schemas (including concatenated dependent definitions) are fully supported; ros2idl is a best-effort conversion of the msg-equivalent subset.

Compatibility levels

--level selects a cumulative bundle of what is reported; the default is source.

Level Reports
wire CDR byte-stream compatibility only
source wire + generated C++/Python API
introspection source + name-based tooling (bags, echo, YAML)
behavior introspection + semantics (defaults, constant values)

Configuration

roschema.toml at the workspace root, a per-package roschema.toml, or [tool.roschema] in pyproject.toml. Precedence: CLI > package > workspace > defaults.

[check]
level = "source"

[check.rules]
RS008 = "off"          # off | warn | error

[check.allow]
# intentional breaks need a fully-qualified target and a mandatory reason
"my_msgs/msg/State.old_field" = { rules = ["RS001"], reason = "removed in 2.0, see #142" }

Inline suppression: a trailing comment on the new-side member line, e.g. float64 theta # roschema: ignore=RS003 (comma-separate multiple IDs). Suppressed findings are counted, not failed.

JSON output

--format json emits a stable document (version: 1):

{
  "version": 1,
  "findings": [
    {
      "rule": "RS003",
      "severity": "error",
      "level": "wire",
      "package": "my_msgs",
      "interface": "my_msgs/msg/Pose",
      "section": "message",
      "target": "theta",
      "message": "type changed float32 -> float64",
      "old": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
      "new": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
      "suppressed": false
    }
  ],
  "summary": {
    "total": 1,
    "errors": 1,
    "warnings": 0,
    "suppressed": 0,
    "advisories": { "my_msgs": "MAJOR" }
  }
}

old/new are null when there is no corresponding side (e.g. a removed interface has no new). --format github emits ::error/::warning workflow-command annotations instead.

GitHub Action

A composite action wraps uvx roschema check and posts inline annotations on the PR diff:

# .github/workflows/interfaces.yml
on: pull_request
jobs:
  interfaces:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0            # needed for the git-ref baseline
      - uses: justagist/roschema/action@v1
        with:
          against: origin/${{ github.base_ref }}
          # level: source
          # deep: "true"
          # exclude: "vendor_msgs* legacy_msgs/msg/Old"

against takes the same values as the CLI (git ref, directory, .mcap, roschema.lock). A lockfile or directory baseline needs no fetch-depth: 0 because it is a plain file in the checkout.

Rule reference

Every rule, its levels, default severity and rationale is documented in docs/rules.md, generated from the rule metadata with python -m roschema.rules.gendocs.

Non-goals

roschema deliberately does not:

  • generate migration or translation rules;
  • parse ROS 1 .msg (md5 semantics differ);
  • parse hand-written .idl beyond the MCAP-embedded subset;
  • provide any registry or server component;
  • parse CMakeLists.txt for discovery (directory convention + ament index only).

Relationship to REP-2011 and type hashes

These are complementary layers, not alternatives:

  • roschema prevents accidental breaks at PR time, before anything ships.
  • RIHS type hashes (REP-2011) detect a pub/sub mismatch at runtime -- after the fact, on the robot.
  • REP-2011 type-description distribution / translation mitigates intentional evolution by carrying enough information to translate between versions.

roschema is the earliest gate: catch the break in review so the later layers never have to.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

roschema-0.1.0.tar.gz (66.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

roschema-0.1.0-py3-none-any.whl (68.4 kB view details)

Uploaded Python 3

File details

Details for the file roschema-0.1.0.tar.gz.

File metadata

  • Download URL: roschema-0.1.0.tar.gz
  • Upload date:
  • Size: 66.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for roschema-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2d2c3912216a03e8c6110d4885e856b2c9005142c90b6c0002c8fcc9d1d781a2
MD5 9a41aecaf65a5538652c6eba195c777b
BLAKE2b-256 8a403b8fe66f905c54dcd1f0cb178d897d17ef624ce7bbcf93a01fef8900e3b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for roschema-0.1.0.tar.gz:

Publisher: release.yml on justagist/roschema

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file roschema-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: roschema-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 68.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for roschema-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 191561db89960abb5b001865f8fcc07b74da99da293f4cbe993347e0564bfd92
MD5 492712db258c5f87d485c791893c65c9
BLAKE2b-256 78237c443b741990f03a349c7756822967d0eddab7c883df1ba5ba7eed368673

See more details on using hashes here.

Provenance

The following attestation bundles were made for roschema-0.1.0-py3-none-any.whl:

Publisher: release.yml on justagist/roschema

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page