Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package helm for openSUSE:Factory checked in at 2026-04-10 17:53:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/helm (Old) and /work/SRC/openSUSE:Factory/.helm.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "helm" Fri Apr 10 17:53:08 2026 rev:100 rq:1345702 version:4.1.4 Changes: -------- --- /work/SRC/openSUSE:Factory/helm/helm.changes 2026-03-13 21:21:33.293099766 +0100 +++ /work/SRC/openSUSE:Factory/.helm.new.21863/helm.changes 2026-04-10 18:02:23.165058401 +0200 @@ -1,0 +2,23 @@ +Thu Apr 9 08:43:18 UTC 2026 - Johannes Kastl <[email protected]> + +- update to 4.1.4 (CVE-2026-35204, CVE-2026-35205, CVE-2026-35206): + Helm v4.1.4 is a security fix patch release. Users are encouraged + to upgrade for the best experience. + * Security fixes + - GHSA-hr2v-4r36-88hr Helm Chart extraction output directory + collapse via Chart.yaml name dot-segment + - GHSA-q5jf-9vfq-h4h7 Plugin verification fails open when .prov + is missing, allowing unsigned plugin install + - GHSA-vmx8-mqv2-9gmg Path traversal in plugin metadata version + enables arbitrary file write outside Helm plugin directory + * Changelog + - fix: Plugin missing provenance bypass 05fa379 (George + Jenkins) + - fix: Chart dot-name path bug 4e7994d (George Jenkins) + - ignore error plugin loads (cli, getter) 2581943 (George + Jenkins) + - fix: Plugin version path traversal 36c8539 (George Jenkins) + - fix: pin codeql-action/upload-sarif to commit SHA in + scorecards workflow c61e086 (Terry Howe) + +------------------------------------------------------------------- Old: ---- helm-4.1.3.obscpio New: ---- helm-4.1.4.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ helm.spec ++++++ --- /var/tmp/diff_new_pack.QPqJ1x/_old 2026-04-10 18:02:27.481236330 +0200 +++ /var/tmp/diff_new_pack.QPqJ1x/_new 2026-04-10 18:02:27.481236330 +0200 @@ -17,7 +17,7 @@ Name: helm -Version: 4.1.3 +Version: 4.1.4 Release: 0 Summary: The Kubernetes Package Manager License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.QPqJ1x/_old 2026-04-10 18:02:27.529238309 +0200 +++ /var/tmp/diff_new_pack.QPqJ1x/_new 2026-04-10 18:02:27.537238639 +0200 @@ -5,7 +5,7 @@ <param name="exclude">.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> - <param name="revision">v4.1.3</param> + <param name="revision">v4.1.4</param> <param name="changesgenerate">disable</param> </service> <service name="set_version" mode="manual"> ++++++ helm-4.1.3.obscpio -> helm-4.1.4.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/cmd/helm/helm_test.go new/helm-4.1.4/cmd/helm/helm_test.go --- old/helm-4.1.3/cmd/helm/helm_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/cmd/helm/helm_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -67,7 +67,7 @@ assert.Empty(t, stdout.String()) - expectedStderr := "Error: plugin \"exitwith\" exited with error\n" + expectedStderr := "level=WARN msg=\"failed to load plugin (ignoring)\" plugin_yaml=../../pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml error=\"failed to load plugin \\\"../../pkg/cmd/testdata/helmhome/helm/plugins/noversion\\\": plugin `version` is required\"\nError: plugin \"exitwith\" exited with error\n" if stderr.String() != expectedStderr { t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String()) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/metadata.go new/helm-4.1.4/internal/chart/v3/metadata.go --- old/helm-4.1.3/internal/chart/v3/metadata.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/metadata.go 2026-04-09 06:58:06.000000000 +0200 @@ -112,6 +112,9 @@ return ValidationError("chart.metadata.name is required") } + if md.Name == "." || md.Name == ".." { + return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name) + } if md.Name != filepath.Base(md.Name) { return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/metadata_test.go new/helm-4.1.4/internal/chart/v3/metadata_test.go --- old/helm-4.1.3/internal/chart/v3/metadata_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/metadata_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -41,6 +41,16 @@ ValidationError("chart.metadata.name is required"), }, { + "chart with dot name", + &Metadata{Name: ".", APIVersion: "v3", Version: "1.0"}, + ValidationError("chart.metadata.name \".\" is not allowed"), + }, + { + "chart with dotdot name", + &Metadata{Name: "..", APIVersion: "v3", Version: "1.0"}, + ValidationError("chart.metadata.name \"..\" is not allowed"), + }, + { "chart without name", &Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"}, ValidationError("chart.metadata.name \"../../test\" is invalid"), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/util/expand.go new/helm-4.1.4/internal/chart/v3/util/expand.go --- old/helm-4.1.3/internal/chart/v3/util/expand.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/util/expand.go 2026-04-09 06:58:06.000000000 +0200 @@ -52,6 +52,17 @@ return errors.New("chart name not specified") } + // Reject chart names that are POSIX path dot-segments or dot-dot segments or contain path separators. + // A dot-segment name (e.g. ".") causes SecureJoin to resolve to the root + // directory and extraction then to write files directly into that extraction root + // instead of a per-chart subdirectory. + if chartName == "." || chartName == ".." { + return fmt.Errorf("chart name %q is not allowed", chartName) + } + if chartName != filepath.Base(chartName) { + return fmt.Errorf("chart name %q must not contain path separators", chartName) + } + // Find the base directory // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up // being wrong or returning an error. This was introduced in v0.4.0. @@ -61,6 +72,12 @@ return err } + // Defense-in-depth: the chart directory must be a subdirectory of dir, + // never dir itself. + if chartdir == dir { + return fmt.Errorf("chart name %q resolves to the extraction root", chartName) + } + // Copy all files verbatim. We don't parse these files because parsing can remove // comments. for _, file := range files { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/util/expand_test.go new/helm-4.1.4/internal/chart/v3/util/expand_test.go --- old/helm-4.1.3/internal/chart/v3/util/expand_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/util/expand_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -17,11 +17,73 @@ package util import ( + "archive/tar" + "bytes" + "compress/gzip" + "io/fs" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// makeTestChartArchive builds a gzipped tar archive from the given sourceDir directory, file entries are prefixed with the given chartName +func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buffer { + t.Helper() + + var result bytes.Buffer + gw := gzip.NewWriter(&result) + tw := tar.NewWriter(gw) + + dir := os.DirFS(sourceDir) + + writeFile := func(relPath string) { + t.Helper() + f, err := dir.Open(relPath) + require.NoError(t, err) + + fStat, err := f.Stat() + require.NoError(t, err) + + err = tw.WriteHeader(&tar.Header{ + Name: filepath.Join(chartName, relPath), + Mode: int64(fStat.Mode()), + Size: fStat.Size(), + }) + require.NoError(t, err) + + data, err := fs.ReadFile(dir, relPath) + require.NoError(t, err) + tw.Write(data) + } + + err := fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + writeFile(path) + + return nil + }) + if err != nil { + t.Fatal(err) + } + + err = tw.Close() + require.NoError(t, err) + err = gw.Close() + require.NoError(t, err) + + return &result +} + func TestExpand(t *testing.T) { dest := t.TempDir() @@ -75,6 +137,28 @@ } } +func TestExpandError(t *testing.T) { + tests := map[string]struct { + chartName string + chartDir string + wantErr string + }{ + "dot name": {"dotname", "testdata/dotname", "not allowed"}, + "dotdot name": {"dotdotname", "testdata/dotdotname", "not allowed"}, + "slash in name": {"slashinname", "testdata/slashinname", "must not contain path separators"}, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + archive := makeTestChartArchive(t, tt.chartName, tt.chartDir) + + dest := t.TempDir() + err := Expand(dest, archive) + assert.ErrorContains(t, err, tt.wantErr) + }) + } +} + func TestExpandFile(t *testing.T) { dest := t.TempDir() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/util/testdata/dotdotname/Chart.yaml new/helm-4.1.4/internal/chart/v3/util/testdata/dotdotname/Chart.yaml --- old/helm-4.1.3/internal/chart/v3/util/testdata/dotdotname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/util/testdata/dotdotname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: .. +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/util/testdata/dotname/Chart.yaml new/helm-4.1.4/internal/chart/v3/util/testdata/dotname/Chart.yaml --- old/helm-4.1.3/internal/chart/v3/util/testdata/dotname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/util/testdata/dotname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: . +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/chart/v3/util/testdata/slashinname/Chart.yaml new/helm-4.1.4/internal/chart/v3/util/testdata/slashinname/Chart.yaml --- old/helm-4.1.3/internal/chart/v3/util/testdata/slashinname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/internal/chart/v3/util/testdata/slashinname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: a/../b +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/installer/installer.go new/helm-4.1.4/internal/plugin/installer/installer.go --- old/helm-4.1.3/internal/plugin/installer/installer.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/installer/installer.go 2026-04-09 06:58:06.000000000 +0200 @@ -98,24 +98,23 @@ // Check if provenance data exists if len(provData) == 0 { - // No .prov file found - emit warning but continue installation - fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n") - } else { - // Provenance data exists - verify the plugin - verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring) - if err != nil { - return nil, fmt.Errorf("plugin verification failed: %w", err) - } + return nil, fmt.Errorf("plugin verification failed: no provenance file (.prov) found") + } + + // Provenance data exists - verify the plugin + verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring) + if err != nil { + return nil, fmt.Errorf("plugin verification failed: %w", err) + } - // Collect verification info - result = &VerificationResult{ - SignedBy: make([]string, 0), - Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint), - FileHash: verification.FileHash, - } - for name := range verification.SignedBy.Identities { - result.SignedBy = append(result.SignedBy, name) - } + // Collect verification info + result = &VerificationResult{ + SignedBy: make([]string, 0), + Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint), + FileHash: verification.FileHash, + } + for name := range verification.SignedBy.Identities { + result.SignedBy = append(result.SignedBy, name) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/installer/verification_test.go new/helm-4.1.4/internal/plugin/installer/verification_test.go --- old/helm-4.1.3/internal/plugin/installer/verification_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/installer/verification_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -16,10 +16,8 @@ package installer import ( - "bytes" "crypto/sha256" "fmt" - "io" "os" "path/filepath" "strings" @@ -44,33 +42,49 @@ } defer os.RemoveAll(installer.Path()) - // Capture stderr to check warning message - oldStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - - // Install with verification enabled (should warn but succeed) + // Install with verification enabled should fail when .prov is missing result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"}) - // Restore stderr and read captured output - w.Close() - os.Stderr = oldStderr - var buf bytes.Buffer - io.Copy(&buf, r) - output := buf.String() - - // Should succeed with nil result (no verification performed) - if err != nil { - t.Fatalf("Expected installation to succeed despite missing .prov file, got error: %v", err) + // Should fail with a missing provenance error + if err == nil { + t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled") + } + if !strings.Contains(err.Error(), "no provenance file") { + t.Errorf("Expected 'no provenance file' in error message, got: %v", err) } if result != nil { t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result) } - // Should contain warning message - expectedWarning := "WARNING: No provenance file found for plugin" - if !strings.Contains(output, expectedWarning) { - t.Errorf("Expected warning message '%s' in output, got: %s", expectedWarning, output) + // Plugin should NOT be installed + if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) { + t.Errorf("Plugin should not be installed when verification fails due to missing .prov") + } +} + +func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) { + ensure.HelmHome(t) + + // Create a temporary plugin tarball without .prov file + pluginDir := createTestPluginDir(t) + pluginTgz := createTarballFromPluginDir(t, pluginDir) + defer os.Remove(pluginTgz) + + // Create local installer + installer, err := NewLocalInstaller(pluginTgz) + if err != nil { + t.Fatalf("Failed to create installer: %v", err) + } + defer os.RemoveAll(installer.Path()) + + // Install with verification explicitly disabled should succeed without .prov + result, err := InstallWithOptions(installer, Options{Verify: false}) + + if err != nil { + t.Fatalf("Expected installation to succeed with --verify=false, got error: %v", err) + } + if result != nil { + t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result) } // Plugin should be installed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/loader.go new/helm-4.1.4/internal/plugin/loader.go --- old/helm-4.1.3/internal/plugin/loader.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/loader.go 2026-04-09 06:58:06.000000000 +0200 @@ -19,6 +19,7 @@ "bytes" "fmt" "io" + "log/slog" "os" "path/filepath" @@ -158,18 +159,27 @@ return pm.CreatePlugin(dirname, m) } -// LoadAll loads all plugins found beneath the base directory. +func LogIgnorePluginLoadErrorFilterFunc(pluginYAML string, err error) error { + slog.Warn("failed to load plugin (ignoring)", slog.String("plugin_yaml", pluginYAML), slog.Any("error", err)) + return nil +} + +// errorFilterFunc is a function that can filter errors during plugin loading +type ErrorFilterFunc func(string, error) error + +// LoadAllDir load all plugins found beneath the base directory, using the provided error filter to determine whether to fail on individual plugin load errors. // // This scans only one directory level. -func LoadAll(basedir string) ([]Plugin, error) { - var plugins []Plugin - // We want basedir/*/plugin.yaml +func LoadAllDir(basedir string, errorFilter ErrorFilterFunc) ([]Plugin, error) { + // We want <basedir>/*/plugin.yaml scanpath := filepath.Join(basedir, "*", PluginFileName) matches, err := filepath.Glob(scanpath) if err != nil { return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err) } + plugins := make([]Plugin, 0, len(matches)) + // empty dir should load if len(matches) == 0 { return plugins, nil @@ -179,9 +189,12 @@ dir := filepath.Dir(yamlFile) p, err := LoadDir(dir) if err != nil { - return plugins, err + if errNew := errorFilter(yamlFile, err); errNew != nil { + return plugins, errNew + } + } else { + plugins = append(plugins, p) } - plugins = append(plugins, p) } return plugins, detectDuplicates(plugins) } @@ -193,8 +206,12 @@ type filterFunc func(Plugin) bool // FindPlugins returns a list of plugins that match the descriptor +// Errors loading a plugin are ignored with a warning func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) { - return findPlugins(pluginsDirs, LoadAll, makeDescriptorFilter(descriptor)) + loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) { + return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc) + } + return findPlugins(pluginsDirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor)) } // findPlugins is the internal implementation that uses the find and filter functions @@ -237,7 +254,11 @@ // FindPlugin returns a single plugin that matches the descriptor func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) { - plugins, err := FindPlugins(dirs, descriptor) + loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) { + return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc) + } + + plugins, err := findPlugins(dirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor)) if err != nil { return nil, err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/loader_test.go new/helm-4.1.4/internal/plugin/loader_test.go --- old/helm-4.1.3/internal/plugin/loader_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/loader_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -205,16 +205,16 @@ } } -func TestLoadAll(t *testing.T) { - // Verify that empty dir loads: - { - plugs, err := LoadAll("testdata") - require.NoError(t, err) - assert.Len(t, plugs, 0) - } +func TestLoadAllDir_Empty(t *testing.T) { + emptyDir := t.TempDir() + plugs, err := LoadAllDir(emptyDir, func(_ string, err error) error { return err }) + require.NoError(t, err) + assert.Len(t, plugs, 0) +} +func TestLoadAllPluginsDir(t *testing.T) { basedir := "testdata/plugdir/good" - plugs, err := LoadAll(basedir) + plugs, err := LoadAllDir(basedir, func(_ string, err error) error { return err }) require.NoError(t, err) require.NotEmpty(t, plugs, "expected plugins to be loaded from %s", basedir) @@ -233,7 +233,7 @@ assert.Contains(t, plugsMap, "postrenderer-v1") } -func TestFindPlugins(t *testing.T) { +func TestLoadAllPluginsDir_Zero(t *testing.T) { cases := []struct { name string plugdirs string @@ -241,28 +241,20 @@ }{ { name: "plugdirs is empty", - plugdirs: "", - expected: 0, + plugdirs: t.TempDir(), }, { name: "plugdirs isn't dir", plugdirs: "./plugin_test.go", - expected: 0, }, { name: "plugdirs doesn't have plugin", plugdirs: ".", - expected: 0, - }, - { - name: "normal", - plugdirs: "./testdata/plugdir/good", - expected: 7, }, } for _, c := range cases { t.Run(t.Name(), func(t *testing.T) { - plugin, err := LoadAll(c.plugdirs) + plugin, err := LoadAllDir(c.plugdirs, func(_ string, err error) error { return err }) require.NoError(t, err) assert.Len(t, plugin, c.expected, "expected %d plugins, got %d", c.expected, len(plugin)) }) @@ -338,6 +330,7 @@ "correct name field": { yaml: `apiVersion: v1 name: my-plugin +version: 1.0.0 type: cli/v1 runtime: subprocess `, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata.go new/helm-4.1.4/internal/plugin/metadata.go --- old/helm-4.1.3/internal/plugin/metadata.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata.go 2026-04-09 06:58:06.000000000 +0200 @@ -19,9 +19,17 @@ "errors" "fmt" + "github.com/Masterminds/semver/v3" + "helm.sh/helm/v4/internal/plugin/schema" ) +// isValidSemver checks if the given string is a valid semantic version +func isValidSemver(v string) bool { + _, err := semver.StrictNewVersion(v) + return err == nil +} + // Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml // Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime type Metadata struct { @@ -57,6 +65,11 @@ errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)) } + // Require version to be valid semver if specified + if m.Version != "" && !isValidSemver(m.Version) { + errs = append(errs, fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version)) + } + if m.APIVersion == "" { errs = append(errs, fmt.Errorf("empty APIVersion")) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata_legacy.go new/helm-4.1.4/internal/plugin/metadata_legacy.go --- old/helm-4.1.3/internal/plugin/metadata_legacy.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata_legacy.go 2026-04-09 06:58:06.000000000 +0200 @@ -71,6 +71,11 @@ if !validPluginName.MatchString(m.Name) { return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name) } + + if m.Version != "" && !isValidSemver(m.Version) { + return fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version) + } + m.Usage = sanitizeString(m.Usage) if len(m.PlatformCommand) > 0 && len(m.Command) > 0 { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata_legacy_test.go new/helm-4.1.4/internal/plugin/metadata_legacy_test.go --- old/helm-4.1.3/internal/plugin/metadata_legacy_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata_legacy_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -26,6 +26,10 @@ "valid metadata": { Name: "myplugin", }, + "valid metadata (empty version)": { + Name: "myplugin", + Version: "", + }, "valid with command": { Name: "myplugin", Command: "echo hello", @@ -59,6 +63,13 @@ }, }, }, + "valid with version": { + Name: "myplugin", + Version: "1.0.0", + }, + "valid with empty version": { + Name: "myplugin", + }, } for testName, metadata := range testsValid { @@ -116,6 +127,14 @@ }, }, }, + "path traversal version": { + Name: "myplugin", + Version: "../../../../tmp/evil", + }, + "invalid version": { + Name: "myplugin", + Version: "not-a-version", + }, } for testName, metadata := range testsInvalid { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata_test.go new/helm-4.1.4/internal/plugin/metadata_test.go --- old/helm-4.1.3/internal/plugin/metadata_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -18,6 +18,8 @@ import ( "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestValidatePluginData(t *testing.T) { @@ -72,6 +74,43 @@ } } +func TestMetadataValidateVersion(t *testing.T) { + testValid := map[string]struct { + version string + }{ + "valid semver": {version: "1.0.0"}, + "valid semver with prerelease": {version: "1.2.3-alpha.1+build.123"}, + "empty version": {version: ""}, + } + + testInvalid := map[string]struct { + version string + }{ + "valid semver with v prefix": {version: "v1.0.0"}, + "path traversal": {version: "../../../../tmp/evil"}, + "path traversal in version": {version: "1.0.0/../../etc"}, + "not a version": {version: "not-a-version"}, + } + + for name, tc := range testValid { + t.Run(name, func(t *testing.T) { + m := mockSubprocessCLIPlugin(t, "testplugin") + m.metadata.Version = tc.version + err := m.Metadata().Validate() + assert.NoError(t, err) + }) + } + + for name, tc := range testInvalid { + t.Run(name, func(t *testing.T) { + m := mockSubprocessCLIPlugin(t, "testplugin") + m.metadata.Version = tc.version + err := m.Metadata().Validate() + assert.ErrorContains(t, err, "invalid plugin version") + }) + } +} + func TestMetadataValidateMultipleErrors(t *testing.T) { // Create metadata with multiple validation issues metadata := Metadata{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata_v1.go new/helm-4.1.4/internal/plugin/metadata_v1.go --- old/helm-4.1.3/internal/plugin/metadata_v1.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata_v1.go 2026-04-09 06:58:06.000000000 +0200 @@ -51,6 +51,13 @@ return fmt.Errorf("invalid plugin `name`") } + if m.Version == "" { + return fmt.Errorf("plugin `version` is required") + } + if !isValidSemver(m.Version) { + return fmt.Errorf("invalid plugin `version` %q: must be valid semver", m.Version) + } + if m.APIVersion != "v1" { return fmt.Errorf("invalid `apiVersion`: %q", m.APIVersion) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/metadata_v1_test.go new/helm-4.1.4/internal/plugin/metadata_v1_test.go --- old/helm-4.1.3/internal/plugin/metadata_v1_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/metadata_v1_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,85 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMetadataV1ValidateVersion(t *testing.T) { + base := func() MetadataV1 { + return MetadataV1{ + APIVersion: "v1", + Name: "myplugin", + Type: "cli/v1", + Runtime: "subprocess", + Version: "1.0.0", + } + } + + testsValid := map[string]string{ + "simple version": "1.0.0", + "with prerelease": "1.2.3-alpha.1", + "with build meta": "1.2.3+build.123", + "full prerelease": "1.2.3-alpha.1+build.123", + } + + for name, version := range testsValid { + t.Run("valid/"+name, func(t *testing.T) { + m := base() + m.Version = version + assert.NoError(t, m.Validate()) + }) + } + + testsInvalid := map[string]struct { + version string + errMsg string + }{ + "empty version": { + version: "", + errMsg: "plugin `version` is required", + }, + "v prefix": { + version: "v1.0.0", + errMsg: "invalid plugin `version` \"v1.0.0\": must be valid semver", + }, + "path traversal": { + version: "../../../../tmp/evil", + errMsg: "invalid plugin `version`", + }, + "path traversal etc": { + version: "../../../etc/passwd", + errMsg: "invalid plugin `version`", + }, + "not a version": { + version: "not-a-version", + errMsg: "invalid plugin `version`", + }, + } + + for name, tc := range testsInvalid { + t.Run("invalid/"+name, func(t *testing.T) { + m := base() + m.Version = tc.version + err := m.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.errMsg) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/plugin_test.go new/helm-4.1.4/internal/plugin/plugin_test.go --- old/helm-4.1.3/internal/plugin/plugin_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/plugin_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -82,7 +82,7 @@ return &SubprocessPluginRuntime{ metadata: Metadata{ Name: pluginName, - Version: "v0.1.2", + Version: "0.1.2", Type: "cli/v1", APIVersion: "v1", Runtime: "subprocess", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/internal/plugin/runtime_subprocess_test.go new/helm-4.1.4/internal/plugin/runtime_subprocess_test.go --- old/helm-4.1.3/internal/plugin/runtime_subprocess_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/internal/plugin/runtime_subprocess_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -41,7 +41,7 @@ md := Metadata{ Name: pluginName, - Version: "v0.1.2", + Version: "0.1.2", Type: "cli/v1", APIVersion: "v1", Runtime: "subprocess", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/metadata.go new/helm-4.1.4/pkg/chart/v2/metadata.go --- old/helm-4.1.3/pkg/chart/v2/metadata.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/metadata.go 2026-04-09 06:58:06.000000000 +0200 @@ -112,6 +112,9 @@ return ValidationError("chart.metadata.name is required") } + if md.Name == "." || md.Name == ".." { + return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name) + } if md.Name != filepath.Base(md.Name) { return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/metadata_test.go new/helm-4.1.4/pkg/chart/v2/metadata_test.go --- old/helm-4.1.3/pkg/chart/v2/metadata_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/metadata_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -41,6 +41,16 @@ ValidationError("chart.metadata.name is required"), }, { + "chart with dot name", + &Metadata{Name: ".", APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name \".\" is not allowed"), + }, + { + "chart with dotdot name", + &Metadata{Name: "..", APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name \"..\" is not allowed"), + }, + { "chart without name", &Metadata{Name: "../../test", APIVersion: "v2", Version: "1.0"}, ValidationError("chart.metadata.name \"../../test\" is invalid"), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/util/expand.go new/helm-4.1.4/pkg/chart/v2/util/expand.go --- old/helm-4.1.3/pkg/chart/v2/util/expand.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/util/expand.go 2026-04-09 06:58:06.000000000 +0200 @@ -52,6 +52,17 @@ return errors.New("chart name not specified") } + // Reject chart names that are POSIX path dot-segments or dot-dot segments or contain path separators. + // A dot-segment name (e.g. ".") causes SecureJoin to resolve to the root + // directory and extraction then to write files directly into that extraction root + // instead of a per-chart subdirectory. + if chartName == "." || chartName == ".." { + return fmt.Errorf("chart name %q is not allowed", chartName) + } + if chartName != filepath.Base(chartName) { + return fmt.Errorf("chart name %q must not contain path separators", chartName) + } + // Find the base directory // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up // being wrong or returning an error. This was introduced in v0.4.0. @@ -61,6 +72,12 @@ return err } + // Defense-in-depth: the chart directory must be a subdirectory of dir, + // never dir itself. + if chartdir == dir { + return fmt.Errorf("chart name %q resolves to the extraction root", chartName) + } + // Copy all files verbatim. We don't parse these files because parsing can remove // comments. for _, file := range files { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/util/expand_test.go new/helm-4.1.4/pkg/chart/v2/util/expand_test.go --- old/helm-4.1.3/pkg/chart/v2/util/expand_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/util/expand_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -17,11 +17,73 @@ package util import ( + "archive/tar" + "bytes" + "compress/gzip" + "io/fs" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// makeTestChartArchive builds a gzipped tar archive from the given sourceDir directory, file entries are prefixed with the given chartName +func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buffer { + t.Helper() + + var result bytes.Buffer + gw := gzip.NewWriter(&result) + tw := tar.NewWriter(gw) + + dir := os.DirFS(sourceDir) + + writeFile := func(relPath string) { + t.Helper() + f, err := dir.Open(relPath) + require.NoError(t, err) + + fStat, err := f.Stat() + require.NoError(t, err) + + err = tw.WriteHeader(&tar.Header{ + Name: filepath.Join(chartName, relPath), + Mode: int64(fStat.Mode()), + Size: fStat.Size(), + }) + require.NoError(t, err) + + data, err := fs.ReadFile(dir, relPath) + require.NoError(t, err) + tw.Write(data) + } + + err := fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + writeFile(path) + + return nil + }) + if err != nil { + t.Fatal(err) + } + + err = tw.Close() + require.NoError(t, err) + err = gw.Close() + require.NoError(t, err) + + return &result +} + func TestExpand(t *testing.T) { dest := t.TempDir() @@ -75,6 +137,28 @@ } } +func TestExpandError(t *testing.T) { + tests := map[string]struct { + chartName string + chartDir string + wantErr string + }{ + "dot name": {"dotname", "testdata/dotname", "not allowed"}, + "dotdot name": {"dotdotname", "testdata/dotdotname", "not allowed"}, + "slash in name": {"slashinname", "testdata/slashinname", "must not contain path separators"}, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + archive := makeTestChartArchive(t, tt.chartName, tt.chartDir) + + dest := t.TempDir() + err := Expand(dest, archive) + assert.ErrorContains(t, err, tt.wantErr) + }) + } +} + func TestExpandFile(t *testing.T) { dest := t.TempDir() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/util/testdata/dotdotname/Chart.yaml new/helm-4.1.4/pkg/chart/v2/util/testdata/dotdotname/Chart.yaml --- old/helm-4.1.3/pkg/chart/v2/util/testdata/dotdotname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/util/testdata/dotdotname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: .. +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/util/testdata/dotname/Chart.yaml new/helm-4.1.4/pkg/chart/v2/util/testdata/dotname/Chart.yaml --- old/helm-4.1.3/pkg/chart/v2/util/testdata/dotname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/util/testdata/dotname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: . +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/chart/v2/util/testdata/slashinname/Chart.yaml new/helm-4.1.4/pkg/chart/v2/util/testdata/slashinname/Chart.yaml --- old/helm-4.1.3/pkg/chart/v2/util/testdata/slashinname/Chart.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/pkg/chart/v2/util/testdata/slashinname/Chart.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,4 @@ +apiVersion: v3 +name: a/../b +description: A Helm chart for Kubernetes +version: 0.1.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/plugin_install.go new/helm-4.1.4/pkg/cmd/plugin_install.go --- old/helm-4.1.3/pkg/cmd/plugin_install.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/plugin_install.go 2026-04-09 06:58:06.000000000 +0200 @@ -50,11 +50,11 @@ This command allows you to install a plugin from a url to a VCS repo or a local path. By default, plugin signatures are verified before installation when installing from -tarballs (.tgz or .tar.gz). This requires a corresponding .prov file to be available -alongside the tarball. +tarballs (.tgz or .tar.gz). A corresponding .prov file must be available alongside +the tarball; installation will fail if it is missing or invalid. For local development, plugins installed from local directories are automatically treated as "local dev" and do not require signatures. -Use --verify=false to skip signature verification for remote plugins. +Use --verify=false to explicitly skip signature verification (NOT recommended). ` func newPluginInstallCmd(out io.Writer) *cobra.Command { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/plugin_test.go new/helm-4.1.4/pkg/cmd/plugin_test.go --- old/helm-4.1.3/pkg/cmd/plugin_test.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/plugin_test.go 2026-04-09 06:58:06.000000000 +0200 @@ -117,6 +117,7 @@ {"exitwith", "exitwith code", "This exits with the specified exit code", "", []string{"2"}, 2}, {"fullenv", "show env vars", "show all env vars", fullEnvOutput, []string{}, 0}, {"shortenv", "env stuff", "show the env", "HELM_PLUGIN_NAME=shortenv\n", []string{}, 0}, + // "noversion": plugin is invalid, and should not be loaded } pluginCmds := cmd.Commands() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/plugin_uninstall.go new/helm-4.1.4/pkg/cmd/plugin_uninstall.go --- old/helm-4.1.3/pkg/cmd/plugin_uninstall.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/plugin_uninstall.go 2026-04-09 06:58:06.000000000 +0200 @@ -62,7 +62,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error { slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory) - plugins, err := plugin.LoadAll(settings.PluginsDirectory) + plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) if err != nil { return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/plugin_update.go new/helm-4.1.4/pkg/cmd/plugin_update.go --- old/helm-4.1.3/pkg/cmd/plugin_update.go 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/plugin_update.go 2026-04-09 06:58:06.000000000 +0200 @@ -62,7 +62,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) - plugins, err := plugin.LoadAll(settings.PluginsDirectory) + plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) if err != nil { return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' "old/helm-4.1.3/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml" "new/helm-4.1.4/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml" --- "old/helm-4.1.3/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml" 2026-03-11 22:47:44.000000000 +0100 +++ "new/helm-4.1.4/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml" 2026-04-09 06:58:06.000000000 +0200 @@ -1,6 +1,7 @@ --- apiVersion: v1 name: fullenv +version: 0.1.0 type: cli/v1 runtime: subprocess config: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,4 +1,5 @@ name: args +version: 0.1.0 type: cli/v1 apiVersion: v1 runtime: subprocess diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,4 +1,5 @@ name: echo +version: 0.1.0 type: cli/v1 apiVersion: v1 runtime: subprocess diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,6 +1,7 @@ --- apiVersion: v1 name: exitwith +version: 0.1.0 type: cli/v1 runtime: subprocess config: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,6 +1,7 @@ --- apiVersion: v1 name: fullenv +version: 0.1.0 type: cli/v1 runtime: subprocess config: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -0,0 +1,7 @@ +apiVersion: v1 +name: noversion +type: cli/v1 +runtime: subprocess +runtimeConfig: + platformCommand: + - command: "echo hello" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,6 +1,7 @@ --- apiVersion: v1 name: shortenv +version: 0.1.0 type: cli/v1 runtime: subprocess config: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helm-4.1.3/pkg/cmd/testdata/testplugin/plugin.yaml new/helm-4.1.4/pkg/cmd/testdata/testplugin/plugin.yaml --- old/helm-4.1.3/pkg/cmd/testdata/testplugin/plugin.yaml 2026-03-11 22:47:44.000000000 +0100 +++ new/helm-4.1.4/pkg/cmd/testdata/testplugin/plugin.yaml 2026-04-09 06:58:06.000000000 +0200 @@ -1,6 +1,7 @@ --- apiVersion: v1 name: testplugin +version: 0.1.0 type: cli/v1 runtime: subprocess config: ++++++ helm.obsinfo ++++++ --- /var/tmp/diff_new_pack.QPqJ1x/_old 2026-04-10 18:02:29.885335492 +0200 +++ /var/tmp/diff_new_pack.QPqJ1x/_new 2026-04-10 18:02:29.885335492 +0200 @@ -1,5 +1,5 @@ name: helm -version: 4.1.3 -mtime: 1773265664 -commit: c94d381b03be117e7e57908edbf642104e00eb8f +version: 4.1.4 +mtime: 1775710686 +commit: 05fa37973dc9e42b76e1d2883494c87174b6074f ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/helm/vendor.tar.gz /work/SRC/openSUSE:Factory/.helm.new.21863/vendor.tar.gz differ: char 30, line 1
