Skip to content

feat: machine readable comp diff#270

Merged
jcogilvie merged 12 commits intomainfrom
machine-readable-comp-diff
Apr 1, 2026
Merged

feat: machine readable comp diff#270
jcogilvie merged 12 commits intomainfrom
machine-readable-comp-diff

Conversation

@jcogilvie
Copy link
Copy Markdown
Collaborator

@jcogilvie jcogilvie commented Mar 27, 2026

Description of your changes

Add machine-readable output for comp diff.

I have:

  • Read and followed Crossplane's contribution process.
  • Run earthly -P +reviewable to ensure this PR is ready for review.
  • Added or updated unit tests.
  • Added or updated e2e tests.
  • Documented this change as needed.
    - [ ] Followed the API promotion workflow if this PR introduces, removes, or promotes an API.

Need help with this checklist? See the cheat sheet.

Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
@jcogilvie jcogilvie marked this pull request as ready for review March 27, 2026 18:11
Copilot AI review requested due to automatic review settings March 27, 2026 18:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds machine-readable (JSON/YAML) output for crossplane-diff comp so composition diffs and their downstream XR impacts can be consumed programmatically (e.g., CI/CD).

Changes:

  • Introduces internal + JSON/YAML output types for composition diff results, including XR impact status and downstream changes.
  • Adds human-readable and structured composition diff renderers, plus unit tests covering JSON/YAML schema and formatting helpers.
  • Refactors the composition diff processor to build a structured result and delegate rendering via a new renderer factory based on --output.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/diff/renderer/structured_renderer.go Adds internal comp-diff result model + JSON output schema types and conversion helpers.
cmd/diff/renderer/comp_diff_renderer.go New human/structured comp diff renderers; structured renderer emits JSON/YAML.
cmd/diff/renderer/comp_diff_renderer_test.go New tests for structured comp diff output + formatting helpers.
cmd/diff/diffprocessor/processor_config.go Adds CompDiffRenderer factory and wires it to --output selection.
cmd/diff/diffprocessor/comp_processor.go Refactors comp diff processing to build CompDiffOutput and render via renderer.
cmd/diff/diffprocessor/comp_processor_test.go Updates tests to use constructor wiring and new method signatures.
README.md Documents `--output json

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/diff/renderer/comp_diff_renderer.go
Comment thread cmd/diff/renderer/comp_diff_renderer.go
Comment thread cmd/diff/diffprocessor/comp_processor.go
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Copilot AI review requested due to automatic review settings March 27, 2026 19:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/diff/renderer/types/types.go Outdated
// OutputError represents an error in structured output.
// Used consistently by both XR diff and comp diff for machine-readable error handling.
type OutputError struct {
ResourceID string `json:"resourceID,omitempty" yaml:"resourceId,omitempty"`
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OutputError.ResourceID uses json:"resourceID" but yaml:"resourceId", and the repo docs (CLAUDE.md) describe resourceId. This makes structured output inconsistent across JSON/YAML and likely breaks consumers expecting a stable field name. Use a single canonical key (e.g., resourceId) for both JSON and YAML tags, and keep it consistent with the documented schema.

Suggested change
ResourceID string `json:"resourceID,omitempty" yaml:"resourceId,omitempty"`
ResourceID string `json:"resourceId,omitempty" yaml:"resourceId,omitempty"`

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +143
type xrImpactJSON struct {
corev1.ObjectReference `json:",inline"`

Status XRStatus `json:"status" yaml:"status"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
DownstreamChanges *DownstreamChanges `json:"downstreamChanges,omitempty" yaml:"downstreamChanges,omitempty"`
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corev1.ObjectReference is embedded with the struct tag json:",inline", but encoding/json does not support inline and the presence of a json tag prevents anonymous-field promotion. This will likely serialize as a nested ObjectReference:{...} field instead of top-level apiVersion/kind/name/namespace, conflicting with the README example and intended machine-readable schema. Embed corev1.ObjectReference without a json tag (and, if needed, use yaml:",inline" for YAML) so identity fields are actually inlined.

Copilot uses AI. Check for mistakes.
Comment thread cmd/diff/renderer/structured_renderer_test.go Outdated
Comment thread CLAUDE.md
Comment thread cmd/diff/diffprocessor/comp_processor.go
Comment on lines +57 to +80
validate: func(t *testing.T, result string) {
t.Helper()

var parsed compDiffJSONOutput
if err := json.Unmarshal([]byte(result), &parsed); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}

if len(parsed.Compositions) != 1 {
t.Fatalf("Expected 1 composition, got %d", len(parsed.Compositions))
}

if parsed.Compositions[0].Name != "test-comp" {
t.Errorf("Expected name 'test-comp', got '%s'", parsed.Compositions[0].Name)
}

if parsed.Compositions[0].AffectedResources.Total != 2 {
t.Errorf("Expected total 2, got %d", parsed.Compositions[0].AffectedResources.Total)
}
// Verify compositionChanges is present in JSON
if parsed.Compositions[0].CompositionChanges == nil {
t.Error("Expected compositionChanges to be present")
}
},
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structured comp diff tests don't currently assert the JSON/YAML shape of the embedded XR identity fields (apiVersion/kind/name/namespace). Adding an assertion that these fields are top-level (and not nested under an ObjectReference key) would catch schema regressions like incorrect struct tags on the embedded reference.

Copilot uses AI. Check for mistakes.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CLAUDE.md
Comment thread cmd/diff/renderer/diff_renderer_test.go
t.Error("Expected Manual update policy message")
}
},
},
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultCompDiffRenderer now has an error-bearing path via CompositionDiff.Error (set by the processor), but the unit tests here don't cover how the renderer should behave when a composition failed to process. Adding a test case with CompositionDiff{Error: ...} would help prevent regressions and ensure error output is user-friendly.

Suggested change
},
},
"CompositionProcessingError": {
output: &CompDiffOutput{
Compositions: []CompositionDiff{{
Name: "error-comp",
Error: errors.New("failed to process composition"),
}},
},
colorize: false,
validate: func(t *testing.T, result string) {
t.Helper()
if !strings.Contains(result, "error-comp") {
t.Error("Expected output to contain the composition name for error case")
}
if !strings.Contains(result, "failed to process composition") {
t.Error("Expected output to include the composition error message")
}
},
},

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment thread cmd/diff/renderer/comp_diff_renderer.go
Comment thread cmd/diff/renderer/structured_renderer.go
Comment thread cmd/diff/renderer/diff_renderer.go Outdated

// Write errors at the end (for human-readable output)
for _, e := range errs {
errMsg := fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultDiffRenderer prints errors as ERROR: %s: %s unconditionally. Since OutputError.ResourceID is optional, this can yield ERROR: : <message> when ResourceID is empty. Consider omitting the extra colon/prefix when ResourceID isn't set (or using a different format for global errors).

Suggested change
errMsg := fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message)
var errMsg string
if e.ResourceID != "" {
errMsg = fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message)
} else {
errMsg = fmt.Sprintf("ERROR: %s", e.Message)
}

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +92
comps := rawParsed["compositions"].([]any)
comp := comps[0].(map[string]any)
impacts := comp["impactAnalysis"].([]any)
impact := impacts[0].(map[string]any)

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test relies on unchecked type assertions (e.g., rawParsed["compositions"].([]any)), which will panic if the JSON shape changes and can obscure the actual failure. Consider using comma-ok assertions and failing the test with a descriptive message instead of panicking.

Suggested change
comps := rawParsed["compositions"].([]any)
comp := comps[0].(map[string]any)
impacts := comp["impactAnalysis"].([]any)
impact := impacts[0].(map[string]any)
rawComps, ok := rawParsed["compositions"]
if !ok {
t.Fatalf("Expected 'compositions' key in JSON output, but it was missing")
}
comps, ok := rawComps.([]any)
if !ok {
t.Fatalf("Expected 'compositions' to be a JSON array, got %T", rawComps)
}
if len(comps) == 0 {
t.Fatalf("Expected at least one composition in 'compositions', but got none")
}
comp, ok := comps[0].(map[string]any)
if !ok {
t.Fatalf("Expected first element of 'compositions' to be an object, got %T", comps[0])
}
rawImpacts, ok := comp["impactAnalysis"]
if !ok {
t.Fatalf("Expected 'impactAnalysis' key in first composition, but it was missing")
}
impacts, ok := rawImpacts.([]any)
if !ok {
t.Fatalf("Expected 'impactAnalysis' to be a JSON array, got %T", rawImpacts)
}
if len(impacts) == 0 {
t.Fatalf("Expected at least one impact in 'impactAnalysis', but got none")
}
impact, ok := impacts[0].(map[string]any)
if !ok {
t.Fatalf("Expected first element of 'impactAnalysis' to be an object, got %T", impacts[0])
}

Copilot uses AI. Check for mistakes.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 15:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 19:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Copilot AI review requested due to automatic review settings March 31, 2026 22:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jcogilvie jcogilvie merged commit d10e74c into main Apr 1, 2026
15 checks passed
@jcogilvie jcogilvie deleted the machine-readable-comp-diff branch April 1, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants