Repository navigation
fix(coverage): harden exclusion-only descriptors; verify yq without contracts - #16
Conversation
…ontracts Follow-ups to #15: - `contracts:` with no value (null) or an omitted list crashed coverage with a TypeError before reaching the new empty-list handling, in both the yq-version check and baseline_overview. One helper, descriptor_contracts(), now reads spec.contracts everywhere: null or omitted means no contracts, and a non-list is a clear error. - With no contracts there was no yqVersion pin, so the yq binary was not verified. coverage takes --yq-version, which must agree with any contract's yqVersion; the reusable coverage workflow passes the version it installed. Without either, coverage warns that yq was not verified. - The yq warning no longer gets overwritten by the classification step. - Docs: the exclusion-only descriptor carries only the exclusions; CRD globs remain a workflow input / CLI flag. The onboarding skill and checklist describe the shape (contracts: [], CRD and coverage jobs only). - Tests: null and omitted contracts, a non-list contracts value, --yq-version verification and mismatch, the unverified-yq warning, the workflow step passing YQ_VERSION, and `check` refusing a descriptor with no contracts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Edson Tirelli <etirelli@redhat.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
antowaddle
left a comment
There was a problem hiding this comment.
@etirelli Thanks for these changes, added some comments.
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def descriptor_contracts(descriptor: Document) -> list[Any]: |
There was a problem hiding this comment.
descriptor_contracts() accepts omitted and null contracts as [] (modules/coverage/checker.py:515), and the new tests assert those descriptors are complete (modules/coverage/tests/test_coverage.py:359). But schemas/contract-set.schema.json:20 still requires contracts, and lines 27–30 type it as an array. The onboarding guide explicitly says descriptors are validated against that schema. Choose one direction: make contracts: [] the only valid exclusion-only shape and reject omitted/null values, or update the schema to make contracts optional/nullable and align the docs. An omitted key otherwise becomes a silent exclusion-only descriptor rather than a schema error. A schema-validation test would prevent this drifting again.
There was a problem hiding this comment.
Went with the schema: spec.contracts stays required, and contracts: [] is the only exclusion-only shape. descriptor_contracts() now rejects an omitted, blank, null, or non-list value with an integrity error (exit 2) that points at contracts: []. Added modules/coverage/tests/test_schema.py (jsonschema, test-only): it validates the selftest descriptor and the exclusion-only fixture, and asserts the omitted, null and mapping shapes fail. That mirrors the checker tests. Fixed in cb6a9cc.
There was a problem hiding this comment.
A blank spec: still produces a traceback. load_exclusions() assumes spec is a mapping (modules/coverage/checker.py:133). With spec: containing no value, coverage exits 1 with AttributeError: 'NoneType' object has no attribute 'get' and no structured JSON report, instead of the documented integrity-error exit code 2. The new or {} in baseline_overview() does not help because classify_openapi() calls load_exclusions() first. Validate spec once and raise CheckError when it is not a mapping; the same pattern remains in generator_for() and pinned_yq_version(). This is pre-existing, but directly adjacent to the hardening goal.
There was a problem hiding this comment.
Fixed in cb6a9cc. load_descriptor() (shared by check and coverage) now requires spec to be a mapping. Coverage reads it through a new descriptor_spec() helper in load_exclusions, generator_for, baseline_overview and descriptor_contracts, so a blank spec: produces the structured JSON report with exit 2. A blank candidate: was a second crash in generator_for and in the openapi-compat check; both now treat it as absent. Tests cover a blank, null, list and omitted spec for both commands.
| raise CheckError(f"Descriptor pins conflicting yq versions: {sorted(versions)}") | ||
| for version in versions: | ||
| version = pinned_yq_version(descriptor, getattr(args, "yq_version", None)) | ||
| if version: |
There was a problem hiding this comment.
When neither the descriptor nor --yq-version supplies a version, coverage records a warning and exits 0 (modules/coverage/checker.py:558-563). The reusable workflow is safe because it always passes YQ_VERSION, but direct CLI or custom workflow callers can pass with an unverified parser. docs/architecture.md:39-41 says unpinned tools must fail closed. If warning-only behavior is intentional for local use, document that exception explicitly; otherwise make it an integrity error in enforce mode.
There was a problem hiding this comment.
Agreed. I made it fail closed instead of documenting an exception. Every contract must pin policy.yqVersion, so this only arises for contracts: [] without --yq-version. That case is now an integrity error (exit 2) in both enforce and report-only mode. The reusable workflow always passes YQ_VERSION, so CI callers are unaffected. The README and onboarding docs are updated. Fixed in cb6a9cc.
There was a problem hiding this comment.
describes yq-version as “declared by the provider descriptor,” which is not true for exclusion-only descriptors. The generic local command in docs/onboarding/README.md:65 also omits --yq-version; add a note or flag for the exclusion-only case.
There was a problem hiding this comment.
Fixed in cb6a9cc. The input now reads "Exact mikefarah/yq version to install and verify. Must match every contract's policy.yqVersion; it is the only pin for a descriptor with no contracts." The local coverage commands in docs/onboarding/README.md (step 5) and openapi-compat.md now pass --yq-version, with a note that it is required for contracts: [].
…nned yq Address review on #16: - spec.contracts is required, as the schema says. `contracts: []` is the only exclusion-only shape; an omitted, blank, or non-list value is an integrity error (exit 2) instead of a silent exclusion-only descriptor. - spec must be a mapping. load_descriptor checks it once for every command, and coverage reads it through descriptor_spec(), so a blank spec: is a structured error rather than an AttributeError traceback. A blank candidate: no longer crashes either. - With no pinned yq version (no contracts and no --yq-version), coverage fails closed in both modes, per docs/architecture.md, instead of warning. - Add a jsonschema-based test that keeps the schema and the checkers' rules in step (jsonschema is a test-only dependency). - Fix the coverage workflow's yq-version description and document --yq-version in the onboarding guide's local coverage commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Edson Tirelli <etirelli@redhat.com>
… verify yq first Follow-ups from an adversarial review of this PR: - spec.exclusions: null is an integrity error again, as on main and in the schema; an omitted list is still allowed. The previous commit had made a blank exclusions list mean "no exclusions". - A non-mapping spec.baseline or contract policy is a structured exit-2 error instead of an AttributeError traceback (or, for `policy: []`, a misleading "no yq pinned"). - With --yq-version, coverage verifies the yq binary before yq parses the descriptor, then checks the descriptor's pin agrees. The reusable workflow always passes it, so CI never parses with an unverified yq. - The unpinned-yq error says "every contract must declare policy.yqVersion" when the descriptor has contracts, and asks for --yq-version only when it has none. - Wording: `contracts: []` in the onboarding guide, the --yq-version help, and the schema's contracts and yqVersion descriptions. - Tests: blank, non-mapping, and fallback spec.candidate for coverage and check; null and mapping exclusions (checker and schema); non-mapping spec.baseline and policy; verify-before-parse; the descriptor-mismatch test now pins a different version in the descriptor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Edson Tirelli <etirelli@redhat.com>
|
Update since the review replies (bfec70e): A further self-review pass found and fixed some gaps:
The PR description's Compatibility section lists every descriptor shape that behaves differently from |
antowaddle
left a comment
There was a problem hiding this comment.
@etirelli
The four earlier concerns are addressed in bfec70e: spec.contracts is required, blank spec is a structured error, unpinned yq fails closed, and the workflow/docs now pass --yq-version. CI is green.
Two issues remain:
-
[P2] Policy validation is still split between commands. coverage/checker.py:549 skips policy: null. A two-contract probe with one valid pin and one policy: null returned exit 0, despite the schema rejecting it. Meanwhile openapi-compat/checker.py:437 still calls .get() on a null, list, or string policy; check exits 1 with AttributeError and writes no report.json. Validate contract policy once in shared descriptor code and test null/non-mapping policies through both commands.
-
[P2] Falsy non-mapping candidates bypass validation. coverage/checker.py:151 and openapi-compat/checker.py:535 use ... or {}, so candidate: [], "", 0, and false are treated as absent. Both commands exit 0 while jsonschema rejects those descriptors. With contracts: [], even candidate: "bad" is never inspected and coverage exits 0. Check for None explicitly, then require a mapping.
candidate: null and baseline: null also remain accepted at runtime but rejected by the schema. Either make those nullable in the schema or reject them consistently, and extend the parity tests beyond contracts/exclusions/spec.
…coverage Address the second review on #16: shape validation was spread over the code that read each field, and coverage and check read different fields. - Add openapi-compat validate_descriptor(), called from load_descriptor(), so check and coverage reject the same malformed descriptors with the same exit-2 error before doing anything else. It follows the schema's types for spec, spec.contracts and each contract, each contract's policy (a mapping with oasdiffVersion and yqVersion strings) and severityOverrides, and the optional candidate, candidate.generator, spec.baseline, contract baseline, contract generator, and exclusions blocks. A present optional block must have the schema's type: null and other falsy values ([], "", 0, false) are no longer treated as absent. candidate and baseline stay non-nullable, as in the schema. - Remove the per-field guards in both checkers that the validator now covers. check no longer crashes on a null or non-mapping policy, and one valid yq pin no longer hides another contract's `policy: null` in coverage. - Make test_schema.py a table-driven parity test: 80+ single-field mutations of a schema-valid descriptor, asserting jsonschema and validate_descriptor agree. - CLI tests for both commands: falsy and non-mapping candidate, baseline, policy, generator and severityOverrides; missing or unquoted pins; candidate validated with `contracts: []`; check writes report.json for each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Edson Tirelli <etirelli@redhat.com>
|
@antowaddle thanks. Both items are addressed in 4adec37 by validating the descriptor's shape once, instead of field by field.
The validator covers types and structure only. SHA format, reasons, adapter names and exclusion entries stay with the commands, which give more specific errors. The PR description's Compatibility section lists the two shapes |
antowaddle
left a comment
There was a problem hiding this comment.
@etirelli thanks for making those changes, some additional comments:
[P2] matchPath is not validated. matchPath: null still produces a raw TypeError in both commands with no JSON report; matchPath: 1 does the same. This contradicts the statement that both commands reject the same malformed descriptors before doing anything else. A schema-valid but syntactically invalid regex (matchPath: "[") is caught by coverage with exit 2, but check dies with PatternError and no report. Validate the type and regex once, or catch re.error in check.
[P2] flattenAllOf is not validated as a boolean. flattenAllOf: null and "false" pass both commands with exit 0. Null makes policy.get("flattenAllOf", True) return None, so check silently omits --flatten-allof; the schema rejects this descriptor. Add a boolean check and a parity-table case.
[P3] The “same malformed descriptors” guarantee is still broader than the validator. Contract id, type, and declaredArtifact are not shape-checked. For example, both commands accept an omitted or list-valued id with exit 0, although the schema requires a string. The table at test_schema.py:74 covers selected mappings only, so it gives the appearance of full parity without testing these fields.
antowaddle
left a comment
There was a problem hiding this comment.
/lgtm
We can followup on the additional review comments as discussed.
|
@antowaddle thanks, all three are valid. I checked them against We'll fix these in a new PR:
|
What
Follow-ups to #15 (exclusion-only ContractSets), from the review of that PR.
Changes
contracts:with no value no longer crashes coverage. YAML parses it asnull. Coverage readspec.contractsbefore feat(coverage): allow ContractSets with no OpenAPI contracts #15's empty-list handling, in theyqversion check and inbaseline_overview, and died with aTypeError. As the schema requires,contracts: []is the only exclusion-only shape: an omitted, blank, or non-list value is an integrity error (exit 2) that points atcontracts: [].yqis verified even with no contracts. The pinned version lives on each contract'spolicy.yqVersion, so an exclusion-only descriptor pinned nothing and the binary went unchecked.coveragenow accepts--yq-version, which must agree with any contract'syqVersion.yq-versioninput →YQ_VERSION→--yq-version).--yq-version, coverage verifies the binary beforeyqparses the descriptor, then checks the descriptor's pin agrees.docs/architecture.md. Every contract must pinpolicy.yqVersion, so this only applies tocontracts: [].Warnings are kept. The classification step was overwriting
report["warnings"]; it now appends to it.One shape validator for both commands.
validate_descriptor()inmodules/openapi-compat/checker.pyruns insideload_descriptor(), socheckandcoveragereject the same malformed descriptors with the same exit-2 error before doing anything else, instead of crashing or silently accepting them. It follows the schema's types for:spec,spec.contractsand each contractpolicy, which must be a mapping with stringoasdiffVersionandyqVersion, and itsseverityOverridescandidate,candidate.generator,spec.baseline, contractbaseline, contractgeneratorandexclusionsblocksAn optional block may be omitted, but if present it must have the schema's type.
nulland other falsy values ([],"",0,false) are no longer treated as absent. Semantic rules (full Git SHAs, reasons, registered adapters, exclusion entries) stay in the commands, which give more specific errors. The per-field guards in both checkers are gone.Docs fix. An exclusion-only descriptor carries only the exclusions. The CRD globs remain the workflow's
crd-globsinput, or--crd-globlocally; they are not descriptor fields.cmd/contract-tests/README.mddocuments--yq-version.Onboarding skill and checklist. They describe the shape:
contracts: []plus exclusions, CRD and coverage jobs only, and confirming with the owner that no document is a servable API.Tests:
contracts: []is complete;contracts:with no value,null, omitted, or a mapping is an errornull, list, or omittedspec(coverage andcheck)candidate,spec.baseline,policy, contractgenerator/baseline, andseverityOverrides; missing or unquoted pins. Both commands, andcheckwritesreport.jsonfor eachpolicy: null;candidateis validated even withcontracts: []spec.candidate.generator(coverage andcheck)--yq-versionverified before the descriptor is parsedmodules/coverage/tests/test_schema.py: a table-driven parity test. Over 80 single-field mutations of a schema-valid descriptor assert that jsonschema andvalidate_descriptor()agree (jsonschema is test-only)--yq-versionverification and mismatch with the descriptoryqfails closed in enforce and report-onlyYQ_VERSIONcheckrefusing a descriptor with no contractsVerification
All 424 Python tests in the CI directories pass, mypy is clean, and the action-pin check and
actionlintpass.Compatibility
No change for valid descriptors with contracts unless
--yq-versionis passed and disagrees with theiryqVersion. Descriptors that are already invalid against the schema are affected:contractspassed coverage onmain; it must now writecontracts: [], and local runs of it must pass--yq-version.policy.yqVersionpassed coverage onmainwithout verifyingyq. It now fails unless--yq-versionis given.checkalready rejected it.contracts:orspec:, anullor non-mappingpolicy(incheck), or a non-mappingspec.baselinecrashed with a traceback onmain. Each is now a structured exit-2 error.policyis missing, lacksoasdiffVersion, or has an unquoted version passed coverage onmain(checkalready rejected it). It now fails both.candidate:orbaseline:written with no value, or another falsy non-mapping value, is rejected.spec.baseline: nullwas accepted by both commands onmain.candidate: nullcrashed.exclusions: nullwas an error onmainand still is.The workflow always passes the
yq-versioninput it already required, and it installs that same version, so existing callers agree by construction.🤖 Generated with Claude Code