Skip to content

feat: add local subcommand to compare two chart folders#866

Open
bo0tzz wants to merge 7 commits intodatabus23:masterfrom
bo0tzz:feat/diff-local
Open

feat: add local subcommand to compare two chart folders#866
bo0tzz wants to merge 7 commits intodatabus23:masterfrom
bo0tzz:feat/diff-local

Conversation

@bo0tzz
Copy link
Copy Markdown

@bo0tzz bo0tzz commented Oct 3, 2025

This adds a new local subcommand that diffs two local chart directories. My intended usecase is mainly for development and CI/CD, to see what effects a change has.

This code was written entirely by an LLM; if that is a dealbreaker, feel free to just close this PR outright.

If there is appetite for it, I'd like to next build on top of this with a git subcommand that diffs two git references against eachother (by checking them out to a temporary folder and then running local against them).

@yxxhero
Copy link
Copy Markdown
Collaborator

yxxhero commented Oct 10, 2025

@bo0tzz maybe we should add an e2e test?

@yxxhero
Copy link
Copy Markdown
Collaborator

yxxhero commented Oct 10, 2025

The rest is LGTM. @bo0tzz

@yxxhero yxxhero requested a review from Copilot October 10, 2025 08:12
Copy link
Copy Markdown

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

This PR adds a new local subcommand to the helm-diff plugin that allows comparing two local chart directories. The feature enables developers to see differences between chart versions during development and CI/CD workflows by rendering both charts with helm template and showing the resulting manifest differences.

Key changes:

  • Implements a new local subcommand with comprehensive flag support for chart rendering options
  • Adds comprehensive test coverage for argument validation and command execution
  • Updates documentation to include the new subcommand usage and examples

Reviewed Changes

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

File Description
cmd/root.go Registers the new localCmd() subcommand in the main command structure
cmd/local.go Complete implementation of the local subcommand with chart rendering and diffing logic
cmd/local_test.go Test suite covering argument validation and basic execution scenarios
README.md Documentation update adding the local subcommand help text and usage examples

cmd/local.go Outdated
func (l *local) run() error {
manifest1, err := l.renderChart(l.chart1)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Error messages should start with lowercase letters to follow Go conventions.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart1, err)

Copilot uses AI. Check for mistakes.
cmd/local.go Outdated

manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Error messages should start with lowercase letters to follow Go conventions.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart2, err)

Copilot uses AI. Check for mistakes.
cmd/local.go Outdated
args := []string{"template", l.release, chartPath}
args = append(args, flags...)

cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Using os.Getenv("HELM_BIN") without validation could allow execution of arbitrary commands. Consider validating the HELM_BIN path or providing a default fallback.

Suggested change
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
helmBin := os.Getenv("HELM_BIN")
if helmBin == "" {
helmBin = "helm"
}
cmd := exec.Command(helmBin, args...)

Copilot uses AI. Check for mistakes.
@bo0tzz
Copy link
Copy Markdown
Author

bo0tzz commented Oct 10, 2025

@yxxhero I'd be down for e2e tests, but I don't think the project has any yet right? How would you like those to look?

@yxxhero
Copy link
Copy Markdown
Collaborator

yxxhero commented Oct 11, 2025

@bo0tzz it will be better if we add e2e test. this is a new feature. thanks so much.

@yxxhero
Copy link
Copy Markdown
Collaborator

yxxhero commented Nov 27, 2025

@copilot open a new pull request to apply changes based on the comments in this thread

@yxxhero yxxhero requested a review from Copilot November 27, 2025 14:57
@yxxhero
Copy link
Copy Markdown
Collaborator

yxxhero commented Nov 27, 2025

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown

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 4 out of 4 changed files in this pull request and generated 3 comments.

cmd/local.go Outdated
Comment on lines +120 to +125
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
}

manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Error messages should start with lowercase letters following Go conventions and consistency with other error messages in the codebase.

For example, change:

  • "Failed to render chart %s: %w" to "failed to render chart %s: %w"

This matches the pattern used in cmd/upgrade.go:292 and cmd/helm3.go:31-41.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
}
manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart1, err)
}
manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("failed to render chart %s: %w", l.chart2, err)

Copilot uses AI. Check for mistakes.
cmd/local.go Outdated

manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

Error message should start with a lowercase letter following Go conventions and consistency with other error messages in the codebase.

For example, change:

  • "Failed to render chart %s: %w" to "failed to render chart %s: %w"

This matches the pattern used in cmd/upgrade.go:292 and cmd/helm3.go:31-41.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart2, err)

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +126
manifest1, err := l.renderChart(l.chart1)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
}

manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The renderChart method is called twice (for chart1 and chart2), but it reads from stdin when -f - is used (line 169). Since stdin can only be read once, the second call to renderChart will fail or receive empty input.

This is different from the upgrade command where renderChart is only called once. For the local command, you would need to either:

  1. Read stdin once before calling renderChart and pass the content to both calls, or
  2. Document that stdin values (-f -) are not supported for this command, or
  3. Reuse the same temp file for both chart renders

Consider refactoring to handle stdin reading outside of the loop or before multiple renderChart calls.

Copilot uses AI. Check for mistakes.
bo0tzz and others added 4 commits March 30, 2026 07:50
- Lowercase error messages to follow Go conventions
- Extract stdin reading into prepareStdinValues() to avoid
  double-read when rendering both charts
- Add HELM_BIN env var validation with "helm" default fallback

Signed-off-by: yxxhero <aiopsclub@163.com>
…string

Signed-off-by: yxxhero <aiopsclub@163.com>
yxxhero added 3 commits March 30, 2026 08:07
Signed-off-by: yxxhero <aiopsclub@163.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Copy link
Copy Markdown

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 4 out of 4 changed files in this pull request and generated 5 comments.

--kube-version string Kubernetes version used for Capabilities.KubeVersion
--namespace string namespace to use for template rendering
--normalize-manifests normalize manifests before running diff to exclude style differences from the output
--output string Possible values: diff, simple, template, dyff. When set to "template", use the env var HELM_DIFF_TPL to specify the template. (default "diff")
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The --output flag help text here omits supported formats (json, structured) that are listed for other commands and are defined in AddDiffOptions (see cmd/options.go). This makes the README inconsistent with the actual CLI/help output. Update this line to reflect the complete set of supported output formats.

Suggested change
--output string Possible values: diff, simple, template, dyff. When set to "template", use the env var HELM_DIFF_TPL to specify the template. (default "diff")
--output string Possible values: diff, simple, template, dyff, json, structured. When set to "template", use the env var HELM_DIFF_TPL to specify the template. (default "diff")

Copilot uses AI. Check for mistakes.
if err != nil {
return err
}
defer func() { _ = os.Remove(tmpfile.Name()) }()
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

prepareStdinValues creates a temp file for -f - but defers os.Remove inside this function. Because the defer runs when prepareStdinValues returns (before renderChart is executed), the temp file path stored in l.valueFiles[i] can be deleted before helm template tries to read it, causing helm template to fail. Keep the temp file alive until after both renderChart calls complete (e.g., defer cleanup in run() or handle stdin values within renderChart).

Suggested change
defer func() { _ = os.Remove(tmpfile.Name()) }()

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +176
for i, valueFile := range l.valueFiles {
if strings.TrimSpace(valueFile) == "-" {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}

tmpfile, err := os.CreateTemp("", "helm-diff-stdin-values")
if err != nil {
return err
}
defer func() { _ = os.Remove(tmpfile.Name()) }()

if _, err := tmpfile.Write(data); err != nil {
_ = tmpfile.Close()
return err
}

if err := tmpfile.Close(); err != nil {
return err
}

l.valueFiles[i] = tmpfile.Name()
break
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

prepareStdinValues stops after the first - values file (break). If the user provides multiple -f - entries, later occurrences remain as "-" and will cause helm template to read from stdin after this function already consumed it via io.ReadAll(os.Stdin). Consider either replacing all "-" entries with the same temp file path (read stdin once) or returning an explicit error when more than one stdin values file is provided.

Suggested change
for i, valueFile := range l.valueFiles {
if strings.TrimSpace(valueFile) == "-" {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
tmpfile, err := os.CreateTemp("", "helm-diff-stdin-values")
if err != nil {
return err
}
defer func() { _ = os.Remove(tmpfile.Name()) }()
if _, err := tmpfile.Write(data); err != nil {
_ = tmpfile.Close()
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
l.valueFiles[i] = tmpfile.Name()
break
var (
stdinData []byte
stdinRead bool
stdinTmpFilePath string
)
for i, valueFile := range l.valueFiles {
if strings.TrimSpace(valueFile) == "-" {
// Read from stdin and create the temp file only once, even if
// "-" appears multiple times in the values files list.
if !stdinRead {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
tmpfile, err := os.CreateTemp("", "helm-diff-stdin-values")
if err != nil {
return err
}
if _, err := tmpfile.Write(data); err != nil {
_ = tmpfile.Close()
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
stdinData = data
stdinTmpFilePath = tmpfile.Name()
stdinRead = true
_ = stdinData // avoid unused variable if not referenced elsewhere
}
l.valueFiles[i] = stdinTmpFilePath

Copilot uses AI. Check for mistakes.
}

specs1 := manifest.Parse(manifest1, l.namespace, l.normalizeManifests, excludes...)
specs2 := manifest.Parse(manifest2, l.namespace, l.normalizeManifests, excludes...)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

manifest1/manifest2 can be large. Other commands in this repo set the raw manifest byte slices to nil after manifest.Parse(...) to allow GC to reclaim memory before diff computation. Consider doing the same here after parsing to reduce peak memory usage when diffing large charts.

Suggested change
specs2 := manifest.Parse(manifest2, l.namespace, l.normalizeManifests, excludes...)
specs2 := manifest.Parse(manifest2, l.namespace, l.normalizeManifests, excludes...)
// Allow GC to reclaim memory used by raw manifests before diff computation.
manifest1 = nil
manifest2 = nil

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +67
func TestLocalCmdExecution(t *testing.T) {
tmpDir := t.TempDir()
fakeHelm := tmpDir + "/helm"
manifestYAML := `---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
namespace: default
data:
key: value
`

err := os.WriteFile(fakeHelm, []byte(`#!/bin/sh
cat <<EOF
`+manifestYAML+`
EOF
`), 0755)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

These tests create a fake Helm binary as a #!/bin/sh script and use Unix-specific assumptions (e.g., /bin/sh, heredocs, POSIX paths). This will fail when running go test on Windows. Consider switching to the existing Go-based fake-helm approach used elsewhere in the repo (e.g., main_test.go's runFakeHelm pattern) or skipping these tests on non-POSIX platforms.

Copilot uses AI. Check for mistakes.
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.

3 participants