Enforce generated command option validation - #3857
Conversation
|
@claude review |
There was a problem hiding this comment.
Review
This directly implements the fix proposed in #3778 and does it cleanly.
What's good:
ConditionalWeakTable<Type, ValidationMetadata>for the per-type "does this type need validation" cache is the right call — it avoids pinning dynamically-loaded plugin assemblies (a regularDictionary<Type, bool>would leak them), and theRequiresValidationshort-circuit keeps the hot path (options with noValidationAttribute/IValidatableObject) essentially free of reflection cost after the first call per type.- Aggregating all
ValidationResults into oneValidationException(rather than throwing on the first failure) matches the PR's stated goal and is genuinely more useful to a pipeline author who fixes one bad property, reruns, and hits the next one. - Deterministic ordering (
.Order(StringComparer.Ordinal)) on the formatted errors is a nice touch for stable test assertions and reproducible CI logs. - Test coverage is well-targeted: multiple simultaneous violations, the
CliOptionValueRangeAttributewrapper path (viaCliOptionValue), a happy-path smoke test, and a regression test against the actual generatedAnsibleExecuteOptions.Verbose[Range(0,6)]that #3778 called out as unenforced. - I checked the blast radius of the "breaking" change: grepping the whole
src/tree,AnsibleExecuteOptions.Verboseis currently the only generated option with a real validation attribute, and its default (0) is within range — so this won't break existing callers who aren't already passing invalid values. Good, low-risk rollout for something flagged as breaking.
One architectural note (non-blocking): Validator.TryValidateObject only validates top-level properties on the options object — it won't recurse into a complex property's own ValidationAttributes or IValidatableObject implementation. Today that's a non-issue: I traced CliTypeMapper/CliOptionDefinition in the options generator and confirmed generated properties are restricted to primitives, enums, and collections thereof, plus KeyValue/CliOptionValue, neither of which carries any validation logic. But since this PR is explicitly building the enforcement contract other generated options will rely on going forward, it might be worth either a one-line remark in CommandLineOptionsValidator noting the "no recursive validation" limitation, or a guard (e.g. a unit test asserting no nested complex option-value type gets silently skipped) so a future generator change that adds a validated nested type doesn't quietly slip past this validator the same way the original issue's attributes slipped past having no validator at all.
No other correctness, design, or test-gap issues found. Approving from a code standpoint.
|
Required pipeline attempt 1 ended in the known core-suite hang/exit 2 with no deterministic assertion or compile error. Requested the single failed-job rerun. |
|
The single failed-job rerun also ended in unrelated runner/MSBuild process failure: multiple MSBuild child nodes exited prematurely ( |
|
Refreshed onto current main at 79b8c1d. Current-head validation: CommandLineBuilderTests 57/57; Ansible.UnitTests 4/4; core and Ansible Release builds both 0 warnings / 0 errors. Please re-review current head. |
There was a problem hiding this comment.
Re-review (current head 79b8c1d)
Verified this head against the previously reviewed commit (170103a): the diff between them touches only unrelated files pulled in from main via rebase (docs, Azure provisioners, engine/module-executor internals, etc.) — the four files that make up this PR's actual change (CommandLineBuilder.cs, CommandLineOptionsValidator.cs, and the two test files) are byte-for-byte identical to what was already reviewed and approved.
Restating that review's conclusion, which still holds:
What's good:
ConditionalWeakTable<Type, ValidationMetadata>for the per-type "requires validation" cache is the right structure — it avoids pinning dynamically-loaded plugin assemblies (a plainDictionary<Type, bool>would leak them), and theRequiresValidationshort-circuit keeps options with noValidationAttribute/IValidatableObjecteffectively reflection-free after the first call per type.- Aggregating all
ValidationResults into a singleValidationExceptioninstead of throwing on the first failure is more useful in practice — a pipeline author fixing one bad property immediately sees the next one instead of re-running iteratively. - Deterministic ordering (
.Order(StringComparer.Ordinal)) on formatted errors keeps assertions and CI logs stable. - Test coverage is well-targeted: multiple simultaneous violations, the
CliOptionValueRangeAttribute/CliOptionValuewrapper path, a happy-path smoke test, and a regression test against the actual generatedAnsibleExecuteOptions.Verbose[Range(0,6)]that #3778 flagged as unenforced. - Blast radius of the breaking change is low:
AnsibleExecuteOptions.Verboseis the only generated option today with a real validation attribute, and its default (0) is within range, so existing callers not already passing invalid values are unaffected.
Non-blocking architectural note (carried over, still unaddressed by design — and that's fine): Validator.TryValidateObject only validates top-level properties; it won't recurse into a complex property's own attributes or IValidatableObject. That's a non-issue today (generated option properties are restricted to primitives, enums, and KeyValue/CliOptionValue, none of which carry validation logic), but since this establishes the enforcement contract other generated options will build on, consider either a one-line remark on CommandLineOptionsValidator documenting the "no recursive validation" limitation, or a test asserting a nested complex option type isn't silently skipped — so a future generator change doesn't quietly slip past this validator the same way the original gap in #3778 went unnoticed.
Also noting for context (not a concern): src/ModularPipelines/Validation/OptionsValidator.cs is a separate, pre-existing validator for pipeline-level configuration (PipelineOptions) using a different pattern (IOptionsValidator + collected ValidationResult/ValidationErrorCategory). It's unrelated to this PR's per-command-options CommandLineOptionsValidator and there's no overlap in what each validates — just flagging so the similar naming doesn't cause confusion later.
No correctness or test-gap issues found. Approving.
|
Refreshed again onto current main after Ubuntu's core test host ran 16m8s and exited without a failing-test summary. Validation on f13c011: CommandLineBuilderTests 57/57, Ansible tests 4/4, ModularPipelines.slnx and Ansible solution Release builds both 0 warnings/errors. |
f13c011 to
b44c2b0
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe command-line build path now validates options before tool resolution and command construction. Validation errors are deterministic and obfuscated. Command creation failures are traced, and execution counting starts only after successful creation. Source-generator metadata now preserves option properties and handles schema compatibility. ChangesCommand option validation
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🟡 Moderate · up to Conflicting command attributes on inherited options can be silently resolved to the derived definition instead of rejected, which may generate incorrect CLI behavior. Merge should wait for this bounded generator correctness issue to be fixed or explicitly accepted. Sequence Diagram(s)sequenceDiagram
participant Test
participant CommandLineBuilder
participant CommandLineOptionsValidator
participant Command
participant Telemetry
Test->>CommandLineBuilder: Build options
CommandLineBuilder->>CommandLineOptionsValidator: Validate options
CommandLineOptionsValidator-->>CommandLineBuilder: Return valid options or exception
CommandLineBuilder->>Command: Create command
Command->>Telemetry: Record success or obfuscated failure
Command-->>Test: Return command or exception
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
b44c2b0 to
aef09fe
Compare
|
Rebased onto current main and resolved the CommandLineBuilder documentation conflict by preserving both validation-first and phase-aware argument ordering. Current-head validation: CommandLineBuilderTests 62/62; AnsibleOptionsTests 3/3; ModularPipelines.slnx and Ansible solution Release builds both 0 warnings/errors. Core and Ansible changed-file whitespace checks pass; the unit-test project reports one pre-existing whitespace diagnostic at CommandLineBuilderTests.cs:1194, outside this PR's changed lines. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aef09feacc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Code Review — PR #3857
Reviewed the DataAnnotations validation enforcement in CommandLineBuilder.Build. The core approach (cache validation-eligibility per type via ConditionalWeakTable, validate before rendering, aggregate all invalid properties into one message) is sound and well-tested (the new CommandLineBuilderTests and AnsibleOptionsTests cases look thorough). A few issues worth addressing before merge:
1. ValidationException breaks the framework's documented exception contract (medium)
src/ModularPipelines/Context/CommandLineOptionsValidator.cs:42 throws a raw System.ComponentModel.DataAnnotations.ValidationException instead of a ModularPipelines.Exceptions.PipelineException-derived type.
PipelineException.cs explicitly documents itself as "the root exception class from which all ModularPipelines-specific exceptions derive" and shows catch (PipelineException ex) as the way to "handle any pipeline-related error." CommandException and PipelineValidationException already exist in that hierarchy specifically for command/validation failures. Any consumer (or the framework's own module-failure handling) that follows the documented pattern of catching PipelineException will not catch this new failure mode — invalid options (e.g. AnsibleExecuteOptions.Verbose = 7) now surface as an unrecognized BCL exception type instead of the framework's own vocabulary.
Suggestion: wrap/throw a PipelineValidationException (or a new CommandOptionsValidationException : PipelineException) that carries the DataAnnotations ValidationException as InnerException, so callers relying on the documented hierarchy keep working, while the original validation detail is still available.
2. Inline step-numbering comments now disagree with the updated <remarks> doc (low)
src/ModularPipelines/Context/CommandLineBuilder.cs:14 — the class-level XML doc's numbered step list was updated to insert "1. Validate DataAnnotations..." and renumber the rest (2-8), but the inline // 1. Resolve tool name... through // 7. A terminal option must not follow... comments in the method body (lines 50-161) were left as-is. The two numbering schemes are now off by one, so a reader cross-referencing the doc comment against the method body will be looking at the wrong step.
Suggestion: renumber the inline comments to match (2-8), or better, drop numbers from one of the two and let the other be the single source of truth to avoid this drifting again.
3. Validation failures bypass command-level tracing/secret obfuscation (medium)
In src/ModularPipelines/Context/Command.cs, CreateCommand(options, execOpts) (line 69, which triggers CommandLineBuilder.Build → CommandLineOptionsValidator.Validate) runs before ModuleActivityTracing.StartCommandActivity (line 90) and before the try/catch block that applies _secretObfuscator.Obfuscate(exception.Message, execOpts) (line 130). So when validation throws, that specific command gets no command-level Activity/telemetry recorded, and the exception message isn't run through secret obfuscation at this layer (it's only caught and obfuscated later by the outer module-level handler in ModuleRunner, if at all).
Given secrets can appear in option values, and the module's own docs emphasize automatic secret obfuscation in logs as a core guarantee, this is worth closing rather than relying on an outer catch-all.
Suggestion: move the Validate call (or at least wrap it) inside the try block / after the Activity is started, so validation failures get the same tracing and obfuscation treatment as execution failures.
None of these are blockers to the core design — the validation mechanism itself (per-type eligibility caching, aggregating multiple invalid properties into one message, IValidatableObject + ValidationAttribute support) is a good fit for this codebase's generated-options pattern. #1 and #3 are the ones I'd want resolved before merge since they affect error-handling consumers and secret-safety guarantees; #2 is a docs nit.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfd75c7be5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@claude review |
There was a problem hiding this comment.
Code Review — PR #3857 (head a9bdfb3383)
Re-reviewed after the fixes in bfd75c7be5 and a9bdfb3383. Good progress on the issues raised in earlier rounds:
Resolved since last round:
- ✅
ValidationException→CommandOptionsValidationException : PipelineException(with the DataAnnotations exception preserved asInnerException). Consumers following the documented "catchPipelineException" pattern now work correctly. - ✅ Validation failures now get command-level
Activitytracing and secret obfuscation —CreateCommandis wrapped in its own try/catch inCommand.csthat starts an activity and obfuscates the failure message even when validation throws before aCliWrap.Commandexists. Well covered by the newCommand_Validation_Failure_Is_Recorded_And_Obfuscatedtest inTelemetryIntegrationTests.cs, which asserts the secret is obfuscated in the recorded exception message. - ✅ Inline step-numbering comments in
CommandLineBuilder.Buildnow match the<remarks>doc (1–8 in both places). - ✅ Trim/AOT metadata gap (Codex P1): generated types now carry
[DynamicDependency(PublicProperties)], with schema versioning so old (schema-1) packages get rescanned. Covered by new/expandedIncompleteMetadataDiagnosticTests.
Still open — two Codex P2 findings from this round don't look addressed yet:
-
_commandExecutionCounter.Record(...)runs before validation, so rejected commands still get counted (src/ModularPipelines/Context/Command.cs:66).ExecuteCommandLineToolAsyncincrements the counter unconditionally as its first line, then callsCreateCommand(which now runsCommandLineOptionsValidator.Validate) inside a try/catch that only handles tracing. When validation throws, no command is ever built or run, yetCommandCount/ModuleCommandCountsin the persisted run report already reflect an "executed" command that never happened. I checkedRunReportTests.cs— it exercises the counter extensively but has no case for a validation failure, so this regression path is untested and unfixed.Rather than just reordering two lines (which would still couple metrics to argument construction), consider making this hard to regress again: increment the counter from the same success path that produces a
CommandResult/CommandLine(i.e., afterCreateCommandreturns), not as a side effect of entering the method. That keeps "a command was counted" and "a command was actually going to run" as the same invariant instead of two things that happen to be ordered correctly today. -
Reflection-based validation only sees public properties, but the generator legitimately emits
internal/protected internalCLI properties (src/ModularPipelines/Context/CommandLineOptionsValidator.cs:58,RequiresValidation, and theValidator.TryValidateObject(... validateAllProperties: true)call above it).Type.GetProperties()with noBindingFlagsonly returns public members, andValidator.TryValidateObjecthas the same restriction. I confirmed inCommandOptionsGenerator.IsPropertyAccessible(line ~1834) thatAccessibility.InternalandAccessibility.ProtectedOrInternalare explicitly treated as valid, generator-emitted CLI properties — so a[Range]/[Required]on such a property is silently never checked, while the property itself still renders to the command line.This is the more interesting architectural gap: the validator is reflecting over the CLR type from scratch, duplicating work the source generator/
ICommandModelProvideralready did to figure out exactly which properties are real CLI options (including accessibility rules). Rather than a second, independently-scoped reflection pass that can drift from what actually gets rendered, it'd be more robust to validate the same property set theCommandModelProvider/ICommandModelProvideralready resolved for rendering (itsPropertyCommandLineParts carry thePropertyInfo/accessor needed to build aValidationContextper member). That guarantees "what gets validated" and "what gets rendered" can never diverge, and sidesteps needingBindingFlags.NonPublic(which would itself need care under trimming — right now theIL2070/IL2026suppressions are scoped to public-only reflection).
Neither of these blocks the core design — the validation mechanism, exception hierarchy, and trim-safety story are all solid now — but both are real correctness gaps in the enforcement contract this PR is establishing, and neither has a regression test guarding it. I'd want at least #1 addressed before merge since it silently corrupts persisted run-report metrics; #2 is lower risk today (no internal CLI properties currently carry validation attributes) but will bite silently the same way the original #3778 gap did once someone adds one.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9bdfb3383
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea9fddc55d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ea9fddc to
4c02fea
Compare
BREAKING CHANGE: Invalid DataAnnotations values now throw ValidationException before command rendering.
Keep command-schema rescans from revalidating secret metadata that the referenced assembly already generated under the current runtime schema.
f578846 to
cf75385
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Code review (re-verification at
|
Summary
Type.Propertyvalues and cover an actual generated Ansible[Range]This is intentionally breaking: invalid generated option values now throw
ValidationExceptionbefore command rendering.Validation
git diff --check: cleanCloses #3778
Summary by CodeRabbit
New Features
Bug Fixes