roschema 0.2.1__tar.gz → 0.3.0__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.1 → roschema-0.3.0}/.github/workflows/ci.yml +3 -3
- {roschema-0.2.1 → roschema-0.3.0}/.github/workflows/release.yml +1 -1
- {roschema-0.2.1 → roschema-0.3.0}/CHANGELOG.md +23 -0
- {roschema-0.2.1 → roschema-0.3.0}/PKG-INFO +37 -16
- {roschema-0.2.1 → roschema-0.3.0}/README.md +35 -15
- {roschema-0.2.1 → roschema-0.3.0}/action/action.yml +22 -2
- {roschema-0.2.1 → roschema-0.3.0}/pyproject.toml +2 -1
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/cli.py +66 -19
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/report/__init__.py +3 -1
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/report/github.py +14 -3
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/report/json_report.py +16 -5
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/report/summary.py +37 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/report/text.py +8 -3
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/golden_check.json +6 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_action.py +21 -5
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_check_cli.py +61 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_golden.py +4 -5
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_lint.py +8 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_mcap.py +28 -0
- {roschema-0.2.1 → roschema-0.3.0}/.gitignore +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/CONTRIBUTING.md +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/LICENSE +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/docs/rules.md +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/config.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/deep.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/differ.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/discovery/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/discovery/discover.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/discovery/model.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/discovery/resolve.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/formatter.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/freeze.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/ir.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/loader.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/mcap_fs.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/parser/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/parser/core.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/parser/errors.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/py.typed +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs001_field_removed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs002_field_added.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs003_field_type_changed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs004_fields_reordered.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs005_field_renamed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs006_array_kind_changed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs007_bound_tightened.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs008_bound_loosened.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs020_constant_removed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs021_constant_value_changed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs022_constant_type_changed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs030_default_changed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs040_interface_removed.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs060_message_version.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/breaking/rs110_transitive.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/gendocs.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/__init__.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/_common.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl001_field_snake_case.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl002_constant_upper_snake.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl003_name_pascal_case.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl004_missing_doc.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl005_discouraged_types.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl006_unbounded.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/lint/rsl010_srv_shadow.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/model.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/rules/registry.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/src/roschema/vfs.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/_helpers.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/conftest.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/AMENT_IGNORE +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/COLCON_IGNORE +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/action/Move.action +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/package.xml +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_new/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/action/Move.action +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Removed.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/package.xml +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/fixtures/ws_old/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_cli_ls.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_cli_version.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_config.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_deep.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_differ.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_discovery.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_fmt.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_freeze.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_gendocs.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_parser.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_report_formats.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_rules.py +0 -0
- {roschema-0.2.1 → roschema-0.3.0}/tests/test_vfs.py +0 -0
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
17
|
- uses: actions/checkout@v4
|
|
18
|
-
- uses: astral-sh/setup-uv@
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.3.3
|
|
19
19
|
with:
|
|
20
20
|
python-version: "3.12"
|
|
21
21
|
enable-cache: true
|
|
@@ -36,10 +36,10 @@ jobs:
|
|
|
36
36
|
strategy:
|
|
37
37
|
fail-fast: false
|
|
38
38
|
matrix:
|
|
39
|
-
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
39
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
40
40
|
steps:
|
|
41
41
|
- uses: actions/checkout@v4
|
|
42
|
-
- uses: astral-sh/setup-uv@
|
|
42
|
+
- uses: astral-sh/setup-uv@v10.3.3
|
|
43
43
|
with:
|
|
44
44
|
python-version: ${{ matrix.python-version }}
|
|
45
45
|
enable-cache: true
|
|
@@ -3,6 +3,29 @@
|
|
|
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.3.0] - 2026-11-03
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Reports say how many interfaces were compared, and on which side the rest
|
|
11
|
+
exist only (JSON: `coverage`).
|
|
12
|
+
- `--warnings-as-errors` for `check`, `bag-check` and `lint`, and the Action's
|
|
13
|
+
`warnings-as-errors` input.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Python 3.14 is supported and tested.
|
|
18
|
+
- An empty baseline is an error (exit `2`) instead of a silent pass; pass
|
|
19
|
+
`--allow-empty` (Action: `allow-empty`) when that is expected.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `--format github` attaches findings with no new-side location, such as a
|
|
24
|
+
removed field or a reorder, to the interface's file instead of leaving GitHub
|
|
25
|
+
to show them against the workflow file.
|
|
26
|
+
- The Action uses `astral-sh/setup-uv@v10.3.3` (Node 24) with its cache off,
|
|
27
|
+
removing the Node 20 deprecation and missing `uv.lock` warnings.
|
|
28
|
+
|
|
6
29
|
## [0.2.1] - 2026-11-03
|
|
7
30
|
|
|
8
31
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: roschema
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Breaking-change detection, linting and formatting for ROS 2 interface definitions
|
|
5
5
|
Project-URL: Homepage, https://github.com/justagist/roschema
|
|
6
6
|
Project-URL: Documentation, https://github.com/justagist/roschema#readme
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
22
|
Classifier: Typing :: Typed
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -153,7 +154,8 @@ roschema ls
|
|
|
153
154
|
|
|
154
155
|
```
|
|
155
156
|
roschema check [TARGET] --against BASELINE [--level LEVEL] [--format text|json|github]
|
|
156
|
-
[--deep | --no-deep] [--exit-zero] [--
|
|
157
|
+
[--deep | --no-deep] [--exit-zero] [--warnings-as-errors] [--allow-empty]
|
|
158
|
+
[--config PATH] [--exclude GLOB]
|
|
157
159
|
```
|
|
158
160
|
|
|
159
161
|
`TARGET` selects what to check; omit it to check every interface package below
|
|
@@ -176,6 +178,13 @@ MCAP bag (`.mcap`) or a lockfile (`roschema.lock`); the type is detected
|
|
|
176
178
|
automatically. Initialised git submodules are compared at the commit the
|
|
177
179
|
baseline records for them.
|
|
178
180
|
|
|
181
|
+
Every report says how much was compared, for example `compared 12 interface(s)
|
|
182
|
+
in 3 package(s); 1 only in the target`. A baseline with no interfaces at all is
|
|
183
|
+
an error (exit `2`): every current interface would look newly added, so the
|
|
184
|
+
check would pass without comparing anything. That usually means the wrong
|
|
185
|
+
baseline or target; on the pull request that adds the first interfaces, pass
|
|
186
|
+
`--allow-empty`.
|
|
187
|
+
|
|
179
188
|
Limitations:
|
|
180
189
|
|
|
181
190
|
- When a git ref and a directory share a name, the git ref wins; prefix the
|
|
@@ -199,7 +208,7 @@ followed.
|
|
|
199
208
|
|
|
200
209
|
```
|
|
201
210
|
roschema lint [TARGET] [--format text|json|github] [--rules RULE=SEV] [--exit-zero]
|
|
202
|
-
[--config PATH] [--exclude GLOB]
|
|
211
|
+
[--warnings-as-errors] [--config PATH] [--exclude GLOB]
|
|
203
212
|
roschema fmt [TARGET] [--check] [--exclude GLOB]
|
|
204
213
|
```
|
|
205
214
|
|
|
@@ -238,7 +247,8 @@ same. Comparison ignores it.
|
|
|
238
247
|
|
|
239
248
|
```
|
|
240
249
|
roschema bag-check BAG.mcap [TARGETS...] [--level LEVEL] [--config PATH]
|
|
241
|
-
[--format text|json|github] [--exit-zero] [--
|
|
250
|
+
[--format text|json|github] [--exit-zero] [--warnings-as-errors]
|
|
251
|
+
[--exclude GLOB]
|
|
242
252
|
```
|
|
243
253
|
|
|
244
254
|
Needs the `bag` extra (`uv tool install 'roschema[bag]'`).
|
|
@@ -327,8 +337,8 @@ allow every rule for that target.
|
|
|
327
337
|
|
|
328
338
|
## Output
|
|
329
339
|
|
|
330
|
-
**Text** (the default)
|
|
331
|
-
semver advice per package:
|
|
340
|
+
**Text** (the default) starts with what was compared, groups findings by
|
|
341
|
+
package and ends with a summary and a semver advice per package:
|
|
332
342
|
|
|
333
343
|
- **MAJOR** when the package has a wire- or source-level error;
|
|
334
344
|
- **MINOR** when its only notable change is an added field (RS002) whose
|
|
@@ -338,8 +348,8 @@ semver advice per package:
|
|
|
338
348
|
The advice only covers reported findings. Additions such as a new interface or
|
|
339
349
|
constant are not findings, so they never raise the advice to MINOR on their own.
|
|
340
350
|
|
|
341
|
-
**`--format github`** emits `::error`/`::warning`
|
|
342
|
-
shows as annotations on the PR diff.
|
|
351
|
+
**`--format github`** prints what was compared, then emits `::error`/`::warning`
|
|
352
|
+
workflow commands, which GitHub shows as annotations on the PR diff.
|
|
343
353
|
|
|
344
354
|
**`--format json`** emits a stable document (`version: 1`):
|
|
345
355
|
|
|
@@ -367,12 +377,19 @@ shows as annotations on the PR diff.
|
|
|
367
377
|
"warnings": 0,
|
|
368
378
|
"suppressed": 0,
|
|
369
379
|
"advisories": { "my_msgs": "MAJOR" }
|
|
380
|
+
},
|
|
381
|
+
"coverage": {
|
|
382
|
+
"packages": 1,
|
|
383
|
+
"compared": 1,
|
|
384
|
+
"target_only": 0,
|
|
385
|
+
"baseline_only": 0
|
|
370
386
|
}
|
|
371
387
|
}
|
|
372
388
|
```
|
|
373
389
|
|
|
374
390
|
`old`/`new` are `null` when there is no corresponding side (a removed interface
|
|
375
|
-
has no `new`). `
|
|
391
|
+
has no `new`). `coverage` counts interfaces present on both sides (`compared`)
|
|
392
|
+
and on one side only. `lint` output has no `advisories` or `coverage`.
|
|
376
393
|
|
|
377
394
|
## Exit codes
|
|
378
395
|
|
|
@@ -380,10 +397,11 @@ has no `new`). `lint` output has no `advisories`.
|
|
|
380
397
|
|---|---|---|---|
|
|
381
398
|
| `0` | no unsuppressed errors (warnings allowed) | every file formatted | success |
|
|
382
399
|
| `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 |
|
|
400
|
+
| `2` | tool or usage error (bad target, baseline, configuration or file, or an empty baseline) | tool or usage error | tool or usage error |
|
|
384
401
|
|
|
385
|
-
`--
|
|
386
|
-
errors still exit
|
|
402
|
+
`--warnings-as-errors` makes unsuppressed warnings exit `1` too. `--exit-zero`
|
|
403
|
+
turns `1` into `0` for `check`, `bag-check` and `lint`; tool errors still exit
|
|
404
|
+
`2`.
|
|
387
405
|
|
|
388
406
|
## GitHub Action
|
|
389
407
|
|
|
@@ -403,9 +421,10 @@ jobs:
|
|
|
403
421
|
- uses: justagist/roschema/action@main
|
|
404
422
|
with:
|
|
405
423
|
against: origin/${{ github.base_ref }}
|
|
406
|
-
# version: "roschema==0.
|
|
424
|
+
# version: "roschema==0.3.0" # pin a release for reproducible results
|
|
407
425
|
# level: source
|
|
408
426
|
# deep: "true"
|
|
427
|
+
# warnings-as-errors: "true"
|
|
409
428
|
# exclude: "vendor_msgs* legacy_msgs/msg/Old"
|
|
410
429
|
```
|
|
411
430
|
|
|
@@ -416,6 +435,8 @@ jobs:
|
|
|
416
435
|
| `level` | (empty) | reporting level; empty uses the configured level, else `source` |
|
|
417
436
|
| `format` | `github` | `text`, `json` or `github` |
|
|
418
437
|
| `deep` | `"false"` | `"true"` adds `--deep` |
|
|
438
|
+
| `allow-empty` | `"false"` | `"true"` adds `--allow-empty` |
|
|
439
|
+
| `warnings-as-errors` | `"false"` | `"true"` adds `--warnings-as-errors` |
|
|
419
440
|
| `exclude` | (empty) | whitespace-separated globs passed as `--exclude` |
|
|
420
441
|
| `version` | `roschema` | the roschema requirement installed with `uvx` |
|
|
421
442
|
|
|
@@ -426,9 +447,9 @@ because it is part of the checkout.
|
|
|
426
447
|
|
|
427
448
|
## Troubleshooting
|
|
428
449
|
|
|
429
|
-
**"no breaking changes found", but you expected some.**
|
|
430
|
-
|
|
431
|
-
that:
|
|
450
|
+
**"no breaking changes found", but you expected some.** The line under the
|
|
451
|
+
header says how many interfaces were compared. Run `roschema ls` with the same
|
|
452
|
+
target to see which packages and interfaces are discovered, then check that:
|
|
432
453
|
|
|
433
454
|
- the packages are not below a `COLCON_IGNORE` or `AMENT_IGNORE` marker;
|
|
434
455
|
- a package-name target did not fall back to the installed copy (the warning
|
|
@@ -119,7 +119,8 @@ roschema ls
|
|
|
119
119
|
|
|
120
120
|
```
|
|
121
121
|
roschema check [TARGET] --against BASELINE [--level LEVEL] [--format text|json|github]
|
|
122
|
-
[--deep | --no-deep] [--exit-zero] [--
|
|
122
|
+
[--deep | --no-deep] [--exit-zero] [--warnings-as-errors] [--allow-empty]
|
|
123
|
+
[--config PATH] [--exclude GLOB]
|
|
123
124
|
```
|
|
124
125
|
|
|
125
126
|
`TARGET` selects what to check; omit it to check every interface package below
|
|
@@ -142,6 +143,13 @@ MCAP bag (`.mcap`) or a lockfile (`roschema.lock`); the type is detected
|
|
|
142
143
|
automatically. Initialised git submodules are compared at the commit the
|
|
143
144
|
baseline records for them.
|
|
144
145
|
|
|
146
|
+
Every report says how much was compared, for example `compared 12 interface(s)
|
|
147
|
+
in 3 package(s); 1 only in the target`. A baseline with no interfaces at all is
|
|
148
|
+
an error (exit `2`): every current interface would look newly added, so the
|
|
149
|
+
check would pass without comparing anything. That usually means the wrong
|
|
150
|
+
baseline or target; on the pull request that adds the first interfaces, pass
|
|
151
|
+
`--allow-empty`.
|
|
152
|
+
|
|
145
153
|
Limitations:
|
|
146
154
|
|
|
147
155
|
- When a git ref and a directory share a name, the git ref wins; prefix the
|
|
@@ -165,7 +173,7 @@ followed.
|
|
|
165
173
|
|
|
166
174
|
```
|
|
167
175
|
roschema lint [TARGET] [--format text|json|github] [--rules RULE=SEV] [--exit-zero]
|
|
168
|
-
[--config PATH] [--exclude GLOB]
|
|
176
|
+
[--warnings-as-errors] [--config PATH] [--exclude GLOB]
|
|
169
177
|
roschema fmt [TARGET] [--check] [--exclude GLOB]
|
|
170
178
|
```
|
|
171
179
|
|
|
@@ -204,7 +212,8 @@ same. Comparison ignores it.
|
|
|
204
212
|
|
|
205
213
|
```
|
|
206
214
|
roschema bag-check BAG.mcap [TARGETS...] [--level LEVEL] [--config PATH]
|
|
207
|
-
[--format text|json|github] [--exit-zero] [--
|
|
215
|
+
[--format text|json|github] [--exit-zero] [--warnings-as-errors]
|
|
216
|
+
[--exclude GLOB]
|
|
208
217
|
```
|
|
209
218
|
|
|
210
219
|
Needs the `bag` extra (`uv tool install 'roschema[bag]'`).
|
|
@@ -293,8 +302,8 @@ allow every rule for that target.
|
|
|
293
302
|
|
|
294
303
|
## Output
|
|
295
304
|
|
|
296
|
-
**Text** (the default)
|
|
297
|
-
semver advice per package:
|
|
305
|
+
**Text** (the default) starts with what was compared, groups findings by
|
|
306
|
+
package and ends with a summary and a semver advice per package:
|
|
298
307
|
|
|
299
308
|
- **MAJOR** when the package has a wire- or source-level error;
|
|
300
309
|
- **MINOR** when its only notable change is an added field (RS002) whose
|
|
@@ -304,8 +313,8 @@ semver advice per package:
|
|
|
304
313
|
The advice only covers reported findings. Additions such as a new interface or
|
|
305
314
|
constant are not findings, so they never raise the advice to MINOR on their own.
|
|
306
315
|
|
|
307
|
-
**`--format github`** emits `::error`/`::warning`
|
|
308
|
-
shows as annotations on the PR diff.
|
|
316
|
+
**`--format github`** prints what was compared, then emits `::error`/`::warning`
|
|
317
|
+
workflow commands, which GitHub shows as annotations on the PR diff.
|
|
309
318
|
|
|
310
319
|
**`--format json`** emits a stable document (`version: 1`):
|
|
311
320
|
|
|
@@ -333,12 +342,19 @@ shows as annotations on the PR diff.
|
|
|
333
342
|
"warnings": 0,
|
|
334
343
|
"suppressed": 0,
|
|
335
344
|
"advisories": { "my_msgs": "MAJOR" }
|
|
345
|
+
},
|
|
346
|
+
"coverage": {
|
|
347
|
+
"packages": 1,
|
|
348
|
+
"compared": 1,
|
|
349
|
+
"target_only": 0,
|
|
350
|
+
"baseline_only": 0
|
|
336
351
|
}
|
|
337
352
|
}
|
|
338
353
|
```
|
|
339
354
|
|
|
340
355
|
`old`/`new` are `null` when there is no corresponding side (a removed interface
|
|
341
|
-
has no `new`). `
|
|
356
|
+
has no `new`). `coverage` counts interfaces present on both sides (`compared`)
|
|
357
|
+
and on one side only. `lint` output has no `advisories` or `coverage`.
|
|
342
358
|
|
|
343
359
|
## Exit codes
|
|
344
360
|
|
|
@@ -346,10 +362,11 @@ has no `new`). `lint` output has no `advisories`.
|
|
|
346
362
|
|---|---|---|---|
|
|
347
363
|
| `0` | no unsuppressed errors (warnings allowed) | every file formatted | success |
|
|
348
364
|
| `1` | at least one unsuppressed error | a file would change | n/a |
|
|
349
|
-
| `2` | tool or usage error (bad target, baseline, configuration or file) | tool or usage error | tool or usage error |
|
|
365
|
+
| `2` | tool or usage error (bad target, baseline, configuration or file, or an empty baseline) | tool or usage error | tool or usage error |
|
|
350
366
|
|
|
351
|
-
`--
|
|
352
|
-
errors still exit
|
|
367
|
+
`--warnings-as-errors` makes unsuppressed warnings exit `1` too. `--exit-zero`
|
|
368
|
+
turns `1` into `0` for `check`, `bag-check` and `lint`; tool errors still exit
|
|
369
|
+
`2`.
|
|
353
370
|
|
|
354
371
|
## GitHub Action
|
|
355
372
|
|
|
@@ -369,9 +386,10 @@ jobs:
|
|
|
369
386
|
- uses: justagist/roschema/action@main
|
|
370
387
|
with:
|
|
371
388
|
against: origin/${{ github.base_ref }}
|
|
372
|
-
# version: "roschema==0.
|
|
389
|
+
# version: "roschema==0.3.0" # pin a release for reproducible results
|
|
373
390
|
# level: source
|
|
374
391
|
# deep: "true"
|
|
392
|
+
# warnings-as-errors: "true"
|
|
375
393
|
# exclude: "vendor_msgs* legacy_msgs/msg/Old"
|
|
376
394
|
```
|
|
377
395
|
|
|
@@ -382,6 +400,8 @@ jobs:
|
|
|
382
400
|
| `level` | (empty) | reporting level; empty uses the configured level, else `source` |
|
|
383
401
|
| `format` | `github` | `text`, `json` or `github` |
|
|
384
402
|
| `deep` | `"false"` | `"true"` adds `--deep` |
|
|
403
|
+
| `allow-empty` | `"false"` | `"true"` adds `--allow-empty` |
|
|
404
|
+
| `warnings-as-errors` | `"false"` | `"true"` adds `--warnings-as-errors` |
|
|
385
405
|
| `exclude` | (empty) | whitespace-separated globs passed as `--exclude` |
|
|
386
406
|
| `version` | `roschema` | the roschema requirement installed with `uvx` |
|
|
387
407
|
|
|
@@ -392,9 +412,9 @@ because it is part of the checkout.
|
|
|
392
412
|
|
|
393
413
|
## Troubleshooting
|
|
394
414
|
|
|
395
|
-
**"no breaking changes found", but you expected some.**
|
|
396
|
-
|
|
397
|
-
that:
|
|
415
|
+
**"no breaking changes found", but you expected some.** The line under the
|
|
416
|
+
header says how many interfaces were compared. Run `roschema ls` with the same
|
|
417
|
+
target to see which packages and interfaces are discovered, then check that:
|
|
398
418
|
|
|
399
419
|
- the packages are not below a `COLCON_IGNORE` or `AMENT_IGNORE` marker;
|
|
400
420
|
- a package-name target did not fall back to the installed copy (the warning
|
|
@@ -22,6 +22,14 @@ inputs:
|
|
|
22
22
|
deep:
|
|
23
23
|
description: Resolve nested types and report transitive breakage (RS110).
|
|
24
24
|
default: "false"
|
|
25
|
+
allow-empty:
|
|
26
|
+
description: >-
|
|
27
|
+
Pass when the baseline has no interfaces, e.g. on the pull request that
|
|
28
|
+
adds the first ones. Otherwise an empty baseline fails the step.
|
|
29
|
+
default: "false"
|
|
30
|
+
warnings-as-errors:
|
|
31
|
+
description: Fail the step on warnings as well as errors.
|
|
32
|
+
default: "false"
|
|
25
33
|
exclude:
|
|
26
34
|
description: >-
|
|
27
35
|
Whitespace-separated globs of packages/interfaces to skip. Split on
|
|
@@ -30,14 +38,18 @@ inputs:
|
|
|
30
38
|
version:
|
|
31
39
|
description: >-
|
|
32
40
|
roschema requirement for uvx. Unpinned runs the latest release (refreshed
|
|
33
|
-
on every run, even with a uv cache); pin it (e.g. "roschema==0.
|
|
41
|
+
on every run, even with a uv cache); pin it (e.g. "roschema==0.3.0") for
|
|
34
42
|
reproducible results.
|
|
35
43
|
default: roschema
|
|
36
44
|
|
|
37
45
|
runs:
|
|
38
46
|
using: composite
|
|
39
47
|
steps:
|
|
40
|
-
- uses: astral-sh/setup-uv@
|
|
48
|
+
- uses: astral-sh/setup-uv@v10.3.3
|
|
49
|
+
with:
|
|
50
|
+
# Nothing to key a cache on in the caller's repo, and uvx refreshes
|
|
51
|
+
# roschema on every run anyway.
|
|
52
|
+
enable-cache: false
|
|
41
53
|
- shell: bash
|
|
42
54
|
env:
|
|
43
55
|
ROSCHEMA_TARGET: ${{ inputs.target }}
|
|
@@ -45,6 +57,8 @@ runs:
|
|
|
45
57
|
ROSCHEMA_LEVEL: ${{ inputs.level }}
|
|
46
58
|
ROSCHEMA_FORMAT: ${{ inputs.format }}
|
|
47
59
|
ROSCHEMA_DEEP: ${{ inputs.deep }}
|
|
60
|
+
ROSCHEMA_ALLOW_EMPTY: ${{ inputs.allow-empty }}
|
|
61
|
+
ROSCHEMA_WARNINGS_AS_ERRORS: ${{ inputs.warnings-as-errors }}
|
|
48
62
|
ROSCHEMA_EXCLUDE: ${{ inputs.exclude }}
|
|
49
63
|
ROSCHEMA_VERSION: ${{ inputs.version }}
|
|
50
64
|
run: |
|
|
@@ -58,6 +72,12 @@ runs:
|
|
|
58
72
|
if [ "$ROSCHEMA_DEEP" = "true" ]; then
|
|
59
73
|
args+=(--deep)
|
|
60
74
|
fi
|
|
75
|
+
if [ "$ROSCHEMA_ALLOW_EMPTY" = "true" ]; then
|
|
76
|
+
args+=(--allow-empty)
|
|
77
|
+
fi
|
|
78
|
+
if [ "$ROSCHEMA_WARNINGS_AS_ERRORS" = "true" ]; then
|
|
79
|
+
args+=(--warnings-as-errors)
|
|
80
|
+
fi
|
|
61
81
|
read -r -a excludes <<< "${ROSCHEMA_EXCLUDE//$'\n'/ }"
|
|
62
82
|
for glob in "${excludes[@]}"; do
|
|
63
83
|
args+=(--exclude "$glob")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "roschema"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Breaking-change detection, linting and formatting for ROS 2 interface definitions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -19,6 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3.11",
|
|
20
20
|
"Programming Language :: Python :: 3.12",
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
22
23
|
"Topic :: Software Development :: Quality Assurance",
|
|
23
24
|
"Typing :: Typed",
|
|
24
25
|
]
|
|
@@ -38,7 +38,7 @@ from .ir import InterfaceFile
|
|
|
38
38
|
from .loader import load_interfaces
|
|
39
39
|
from .mcap_fs import McapTypeFS
|
|
40
40
|
from .parser import ParseError
|
|
41
|
-
from .report import build_github, build_json, render_lint, render_text
|
|
41
|
+
from .report import Coverage, build_github, build_json, coverage, render_lint, render_text
|
|
42
42
|
from .rules import Finding, all_lint_rules, evaluate, lint_interfaces
|
|
43
43
|
from .vfs import GitTreeFS, RealFS, VfsError, git_toplevel
|
|
44
44
|
|
|
@@ -113,6 +113,17 @@ def check(
|
|
|
113
113
|
exit_zero: Annotated[
|
|
114
114
|
bool, typer.Option("--exit-zero", help="Exit 0 even when findings exist.")
|
|
115
115
|
] = False,
|
|
116
|
+
warnings_as_errors: Annotated[
|
|
117
|
+
bool,
|
|
118
|
+
typer.Option("--warnings-as-errors", help="Exit 1 on warnings as well as errors."),
|
|
119
|
+
] = False,
|
|
120
|
+
allow_empty: Annotated[
|
|
121
|
+
bool,
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--allow-empty",
|
|
124
|
+
help="Pass when the baseline has no interfaces (e.g. before any exist).",
|
|
125
|
+
),
|
|
126
|
+
] = False,
|
|
116
127
|
exclude: Annotated[
|
|
117
128
|
list[str] | None,
|
|
118
129
|
typer.Option("--exclude", help="Glob of packages/interfaces to skip."),
|
|
@@ -127,7 +138,7 @@ def check(
|
|
|
127
138
|
raise typer.Exit(code=2)
|
|
128
139
|
|
|
129
140
|
try:
|
|
130
|
-
findings, new_source, old_source, prefix = _compute(
|
|
141
|
+
findings, new_source, old_source, prefix, files, cov = _compute(
|
|
131
142
|
target, lambda root: resolve_baseline(against, root), level, config_path, exclude, deep
|
|
132
143
|
)
|
|
133
144
|
except (VfsError, ParseError, ConfigError) as exc:
|
|
@@ -139,8 +150,12 @@ def check(
|
|
|
139
150
|
f"[dim]note: {against!r} matched a git ref; "
|
|
140
151
|
f"use './{against}' to compare against the directory instead.[/dim]"
|
|
141
152
|
)
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
if not allow_empty:
|
|
154
|
+
_require_baseline(
|
|
155
|
+
cov, f"the baseline ({old_source})", "pass --allow-empty if that is expected"
|
|
156
|
+
)
|
|
157
|
+
_emit(findings, fmt, new_source, old_source, cov, prefix, files)
|
|
158
|
+
_exit_for(findings, exit_zero, warnings_as_errors)
|
|
144
159
|
|
|
145
160
|
|
|
146
161
|
@app.command(name="bag-check")
|
|
@@ -170,6 +185,10 @@ def bag_check(
|
|
|
170
185
|
exit_zero: Annotated[
|
|
171
186
|
bool, typer.Option("--exit-zero", help="Exit 0 even when findings exist.")
|
|
172
187
|
] = False,
|
|
188
|
+
warnings_as_errors: Annotated[
|
|
189
|
+
bool,
|
|
190
|
+
typer.Option("--warnings-as-errors", help="Exit 1 on warnings as well as errors."),
|
|
191
|
+
] = False,
|
|
173
192
|
exclude: Annotated[
|
|
174
193
|
list[str] | None,
|
|
175
194
|
typer.Option("--exclude", help="Glob of packages/interfaces to skip."),
|
|
@@ -208,8 +227,11 @@ def bag_check(
|
|
|
208
227
|
findings = finalize_findings(
|
|
209
228
|
evaluate(diff_interfaces(old_ifaces, new_ifaces)), config, level, BAG_DEFAULT_LEVEL
|
|
210
229
|
)
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
cov = coverage(old_ifaces, new_ifaces)
|
|
231
|
+
_require_baseline(cov, "the bag", "check the --exclude patterns")
|
|
232
|
+
files = {key: iface.source_file for key, iface in new_ifaces.items()}
|
|
233
|
+
_emit(findings, fmt, new_source, old_res.source, cov, interface_files=files)
|
|
234
|
+
_exit_for(findings, exit_zero, warnings_as_errors)
|
|
213
235
|
|
|
214
236
|
|
|
215
237
|
def _resolve_current(
|
|
@@ -263,8 +285,12 @@ def _compute(
|
|
|
263
285
|
config_path: Path | None,
|
|
264
286
|
exclude: list[str] | None,
|
|
265
287
|
deep: bool | None = None,
|
|
266
|
-
) -> tuple[list[Finding], str, str, str]:
|
|
267
|
-
"""Resolve both sides, diff and finalize; the shared check pipeline.
|
|
288
|
+
) -> tuple[list[Finding], str, str, str, dict[str, str], Coverage]:
|
|
289
|
+
"""Resolve both sides, diff and finalize; the shared check pipeline.
|
|
290
|
+
|
|
291
|
+
Also returns each current interface's source file, keyed by interface, and
|
|
292
|
+
how many interfaces were compared.
|
|
293
|
+
"""
|
|
268
294
|
new_res = _resolve_target(target, allow_empty_glob=True)
|
|
269
295
|
if not isinstance(new_res.vfs, RealFS):
|
|
270
296
|
raise VfsError("target must be a directory or workspace")
|
|
@@ -305,7 +331,9 @@ def _compute(
|
|
|
305
331
|
findings += finalize_findings(
|
|
306
332
|
[f for f in transitive if f.package in deep_packages], config, level
|
|
307
333
|
)
|
|
308
|
-
|
|
334
|
+
files = {key: iface.source_file for key, iface in new_ifaces.items()}
|
|
335
|
+
cov = coverage(old_ifaces, new_ifaces)
|
|
336
|
+
return findings, new_res.source, old_res.source, prefix, files, cov
|
|
309
337
|
|
|
310
338
|
|
|
311
339
|
def _note_skipped_schemas(res: Resolution) -> None:
|
|
@@ -326,11 +354,26 @@ def _display_prefix(target_dir: Path) -> str:
|
|
|
326
354
|
return "" if rel == "." else rel
|
|
327
355
|
|
|
328
356
|
|
|
329
|
-
def
|
|
357
|
+
def _require_baseline(cov: Coverage, baseline: str, hint: str) -> None:
|
|
358
|
+
"""Exit 2 when the baseline is empty, naming ``baseline`` and how to resolve it.
|
|
359
|
+
|
|
360
|
+
With no baseline interfaces every current one looks added, which is never a
|
|
361
|
+
finding, so the check would pass without comparing anything.
|
|
362
|
+
"""
|
|
363
|
+
if cov.baseline_empty:
|
|
364
|
+
err_console.print(
|
|
365
|
+
f"[red]error:[/red] {baseline} has no interfaces, so nothing was compared; {hint}"
|
|
366
|
+
)
|
|
367
|
+
raise typer.Exit(code=2)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _exit_for(findings: list[Finding], exit_zero: bool, warnings_as_errors: bool) -> None:
|
|
371
|
+
"""Exit 1 if an unsuppressed finding fails the check, else 0."""
|
|
330
372
|
if exit_zero:
|
|
331
373
|
raise typer.Exit(code=0)
|
|
332
|
-
|
|
333
|
-
|
|
374
|
+
failing = {"error", "warn"} if warnings_as_errors else {"error"}
|
|
375
|
+
has_failure = any(f.severity in failing and not f.suppressed for f in findings)
|
|
376
|
+
raise typer.Exit(code=1 if has_failure else 0)
|
|
334
377
|
|
|
335
378
|
|
|
336
379
|
def _emit(
|
|
@@ -338,15 +381,18 @@ def _emit(
|
|
|
338
381
|
fmt: str,
|
|
339
382
|
new_source: str,
|
|
340
383
|
old_source: str,
|
|
384
|
+
cov: Coverage,
|
|
341
385
|
display_prefix: str = "",
|
|
386
|
+
interface_files: dict[str, str] | None = None,
|
|
342
387
|
) -> None:
|
|
343
388
|
if fmt == "json":
|
|
344
|
-
typer.echo(json.dumps(build_json(findings), indent=2))
|
|
389
|
+
typer.echo(json.dumps(build_json(findings, coverage=cov), indent=2))
|
|
345
390
|
elif fmt == "github":
|
|
346
|
-
|
|
391
|
+
typer.echo(cov.describe())
|
|
392
|
+
for line in build_github(findings, display_prefix, interface_files):
|
|
347
393
|
typer.echo(line)
|
|
348
394
|
else:
|
|
349
|
-
render_text(findings, console, new_source, old_source)
|
|
395
|
+
render_text(findings, console, new_source, old_source, cov)
|
|
350
396
|
|
|
351
397
|
|
|
352
398
|
@app.command()
|
|
@@ -372,6 +418,10 @@ def lint(
|
|
|
372
418
|
exit_zero: Annotated[
|
|
373
419
|
bool, typer.Option("--exit-zero", help="Exit 0 even when findings exist.")
|
|
374
420
|
] = False,
|
|
421
|
+
warnings_as_errors: Annotated[
|
|
422
|
+
bool,
|
|
423
|
+
typer.Option("--warnings-as-errors", help="Exit 1 on warnings as well as errors."),
|
|
424
|
+
] = False,
|
|
375
425
|
exclude: Annotated[
|
|
376
426
|
list[str] | None,
|
|
377
427
|
typer.Option("--exclude", help="Glob of packages/interfaces to skip."),
|
|
@@ -402,10 +452,7 @@ def lint(
|
|
|
402
452
|
else:
|
|
403
453
|
render_lint(findings, console, res.source)
|
|
404
454
|
|
|
405
|
-
|
|
406
|
-
raise typer.Exit(code=0)
|
|
407
|
-
has_error = any(f.severity == "error" and not f.suppressed for f in findings)
|
|
408
|
-
raise typer.Exit(code=1 if has_error else 0)
|
|
455
|
+
_exit_for(findings, exit_zero, warnings_as_errors)
|
|
409
456
|
|
|
410
457
|
|
|
411
458
|
def _parse_rule_overrides(items: list[str]) -> dict[str, str]:
|
|
@@ -4,13 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from .github import build_github
|
|
6
6
|
from .json_report import build_json
|
|
7
|
-
from .summary import Summary, summarize
|
|
7
|
+
from .summary import Coverage, Summary, coverage, summarize
|
|
8
8
|
from .text import render_lint, render_text
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
11
|
+
"Coverage",
|
|
11
12
|
"Summary",
|
|
12
13
|
"build_github",
|
|
13
14
|
"build_json",
|
|
15
|
+
"coverage",
|
|
14
16
|
"render_lint",
|
|
15
17
|
"render_text",
|
|
16
18
|
"summarize",
|
|
@@ -2,26 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
5
7
|
from ..rules import Finding
|
|
6
8
|
|
|
7
9
|
_LEVEL = {"error": "error", "warn": "warning"}
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
def build_github(
|
|
12
|
+
def build_github(
|
|
13
|
+
findings: list[Finding],
|
|
14
|
+
display_prefix: str = "",
|
|
15
|
+
interface_files: Mapping[str, str] | None = None,
|
|
16
|
+
) -> list[str]:
|
|
11
17
|
"""Build ``::error``/``::warning`` annotation lines for non-suppressed findings.
|
|
12
18
|
|
|
13
19
|
Uses the new-side file/line so annotations land on the changed code.
|
|
14
20
|
``display_prefix`` is prepended to file paths so they are repo-root-relative
|
|
15
21
|
(GitHub resolves annotation paths from the repo root, not the check target).
|
|
22
|
+
A finding with no new-side location (e.g. a removed field) is attached to its
|
|
23
|
+
interface's file from ``interface_files``; GitHub otherwise shows it against
|
|
24
|
+
the workflow file.
|
|
16
25
|
"""
|
|
26
|
+
files = interface_files or {}
|
|
17
27
|
lines: list[str] = []
|
|
18
28
|
for finding in findings:
|
|
19
29
|
if finding.suppressed:
|
|
20
30
|
continue
|
|
21
31
|
command = _LEVEL.get(finding.severity, "notice")
|
|
22
32
|
props = [f"title={_escape_property(finding.rule)}"]
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
file = finding.new_file or files.get(finding.interface)
|
|
34
|
+
if file:
|
|
35
|
+
path = f"{display_prefix}/{file}" if display_prefix else file
|
|
25
36
|
props.insert(0, f"file={_escape_property(path)}")
|
|
26
37
|
if finding.new_line is not None:
|
|
27
38
|
props.insert(1, f"line={finding.new_line}")
|