feat(cli): add conduit pipelines validate#2574
Merged
Merged
Conversation
Offline, static verification of pipeline config files: parse -> enrich -> validate over pkg/provisioning/config, reusing the same machinery `conduit run`'s provisioning path already uses. Ships as the first of three verbs (validate|lint|dry-run) sharing one engine (cmd/conduit/internal/validate); lint and dry-run follow in a later PR. Per docs/design-documents/20260707-cli-pipeline-validate.md and the shared 20260707-cli-output-conventions.md contract: - `conduit pipelines validate <path>` (alias `conduit pipeline validate`), built on cecdysis.CommandWithResult for the shared --json envelope. - Collects every finding across every resolved file (no fail-fast); each Finding carries code, configPath, message, and suggestion. - Directory input aggregates all .yml/.yaml files (not recursed), showing passing files too; `-q/--quiet` suppresses passing/progress lines only. - Exit 0 clean, 2 on any finding, via pkg/conduit/exitcode. Review-flagged must-fixes applied: - Walks config.Validate's and the parser's raw cerrors.Join with cerrors.ForEach directly, never after an outer "%w" wrap (a re-wrap collapses N findings into one — see TestValidateFile_ForEachUnwrappedJoin_YieldsAllFindings). - Cross-file/-document duplicate pipeline ID detection is new engine code (checkCrossFileDuplicateIDs), not a reuse of provisioning.Service.findDuplicateIDs's bare sentinel — it carries a real code (new provisioning.CodePipelineIDDuplicate) and a configPath. - Populated Suggestion on every pkg/provisioning/config validator (fieldError gained a suggestion parameter); it was previously always empty. - Enrich runs before Validate, matching service.go's provisioning order. Supporting refactor: extracted config.ResolveFiles (+ YAMLFilesInDir, IsYAMLFile) as the single file/directory resolution shared by provisioning.Service and the new offline command, replacing Service's private, duplicate implementation. Additive change to cecdysis: Outcome gained an ExitErr field so a command whose Outcome is OK:false (a domain failure, not a hard command failure — Error stays null in the envelope per CLI output conventions §1) can still give the process a correctly classified nonzero exit code. This was needed because CommandWithResultDecorator's success path always returned nil before this change, which would have made `validate`'s exit code always 0 regardless of findings. Self-review: re-read the diff for the masked-bug risk the design review flagged (ForEach over a re-wrapped Join collapsing findings) and added a dedicated regression test proving both directions. Verified end-to-end with the built binary (single file, directory, cross-file duplicate ID, --json, --quiet) to confirm exit codes and rendered output match the acceptance criteria, not just the unit tests. Confirmed via `go build` grep that no cmd/conduit/internal/validate or pipelines/validate.go code references an API client — the offline invariant is structural, not just tested. Tier 2. Roadmap: execution plan §3 (CLI as product). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tapg9dLWXKMZJoL65R24vd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
conduit pipelines validate <path>(aliasconduit pipeline validate) perdocs/design-documents/20260707-cli-pipeline-validate.md, built on the shared v0.17"CLI as product" foundations merged in #2572 (
cecdysis.CommandWithResult,cmd/conduit/internal/ui,pkg/conduit/exitcode) and the contract indocs/design-documents/20260707-cli-output-conventions.md.parse -> enrich -> validateoverpkg/provisioning/config— the same machineryconduit run's provisioning path uses, never dials the API.cmd/conduit/internal/validate(Run, oneReporttype both the humanrenderer and
--jsonmarshal from) — designed solint/dry-runcan reuse it in a follow-upPR, per the design doc's shipping order.
PipelinesCommand(pipeline/pipelinesalias already in place).code+configPath+message+suggestion..yml/.yamlfile (not recursed), showing passing files too(
✓ <file>).-q/--quietsuppresses passing/progress lines only, not failures or the summary.pkg/conduit/exitcode. No--strict(validate iserrors-only, per the conventions doc — that flag belongs to
lint/dry-run).Review-flagged must-fixes (from the design doc's review outcome)
cerrors.ForEachon the unwrappedcerrors.Join.config.Validateand the yaml parserboth already return a raw
cerrors.Join(...); the engine walks that directly, never afterre-wrapping it with
%w(which would collapse N findings into one — this was called out asthe top masked-bug risk). See
TestValidateFile_ForEachUnwrappedJoin_YieldsAllFindings, whichproves both directions.
(
checkCrossFileDuplicateIDs), not a reuse ofprovisioning.Service.findDuplicateIDs's baresentinel (
ErrDuplicatedPipelineID) — that method is a same-process, flattened, skip-and-continuecheck suited to provisioning's recovery path. The new engine code needed a real, located
finding, so it's new: a new
provisioning.CodePipelineIDDuplicatecode + aconfigPathintothe offending file's own
pipelineslist, reported against every file/document thatcontributes an occurrence.
Suggestionin everypkg/provisioning/configvalidator —fieldErrortook nosuggestion before this PR and always shipped one empty. Additive; the connector-missing-plugin
and connector-id-duplicate suggestions match the design doc's own FAIL-output example verbatim
(locked in by a dedicated test).
service.go's provisioning order (provisionPipeline).Supporting refactor
Extracted
config.ResolveFiles(+YAMLFilesInDir,IsYAMLFile) as the singlefile/directory-resolution implementation shared by
pkg/provisioning.Service(config-fileprovisioning at startup) and this new offline command —
Service.getPipelineConfigFilesnowdelegates to it instead of carrying its own private copy. Existing
provisioningtests(
TestService_getYamlFiles*,TestService_Init_Delete) still pass unmodified.Additive change to
cecdysiscecdysis.Outcomegained anExitErrfield, andCommandWithResultDecorator's success pathnow returns it instead of unconditionally
nil. This was necessary, not optional: per the CLIoutput conventions, a validation run that finds problems is
ok:false/error:null— the runitself succeeded, so
ExecuteWithResultmust return a nil error (that's the documented contractfor "domain failure" vs. "hard command failure"). But
cmd/conduit/cliderives the process exitcode entirely from
cmd.Execute()'s returned error, and the pre-existing decorator's successpath always returned
nilregardless ofOutcome.OK— so without this change,validatecouldnever exit non-zero on a finding.
ExitErris returned after the envelope/human output isalready written, is never itself rendered, and is nil (today's default behavior, unchanged) for
every other command using this decorator. Covered by three new tests in
cecdysis/result_test.go(JSON path, human path, and the nil-ExitErr default-unaffected case).
Adversarial self-review
validateFileinengine.gocallscerrors.ForEachdirectly onparser.Parse's andconfig.Validate's returnvalues, with no
cerrors.Errorf("...: %w", err)wrap anywhere in between. Added the dedicatedregression test rather than trusting a read-through alone.
with 5 field errors, a directory with 2 passing + 1 failing file, two files sharing a pipeline
ID,
--json,--quiet— confirmed exit codes (0/2) and rendered output match by hand beforetrusting the test suite.
cmd/conduit/internal/validateandcmd/conduit/root/pipelines/validate.gofor anyapi.Client/gRPC dial — none exists; the onlygrpcimport iscodes(for classificationconstants), not a client.
ValidateCommand.Render(c.rendereris set inExecuteWithResult, called beforeRenderby the same decorator) — unreachable in the realcommand pipeline, but hardened with a defensive fallback anyway in case a future test calls
Renderdirectly.--jsonoutput:findingswasserializing as JSON
null(not[]) for a passing file, which would make a naive--jsonconsumer iterating
result.files[].findingsneed to special-casenull. Now always[]whenempty.
%vslice ([a.yaml b.yaml]);fixed to a comma-joined string before finalizing.
Test plan
go build ./...go test ./cmd/conduit/... ./pkg/provisioning/... -race -count=1— greengolangci-lint run ./cmd/... ./pkg/provisioning/...— 0 issuesTestValidateFile_ForEachUnwrappedJoin_YieldsAllFindings— proves the ForEach-on-unwrapped-Joininvariant both ways (raw Join → all findings; re-wrapped Join → collapses to 1, documenting
exactly the bug this guards against)
TestRun_CrossFileDuplicateID/TestRun_SameFileDuplicateID— two files, and two documentsin one file, sharing a pipeline ID both get the dup-ID code
TestRun_Offline_NoDial+ structural grep confirmation — no API client anywhere in the callgraph
TestValidateCommand_FileWithErrors_JSON— all 5 findings present in--json, each withnon-empty code/configPath/message/suggestion,
code== a registeredconduiterrreasonTestValidateCommand_Directory_ShowsPassingFiles/_Quiet_SuppressesPassingLinesTestValidateCommand_NonexistentPath_HardFailure— unresolvable path is a HARD failure(envelope
errorset), still classified as exit 2cmd/conduit/internal/validate/testdata/Tier 2. Roadmap: execution plan §3 (CLI as product).
lint/dry-runfollow in a second PR perthe design doc's shipping order.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Tapg9dLWXKMZJoL65R24vd