From c0e0010683af9f71307d22668a497efacf6d027b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 03:20:17 +0000 Subject: [PATCH] Migrate ConfigKit reports to the reporter --- docs/roadmap/implementation.md | 14 +++- integrations/ckconfig/diff.go | 39 ++++++----- integrations/ckconfig/diff_execution_test.go | 52 ++++++++++++--- integrations/ckconfig/explain.go | 70 ++++++++------------ integrations/ckconfig/family.go | 45 ++++--------- integrations/ckconfig/family_reports_test.go | 32 ++++++++- integrations/ckconfig/show.go | 4 +- integrations/ckconfig/sources.go | 45 +++++++------ integrations/ckconfig/validate.go | 5 +- 9 files changed, 177 insertions(+), 129 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4cd829f..5044443 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Invocation reporting implementation plan -Status: Stages 1–11 complete. Stage 12 is next. +Status: Stages 1–12 complete. Stage 13 is next. ## Objective and authority @@ -460,7 +460,7 @@ assembled suites. ## Stage 12: Migrate configuration sources, explain, and diff -Status: pending. +Status: complete. Primary files: [sources.go](../../integrations/ckconfig/sources.go), [explain.go](../../integrations/ckconfig/explain.go), @@ -484,6 +484,16 @@ Validation: retain comparison preflight, selector, safe-report, and native forwarding tests. Cover a representative failure on each destination and quiet rendering without duplicating core's complete visibility matrix in every command. +Configuration sources, explanation, diff, and scope inventory now submit +provider messages through the shared reporter and render through managed output. +Native explanation rendering retains already-written partial output, while +renderer and destination failures are recorded in `Presentation` independently +of operation causes. Focused ckconfig package tests pass, including comparison +preflight, source evidence, safe reports, native forwarding, warning/context +ordering, quiet rendering, and result/diagnostic destination failures. The full +ckconfig module command still fails to compile `examples/simple`, which uses the +old invocation API and is scheduled for migration in Stage 17. + ## Stage 13: Migrate PromptKit acquisition adapters Status: pending. diff --git a/integrations/ckconfig/diff.go b/integrations/ckconfig/diff.go index 639b6b6..7168919 100644 --- a/integrations/ckconfig/diff.go +++ b/integrations/ckconfig/diff.go @@ -3,41 +3,44 @@ package ckconfig import ( "errors" "fmt" + "io" commandkit "gitea.maximumdirect.net/eric/CommandKit" ) func runConfigDiff[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome { if plan.comparison == nil || plan.comparison.scope == nil { - return configOperationFailure(errors.New("configuration comparison was not prepared"), nil, false) + cause := errors.New("configuration comparison was not prepared") + return configFailure("Configuration comparison was not prepared.", commandkit.Explanation{}, cause) } selected := plan.comparison if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, nil, false) + return configFailure("Configuration comparison was canceled.", commandkit.Explanation{}, err) } compare := selected.scope.scope.Compare if compare == nil { - return configOperationFailure(fmt.Errorf("configuration scope %q has no comparison callback", selected.scope.scope.Name), nil, false) + cause := fmt.Errorf("configuration scope %q has no comparison callback", selected.scope.scope.Name) + return configFailure("Configuration scope does not support comparison.", commandkit.Explanation{}, cause) } acquired, err := compare(invocation.Context, selected.request, state) - diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...) + reporter := invocation.Reporter() + queueConfigDiagnostics(reporter, acquired.Diagnostics) if err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure(fmt.Sprintf("Unable to compare configuration scope %q.", selected.scope.scope.Name), acquired.Explanation, err) } if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure("Configuration comparison was canceled.", acquired.Explanation, err) } - report, err := renderDiffReport(acquired.Value) - if err != nil { - return configOperationFailure(err, diagnostics, false) + err = reporter.RenderOutput(func(writer io.Writer) error { + report, err := renderDiffReport(acquired.Value) + if err != nil { + return err + } + _, err = writer.Write(report) + return err + }) + if err != nil && invocation.Context.Err() != nil { + return configFailure("Configuration comparison was canceled.", acquired.Explanation, invocation.Context.Err()) } - if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) - } - if err := writeConfigReport(invocation.Streams.Out, report); err != nil { - return configDeliveryFailure(err, diagnostics, true) - } - outcome := commandkit.Success() - outcome.Diagnostics = diagnostics - return outcome + return commandkit.Success() } diff --git a/integrations/ckconfig/diff_execution_test.go b/integrations/ckconfig/diff_execution_test.go index 37b4a8e..3ee45d7 100644 --- a/integrations/ckconfig/diff_execution_test.go +++ b/integrations/ckconfig/diff_execution_test.go @@ -101,21 +101,30 @@ func TestConfigDiffRunsComparisonOnlyScopeAndRejectsUnsupportedScopeBeforeWork(t func TestConfigDiffProviderFailureRetainsDiagnosticsAndCause(t *testing.T) { wantErr := errors.New("provider unavailable") - diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error { + diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(writer io.Writer) error { _, err := io.WriteString(writer, "provider warning\n") return err }} app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{ Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { - return commandkit.Acquired[[]configkit.Difference]{Diagnostics: []commandkit.Diagnostic{diagnostic}}, fmt.Errorf("comparison setup: %w", wantErr) + return commandkit.Acquired[[]configkit.Difference]{ + Diagnostics: []commandkit.Diagnostic{diagnostic}, + Explanation: commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error { + _, err := io.WriteString(writer, "native comparison context\n") + return err + }}}, + }, fmt.Errorf("comparison setup: %w", wantErr) }, }}})) - var output bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, wantErr) || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityError { + var output, diagnosticOutput bytes.Buffer + outcome := app.Execute(context.Background(), []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnosticOutput}) + if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, wantErr) || output.Len() != 0 { t.Fatalf("provider failure outcome/output = %#v / %q", outcome, output.String()) } + if got := diagnosticOutput.String(); !strings.Contains(got, "provider warning") || !strings.Contains(got, "native comparison context") || strings.Index(got, "provider warning") > strings.Index(got, "native comparison context") || strings.Contains(got, wantErr.Error()) { + t.Fatalf("provider messages and terminal context = %q", got) + } } func TestConfigDiffReportPreparationFailureWritesNoReport(t *testing.T) { @@ -128,8 +137,8 @@ func TestConfigDiffReportPreparationFailureWritesNoReport(t *testing.T) { }, }}})) var output bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 { + outcome := app.Execute(context.Background(), []string{"config", "diff", "--quiet"}, commandkit.Streams{Out: failingOutput{err: errors.New("quiet destination should not be called")}, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete || outcome.Presentation.Output.RenderError == nil || output.Len() != 0 { t.Fatalf("report preparation failure outcome/output = %#v / %q", outcome, output.String()) } } @@ -146,8 +155,8 @@ func TestConfigDiffCancellationAfterAcquisitionRetainsDiagnosticsWithoutWriting( }}})) defer cancel() var output bytes.Buffer - outcome := app.Execute(ctx, []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) || outcome.ReportIncomplete || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityWarning { + outcome := app.Execute(ctx, []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) || output.Len() != 0 { t.Fatalf("cancelled comparison outcome/output = %#v / %q", outcome, output.String()) } } @@ -170,13 +179,36 @@ func TestConfigDiffOutputErrorsCarryIncompleteDeliveryOutcome(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: test.writer, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.DeliveryError, test.want) || !errors.Is(outcome.Cause, test.want) || !outcome.ReportIncomplete { + if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.WriteError, test.want) || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete { t.Fatalf("delivery failure outcome = %#v", outcome) } }) } } +func TestConfigDiffDiagnosticDestinationFailureIsPresentationEvidence(t *testing.T) { + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{ + Name: "app", Operations: []Operation{Diff}, + Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) { + return commandkit.Acquired[[]configkit.Difference]{ + Diagnostics: []commandkit.Diagnostic{{Severity: commandkit.SeverityWarning, Write: func(writer io.Writer) error { + _, err := io.WriteString(writer, "provider warning\n") + return err + }}}, + }, nil + }, + }}})) + writeErr := errors.New("diagnostic destination failed") + var output bytes.Buffer + outcome := app.Execute(context.Background(), []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: failingOutput{err: writeErr}}) + if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Diagnostics.WriteError, writeErr) || outcome.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete { + t.Fatalf("diagnostic delivery failure outcome = %#v", outcome) + } + if output.Len() == 0 { + t.Fatal("healthy result destination did not receive the diff report") + } +} + func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) { type input struct { ID int diff --git a/integrations/ckconfig/explain.go b/integrations/ckconfig/explain.go index b1c4e9d..db70000 100644 --- a/integrations/ckconfig/explain.go +++ b/integrations/ckconfig/explain.go @@ -2,7 +2,6 @@ package ckconfig import ( "bytes" - "errors" "fmt" "io" @@ -54,70 +53,59 @@ type secretReferenceDTO struct { func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome { selected := plan.scopes[0] name := selected.scope.scope.Name + reporter := invocation.Reporter() if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, nil, false) + return configFailure("Configuration explanation was canceled.", commandkit.Explanation{}, err) } if selected.scope.scope.Acquire == nil { - return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false) + cause := fmt.Errorf("configuration scope %q has no acquisition callback", name) + return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), commandkit.Explanation{}, cause) } acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{ Operation: Explain, Selection: plan.selection, }, selected.input, state) - diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...) + queueConfigDiagnostics(reporter, acquired.Diagnostics) if acquireErr != nil { - return configOperationFailure(acquireErr, diagnostics, false) + return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr) } if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure("Configuration explanation was canceled.", acquired.Explanation, err) } if acquired.Value.Explanation == nil { - return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false) + cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name) + return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause) } explanation, found := acquired.Value.Explanation.Explain(plan.path) if !found { - return configOperationFailure(fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name), diagnostics, false) + cause := fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name) + return configFailure("The requested configuration path is not available.", acquired.Explanation, cause) } if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure("Configuration explanation was canceled.", acquired.Explanation, err) } if plan.format != "yaml" { - return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, plan.path, diagnostics) - } - var report bytes.Buffer - if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil { - return configOperationFailure(err, diagnostics, false) - } - if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) - } - if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil { - return configDeliveryFailure(err, diagnostics, true) - } - outcome := commandkit.Success() - outcome.Diagnostics = diagnostics - return outcome -} - -func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []commandkit.Diagnostic) commandkit.Outcome { - checked := &checkedWriter{writer: writer} - renderErr := reader.WriteExplanation(checked, path) - if checked.failure != nil { - if renderErr == nil { - renderErr = checked.failure - } else { - renderErr = errors.Join(renderErr, checked.failure) + err := reporter.RenderOutput(func(writer io.Writer) error { + return acquired.Value.Explanation.WriteExplanation(writer, plan.path) + }) + if err != nil && invocation.Context.Err() != nil { + return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err()) } + return commandkit.Success() } - if renderErr != nil { - outcome := configOperationFailure(renderErr, diagnostics, true) - outcome.DeliveryError = checked.failure - return outcome + err := reporter.RenderOutput(func(writer io.Writer) error { + var report bytes.Buffer + if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil { + return err + } + _, err := writer.Write(report.Bytes()) + return err + }) + if err != nil && invocation.Context.Err() != nil { + return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err()) } - outcome := commandkit.Success() - outcome.Diagnostics = diagnostics - return outcome + return commandkit.Success() } func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport { diff --git a/integrations/ckconfig/family.go b/integrations/ckconfig/family.go index a6444e3..fa23679 100644 --- a/integrations/ckconfig/family.go +++ b/integrations/ckconfig/family.go @@ -1,6 +1,7 @@ package ckconfig import ( + "bytes" "context" "errors" "fmt" @@ -10,7 +11,6 @@ import ( commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" - "go.yaml.in/yaml/v4" ) type scopeMetadata struct { @@ -265,19 +265,18 @@ func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit. )}, CheckInput: func(input scopeInventoryInput) error { return checkConfigFormat(input.Format) }, Run: func(invocation commandkit.Invocation, input scopeInventoryInput) commandkit.Outcome { - var output []byte - var err error - if input.Format == "yaml" { - output, err = yaml.Marshal(prepared.inventory) - } else { - output = []byte(formatScopeInventory(prepared.inventory)) - } - if err != nil { - return commandkit.Failure(err) - } - if err := writeConfigReport(invocation.Streams.Out, output); err != nil { - return incompleteConfigReport(err) - } + _ = invocation.Reporter().RenderOutput(func(writer io.Writer) error { + var report bytes.Buffer + if input.Format == "yaml" { + if err := encodeYAML(&report, prepared.inventory); err != nil { + return err + } + } else { + _, _ = report.WriteString(formatScopeInventory(prepared.inventory)) + } + _, err := writer.Write(report.Bytes()) + return err + }) return commandkit.Success() }, } @@ -314,21 +313,3 @@ func checkConfigFormat(format string) error { } func parseConfigString(value string) (string, error) { return value, nil } - -func writeConfigReport(writer io.Writer, content []byte) error { - written, err := writer.Write(content) - if err != nil { - return err - } - if written != len(content) { - return io.ErrShortWrite - } - return nil -} - -func incompleteConfigReport(err error) commandkit.Outcome { - outcome := commandkit.Failure(err) - outcome.ReportIncomplete = true - outcome.DeliveryError = err - return outcome -} diff --git a/integrations/ckconfig/family_reports_test.go b/integrations/ckconfig/family_reports_test.go index 5e01998..60eda90 100644 --- a/integrations/ckconfig/family_reports_test.go +++ b/integrations/ckconfig/family_reports_test.go @@ -472,13 +472,28 @@ func TestExplainParsesBeforeAcquisitionAndPreservesNativeSafeHistory(t *testing. } } -func TestReportWriteFailureTracksIncompleteDeliveryAndCancellation(t *testing.T) { +func TestExplainTracksNativeRenderFailureWithoutReplacingOperationCause(t *testing.T) { + renderErr := errors.New("native explanation renderer failed") + reader := &failingNativeExplanation{err: renderErr} + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{ + Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { + return commandkit.Acquired[View]{Value: View{Explanation: reader}}, nil + }, + }}})) + var output bytes.Buffer + outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", "/token"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.RenderError, renderErr) || output.String() != "partial native explanation\n" { + t.Fatalf("native explanation failure outcome/output = %#v / %q", outcome, output.String()) + } +} + +func TestReportWriteFailureIsRecordedByReporterAndPreservesCancellation(t *testing.T) { writeErr := errors.New("output failed") app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { return commandkit.Acquired[View]{Value: View{Sources: &sourceInventory{values: []configkit.SourceInfo{}}}}, nil }}}})) outcome := app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard}) - if !errors.Is(outcome.Cause, writeErr) || outcome.DeliveryError != writeErr || !outcome.ReportIncomplete { + if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete { t.Fatalf("source delivery failure outcome = %#v", outcome) } @@ -531,6 +546,19 @@ type nativeExplanation struct { found bool } +type failingNativeExplanation struct{ err error } + +func (reader *failingNativeExplanation) Explain(configkit.Path) (configkit.Explanation, bool) { + return configkit.Explanation{}, true +} + +func (reader *failingNativeExplanation) WriteExplanation(writer io.Writer, _ configkit.Path) error { + if _, err := io.WriteString(writer, "partial native explanation\n"); err != nil { + return err + } + return reader.err +} + func (reader *nativeExplanation) Explain(path configkit.Path) (configkit.Explanation, bool) { reader.path = path return reader.value, reader.found diff --git a/integrations/ckconfig/show.go b/integrations/ckconfig/show.go index fd17788..3d64b18 100644 --- a/integrations/ckconfig/show.go +++ b/integrations/ckconfig/show.go @@ -70,14 +70,14 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera } framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes()) if index > 0 && hasDirective { - if err := writeConfigReport(writer, []byte("...\n")); err != nil { + if _, err := io.WriteString(writer, "...\n"); err != nil { return err } } if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' { framed = append(framed, '\n') } - if err := writeConfigReport(writer, framed); err != nil { + if _, err := writer.Write(framed); err != nil { return err } } diff --git a/integrations/ckconfig/sources.go b/integrations/ckconfig/sources.go index 50f47d3..ef1b4e6 100644 --- a/integrations/ckconfig/sources.go +++ b/integrations/ckconfig/sources.go @@ -52,25 +52,28 @@ type versionDTO struct { func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome { selected := plan.scopes[0] name := selected.scope.scope.Name + reporter := invocation.Reporter() if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, nil, false) + return configFailure("Configuration source listing was canceled.", commandkit.Explanation{}, err) } if selected.scope.scope.Acquire == nil { - return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false) + cause := fmt.Errorf("configuration scope %q has no acquisition callback", name) + return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), commandkit.Explanation{}, cause) } acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{ Operation: Sources, Selection: plan.selection, }, selected.input, state) - diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...) + queueConfigDiagnostics(reporter, acquired.Diagnostics) if acquireErr != nil { - return configOperationFailure(acquireErr, diagnostics, false) + return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr) } if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure("Configuration source listing was canceled.", acquired.Explanation, err) } if acquired.Value.Sources == nil { - return configOperationFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false) + cause := fmt.Errorf("configuration scope %q did not provide a source reader", name) + return configFailure("Configuration scope did not provide source information.", acquired.Explanation, cause) } sources := acquired.Value.Sources.Sources() @@ -111,23 +114,25 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp } document.Sources = append(document.Sources, item) } - var report bytes.Buffer - if plan.format == "yaml" { - if err := encodeYAML(&report, document); err != nil { - return configOperationFailure(err, diagnostics, false) - } - } else { - writeSourceReportText(&report, document) - } if err := invocation.Context.Err(); err != nil { - return configOperationFailure(err, diagnostics, false) + return configFailure("Configuration source listing was canceled.", acquired.Explanation, err) } - if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil { - return configDeliveryFailure(err, diagnostics, true) + err := reporter.RenderOutput(func(writer io.Writer) error { + var report bytes.Buffer + if plan.format == "yaml" { + if err := encodeYAML(&report, document); err != nil { + return err + } + } else { + writeSourceReportText(&report, document) + } + _, err := writer.Write(report.Bytes()) + return err + }) + if err != nil && invocation.Context.Err() != nil { + return configFailure("Configuration source listing was canceled.", acquired.Explanation, invocation.Context.Err()) } - outcome := commandkit.Success() - outcome.Diagnostics = diagnostics - return outcome + return commandkit.Success() } func writeSourceReportText(writer *bytes.Buffer, document sourceReport) { diff --git a/integrations/ckconfig/validate.go b/integrations/ckconfig/validate.go index fbc21f3..58be8f3 100644 --- a/integrations/ckconfig/validate.go +++ b/integrations/ckconfig/validate.go @@ -39,11 +39,12 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi return err } if plan.format == "yaml" && !writtenRecord { - if err := writeConfigReport(writer, []byte("scopes:\n")); err != nil { + if _, err := io.WriteString(writer, "scopes:\n"); err != nil { return err } } - return writeConfigReport(writer, record) + _, err = writer.Write(record) + return err }) if err != nil { if ctxErr := invocation.Context.Err(); ctxErr != nil {