From 30352ca1ac03f89ada5c965bdf2ce8073698b64b Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:24:50 +0100 Subject: [PATCH 01/18] feat(plugin): add --version flag to plugin update command Add support for specifying a version constraint when updating plugins, matching the existing behavior of helm plugin install. Changes: - Add --version flag to plugin update command - Update VCSInstaller.Update() to resolve and checkout specified version - Update FindSource() to accept version parameter - Add TestVCSInstallerUpdateWithVersion test for version support Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/plugin/installer/installer.go | 4 +- internal/plugin/installer/vcs_installer.go | 18 +++++- .../plugin/installer/vcs_installer_test.go | 63 ++++++++++++++++++- pkg/cmd/plugin_update.go | 12 ++-- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index 4728879f5..28542edb9 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -157,8 +157,8 @@ func NewForSource(source, version string) (installer Installer, err error) { } // FindSource determines the correct Installer for the given source. -func FindSource(location string) (Installer, error) { - installer, err := existingVCSRepo(location) +func FindSource(location string, version string) (Installer, error) { + installer, err := existingVCSRepo(location, version) if err != nil && err.Error() == "Cannot detect VCS" { slog.Warn( "cannot get information about plugin source", diff --git a/internal/plugin/installer/vcs_installer.go b/internal/plugin/installer/vcs_installer.go index 13aba74dc..f194faa30 100644 --- a/internal/plugin/installer/vcs_installer.go +++ b/internal/plugin/installer/vcs_installer.go @@ -38,14 +38,15 @@ type VCSInstaller struct { base } -func existingVCSRepo(location string) (Installer, error) { +func existingVCSRepo(location string, version string) (Installer, error) { repo, err := vcs.NewRepo("", location) if err != nil { return nil, err } i := &VCSInstaller{ - Repo: repo, - base: newBase(repo.Remote()), + Repo: repo, + Version: version, + base: newBase(repo.Remote()), } return i, nil } @@ -104,6 +105,17 @@ func (i *VCSInstaller) Update() error { if err := i.Repo.Update(); err != nil { return err } + + ref, err := i.solveVersion(i.Repo) + if err != nil { + return err + } + if ref != "" { + if err := i.setVersion(i.Repo, ref); err != nil { + return err + } + } + if !isPlugin(i.Repo.LocalPath()) { return ErrMissingMetadata } diff --git a/internal/plugin/installer/vcs_installer_test.go b/internal/plugin/installer/vcs_installer_test.go index 3f5b098e4..7149ed71c 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -49,6 +49,7 @@ func (r *testRepo) UpdateVersion(version string) error { r.current = version return r.err } +func (r *testRepo) IsDirty() bool { return false } func TestVCSInstaller(t *testing.T) { ensure.HelmHome(t) @@ -81,7 +82,7 @@ func TestVCSInstaller(t *testing.T) { require.EqualErrorf(t, Install(i), "plugin already exists", "expected error for plugin exists") // Testing FindSource method, expect error because plugin code is not a cloned repository - _, err = FindSource(i.Path()) + _, err = FindSource(i.Path(), "") require.Error(t, err, "expected error for inability to find plugin source, got none") require.EqualErrorf(t, err, "cannot get information about plugin source", "expected error for inability to find plugin source") } @@ -131,7 +132,7 @@ func TestVCSInstallerUpdate(t *testing.T) { } // Test FindSource method for positive result - pluginInfo, err := FindSource(i.Path()) + pluginInfo, err := FindSource(i.Path(), "") require.NoError(t, err) vcsInstaller := pluginInfo.(*VCSInstaller) @@ -147,3 +148,61 @@ func TestVCSInstallerUpdate(t *testing.T) { // Testing update for error require.EqualErrorf(t, Update(vcsInstaller), "plugin repo was modified", "expected error for plugin modified") } + +func TestVCSInstallerUpdateWithVersion(t *testing.T) { + ensure.HelmHome(t) + + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) + } + + source := "https://github.com/adamreese/helm-env" + testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo-v1") + repo := &testRepo{ + local: testRepoPath, + remote: source, + tags: []string{"0.1.0", "0.1.1", "0.2.0"}, + } + + // First install without version + i, err := NewForSource(source, "") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + vcsInstaller, ok := i.(*VCSInstaller) + if !ok { + t.Fatal("expected a VCSInstaller") + } + vcsInstaller.Repo = repo + + if err := Install(i); err != nil { + t.Fatal(err) + } + + // Now test update with specific version constraint + vcsInstaller.Version = "~0.1.0" + if err := Update(vcsInstaller); err != nil { + t.Fatal(err) + } + if repo.current != "0.1.1" { + t.Fatalf("expected version '0.1.1', got %q", repo.current) + } + + // Test update with different version constraint + vcsInstaller.Version = "0.2.0" + if err := Update(vcsInstaller); err != nil { + t.Fatal(err) + } + if repo.current != "0.2.0" { + t.Fatalf("expected version '0.2.0', got %q", repo.current) + } + + // Test update with non-existent version + vcsInstaller.Version = "0.3.0" + if err := Update(vcsInstaller); err == nil { + t.Fatal("expected error for version does not exist, got none") + } else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", "0.3.0", source) { + t.Fatalf("expected error for version does not exist, got (%v)", err) + } +} diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 1cba7ffa9..3d6aecc94 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -29,7 +29,8 @@ import ( ) type pluginUpdateOptions struct { - names []string + names []string + version string } func newPluginUpdateCmd(out io.Writer) *cobra.Command { @@ -49,6 +50,7 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command { return o.run(out) }, } + cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed") return cmd } @@ -70,8 +72,8 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { for _, name := range o.names { if found := findPlugin(plugins, name); found != nil { - if err := updatePlugin(found); err != nil { - errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%w)", name, err)) + if err := updatePlugin(found, o.version); err != nil { + errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err)) } else { fmt.Fprintf(out, "Updated plugin: %s\n", name) } @@ -85,7 +87,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { return nil } -func updatePlugin(p plugin.Plugin) error { +func updatePlugin(p plugin.Plugin, version string) error { exactLocation, err := filepath.EvalSymlinks(p.Dir()) if err != nil { return err @@ -95,7 +97,7 @@ func updatePlugin(p plugin.Plugin) error { return err } - i, err := installer.FindSource(absExactLocation) + i, err := installer.FindSource(absExactLocation, version) if err != nil { return err } From 0f705e94b500c281727c86408fbcccbb686c4800 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:45:06 +0100 Subject: [PATCH 02/18] Applied feedback from copilot pt. 1 Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 3d6aecc94..8c038b8a9 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -50,7 +50,7 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command { return o.run(out) }, } - cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed") + cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the plugin is updated to the latest version") return cmd } From a030a7d5383047e37628ee8299825dc980b6b6b4 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:32:54 +0100 Subject: [PATCH 03/18] Applied feedback from review pt. 2 Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/plugin/installer/installer.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index 28542edb9..164f2cb59 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -157,8 +157,13 @@ func NewForSource(source, version string) (installer Installer, err error) { } // FindSource determines the correct Installer for the given source. -func FindSource(location string, version string) (Installer, error) { - installer, err := existingVCSRepo(location, version) +// Version is optional; if not provided, it defaults to empty string. +func FindSource(location string, version ...string) (Installer, error) { + v := "" + if len(version) > 0 { + v = version[0] + } + installer, err := existingVCSRepo(location, v) if err != nil && err.Error() == "Cannot detect VCS" { slog.Warn( "cannot get information about plugin source", From a726dd8c00de385006897a8913c575f1bd28e0bc Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:59:59 +0100 Subject: [PATCH 04/18] Drop --version flag and support the @ pattern Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 56 +++++++++++-- pkg/cmd/plugin_update_test.go | 154 ++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 pkg/cmd/plugin_update_test.go diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 8c038b8a9..8f1fd30d0 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -21,7 +21,9 @@ import ( "io" "log/slog" "path/filepath" + "strings" + "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "helm.sh/helm/v4/internal/plugin" @@ -29,17 +31,29 @@ import ( ) type pluginUpdateOptions struct { - names []string - version string + plugins map[string]string } +const pluginUpdateDesc = `Update one or more Helm plugins. + +An exact version can be supplied per-plugin using the @version syntax: + + helm plugin update myplugin@1.2.3 otherplugin@2.0.0 + helm plugin update myplugin@v1.0.0 + +If no version is given for a plugin it is updated to the latest version: + + helm plugin update myplugin otherplugin +` + func newPluginUpdateCmd(out io.Writer) *cobra.Command { o := &pluginUpdateOptions{} cmd := &cobra.Command{ - Use: "update ...", + Use: "update ...", Aliases: []string{"up"}, Short: "update one or more Helm plugins", + Long: pluginUpdateDesc, ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp }, @@ -50,7 +64,6 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command { return o.run(out) }, } - cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the plugin is updated to the latest version") return cmd } @@ -58,21 +71,39 @@ func (o *pluginUpdateOptions) complete(args []string) error { if len(args) == 0 { return errors.New("please provide plugin name to update") } - o.names = args + + o.plugins = make(map[string]string, len(args)) + + for _, arg := range args { + name, version := parsePluginVersion(arg) + if name == "" { + return fmt.Errorf("invalid plugin reference %q: plugin name must not be empty", arg) + } + if _, exists := o.plugins[name]; exists { + return fmt.Errorf("plugin %q specified more than once", name) + } + if version != "" { + if _, err := semver.NewVersion(version); err != nil { + return fmt.Errorf("invalid version %q for plugin %q: must be an exact version (e.g. 1.2.3 or v1.2.3)", version, name) + } + } + o.plugins[name] = version + } + return nil } func (o *pluginUpdateOptions) run(out io.Writer) error { slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) - plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) + installed, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) if err != nil { return err } var errorPlugins []error - for _, name := range o.names { - if found := findPlugin(plugins, name); found != nil { - if err := updatePlugin(found, o.version); err != nil { + for name, version := range o.plugins { + if found := findPlugin(installed, name); found != nil { + if err := updatePlugin(found, version); err != nil { errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err)) } else { fmt.Fprintf(out, "Updated plugin: %s\n", name) @@ -87,6 +118,13 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { return nil } +func parsePluginVersion(arg string) (name, version string) { + if i := strings.LastIndex(arg, "@"); i >= 0 { + return arg[:i], arg[i+1:] + } + return arg, "" +} + func updatePlugin(p plugin.Plugin, version string) error { exactLocation, err := filepath.EvalSymlinks(p.Dir()) if err != nil { diff --git a/pkg/cmd/plugin_update_test.go b/pkg/cmd/plugin_update_test.go new file mode 100644 index 000000000..5c72074ab --- /dev/null +++ b/pkg/cmd/plugin_update_test.go @@ -0,0 +1,154 @@ +/* +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 cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParsePluginVersion(t *testing.T) { + tests := []struct { + arg string + wantName string + wantVersion string + }{ + {"myplugin", "myplugin", ""}, + {"myplugin@1.2.3", "myplugin", "1.2.3"}, + {"myplugin@v1.2.3", "myplugin", "v1.2.3"}, + {"myplugin@", "myplugin", ""}, + {"@version", "", "version"}, + {"", "", ""}, + // LastIndex ensures the last @ is used as delimiter + {"weird@name@1.0", "weird@name", "1.0"}, + } + for _, tt := range tests { + t.Run(tt.arg, func(t *testing.T) { + name, version := parsePluginVersion(tt.arg) + assert.Equal(t, tt.wantName, name) + assert.Equal(t, tt.wantVersion, version) + }) + } +} + +func TestPluginUpdateComplete(t *testing.T) { + tests := []struct { + name string + args []string + wantPlugins map[string]string + wantErr string + }{ + { + name: "no args", + args: []string{}, + wantErr: "please provide plugin name to update", + }, + { + name: "single plugin no version", + args: []string{"myplugin"}, + wantPlugins: map[string]string{"myplugin": ""}, + }, + { + name: "single plugin with inline version", + args: []string{"myplugin@1.2.3"}, + wantPlugins: map[string]string{"myplugin": "1.2.3"}, + }, + { + name: "multiple plugins no versions", + args: []string{"plugin-a", "plugin-b"}, + wantPlugins: map[string]string{"plugin-a": "", "plugin-b": ""}, + }, + { + name: "multiple plugins with inline versions", + args: []string{"plugin-a@1.0.0", "plugin-b@2.0.0"}, + wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": "2.0.0"}, + }, + { + name: "multiple plugins each with different exact versions", + args: []string{"plugin-a@v1.2.3", "plugin-b@2.0.0", "plugin-c@3.0.0"}, + wantPlugins: map[string]string{"plugin-a": "v1.2.3", "plugin-b": "2.0.0", "plugin-c": "3.0.0"}, + }, + { + name: "multiple plugins mixed versions", + args: []string{"plugin-a@1.0.0", "plugin-b"}, + wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": ""}, + }, + { + name: "multiple plugins mixed with latest in the middle", + args: []string{"plugin-a@1.0.0", "plugin-b", "plugin-c@3.0.0"}, + wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": "", "plugin-c": "3.0.0"}, + }, + { + name: "duplicate plugin name errors", + args: []string{"myplugin@1.0.0", "myplugin@2.0.0"}, + wantErr: `plugin "myplugin" specified more than once`, + }, + { + name: "empty plugin name errors", + args: []string{"@1.0.0"}, + wantErr: `invalid plugin reference "@1.0.0": plugin name must not be empty`, + }, + { + name: "tilde range version rejected", + args: []string{"myplugin@~1.2"}, + wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "caret range version rejected", + args: []string{"myplugin@^1.2.3"}, + wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "gte constraint rejected", + args: []string{"myplugin@>=1.0.0"}, + wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "wildcard version rejected", + args: []string{"myplugin@1.x"}, + wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "range constraint rejected", + args: []string{"myplugin@>=1.0.0, <2.0.0"}, + wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "garbage version rejected", + args: []string{"myplugin@notaversion"}, + wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + { + name: "range rejected among multiple plugins", + args: []string{"plugin-a@1.0.0", "plugin-b@~2.0"}, + wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &pluginUpdateOptions{} + err := o.complete(tt.args) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantPlugins, o.plugins) + }) + } +} From 18d4541ff86ed4ea6504c51dba32a06a9fc7cac7 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:09:52 +0100 Subject: [PATCH 05/18] Updated pluginUpdateDesc Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 8f1fd30d0..f85978a3b 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -36,11 +36,13 @@ type pluginUpdateOptions struct { const pluginUpdateDesc = `Update one or more Helm plugins. -An exact version can be supplied per-plugin using the @version syntax: +An exact semver version can be supplied per-plugin using the @version syntax: helm plugin update myplugin@1.2.3 otherplugin@2.0.0 helm plugin update myplugin@v1.0.0 +Range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported. + If no version is given for a plugin it is updated to the latest version: helm plugin update myplugin otherplugin From dc5248a7ba6d0a45869caef0ae276330972e2d1d Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:03:20 +0100 Subject: [PATCH 06/18] Updated FindSource signature Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/plugin/installer/installer.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index 164f2cb59..fe96b1c56 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -157,13 +157,8 @@ func NewForSource(source, version string) (installer Installer, err error) { } // FindSource determines the correct Installer for the given source. -// Version is optional; if not provided, it defaults to empty string. -func FindSource(location string, version ...string) (Installer, error) { - v := "" - if len(version) > 0 { - v = version[0] - } - installer, err := existingVCSRepo(location, v) +func FindSource(location, version string) (Installer, error) { + installer, err := existingVCSRepo(location, version) if err != nil && err.Error() == "Cannot detect VCS" { slog.Warn( "cannot get information about plugin source", From 74f71f10b0249fb172e0c917ab5732edfc69da71 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:04:21 +0100 Subject: [PATCH 07/18] Reject v-versions Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 6 +++--- pkg/cmd/plugin_update_test.go | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index f85978a3b..488c8f8ee 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -41,7 +41,7 @@ An exact semver version can be supplied per-plugin using the @version syntax: helm plugin update myplugin@1.2.3 otherplugin@2.0.0 helm plugin update myplugin@v1.0.0 -Range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported. +Range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0, v1.0.0) are not supported. If no version is given for a plugin it is updated to the latest version: @@ -85,8 +85,8 @@ func (o *pluginUpdateOptions) complete(args []string) error { return fmt.Errorf("plugin %q specified more than once", name) } if version != "" { - if _, err := semver.NewVersion(version); err != nil { - return fmt.Errorf("invalid version %q for plugin %q: must be an exact version (e.g. 1.2.3 or v1.2.3)", version, name) + if _, err := semver.StrictNewVersion(version); err != nil { + return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the \"v\" prefix is not allowed", version, name) } } o.plugins[name] = version diff --git a/pkg/cmd/plugin_update_test.go b/pkg/cmd/plugin_update_test.go index 5c72074ab..ec7256d3a 100644 --- a/pkg/cmd/plugin_update_test.go +++ b/pkg/cmd/plugin_update_test.go @@ -80,8 +80,8 @@ func TestPluginUpdateComplete(t *testing.T) { }, { name: "multiple plugins each with different exact versions", - args: []string{"plugin-a@v1.2.3", "plugin-b@2.0.0", "plugin-c@3.0.0"}, - wantPlugins: map[string]string{"plugin-a": "v1.2.3", "plugin-b": "2.0.0", "plugin-c": "3.0.0"}, + args: []string{"plugin-a@1.2.3", "plugin-b@2.0.0", "plugin-c@3.0.0"}, + wantPlugins: map[string]string{"plugin-a": "1.2.3", "plugin-b": "2.0.0", "plugin-c": "3.0.0"}, }, { name: "multiple plugins mixed versions", @@ -103,40 +103,45 @@ func TestPluginUpdateComplete(t *testing.T) { args: []string{"@1.0.0"}, wantErr: `invalid plugin reference "@1.0.0": plugin name must not be empty`, }, + { + name: "v-prefixed version rejected", + args: []string{"myplugin@v1.2.3"}, + wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + }, { name: "tilde range version rejected", args: []string{"myplugin@~1.2"}, - wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "caret range version rejected", args: []string{"myplugin@^1.2.3"}, - wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "gte constraint rejected", args: []string{"myplugin@>=1.0.0"}, - wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "wildcard version rejected", args: []string{"myplugin@1.x"}, - wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "range constraint rejected", args: []string{"myplugin@>=1.0.0, <2.0.0"}, - wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "garbage version rejected", args: []string{"myplugin@notaversion"}, - wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "range rejected among multiple plugins", args: []string{"plugin-a@1.0.0", "plugin-b@~2.0"}, - wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact version (e.g. 1.2.3 or v1.2.3)`, + wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, } for _, tt := range tests { From 56e3c7c12f8d6373a2b439bff3354778399367cc Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:05:59 +0100 Subject: [PATCH 08/18] Updated message Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 488c8f8ee..b8c43dcb0 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -86,7 +86,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { } if version != "" { if _, err := semver.StrictNewVersion(version); err != nil { - return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the \"v\" prefix is not allowed", version, name) + return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3)", version, name) } } o.plugins[name] = version From 93a86d2fda262c6a086ef3ff0633d114dd8d7bb5 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:51:31 +0100 Subject: [PATCH 09/18] Fixed descriptions and tests Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/plugin/installer/vcs_installer_test.go | 6 +++--- pkg/cmd/plugin_update.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/plugin/installer/vcs_installer_test.go b/internal/plugin/installer/vcs_installer_test.go index 7149ed71c..54a344ad0 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -180,8 +180,8 @@ func TestVCSInstallerUpdateWithVersion(t *testing.T) { t.Fatal(err) } - // Now test update with specific version constraint - vcsInstaller.Version = "~0.1.0" + // Now test update with specific version + vcsInstaller.Version = "0.1.1" if err := Update(vcsInstaller); err != nil { t.Fatal(err) } @@ -189,7 +189,7 @@ func TestVCSInstallerUpdateWithVersion(t *testing.T) { t.Fatalf("expected version '0.1.1', got %q", repo.current) } - // Test update with different version constraint + // Test update with different version vcsInstaller.Version = "0.2.0" if err := Update(vcsInstaller); err != nil { t.Fatal(err) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index b8c43dcb0..6bffe1658 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -39,7 +39,7 @@ const pluginUpdateDesc = `Update one or more Helm plugins. An exact semver version can be supplied per-plugin using the @version syntax: helm plugin update myplugin@1.2.3 otherplugin@2.0.0 - helm plugin update myplugin@v1.0.0 + helm plugin update myplugin@1.0.0 Range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0, v1.0.0) are not supported. @@ -86,7 +86,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { } if version != "" { if _, err := semver.StrictNewVersion(version); err != nil { - return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3)", version, name) + return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the 'v' prefix is not allowed", version, name) } } o.plugins[name] = version From 59efc297c50e47313410b4192f8ab4bf9d3d726c Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:44:47 +0100 Subject: [PATCH 10/18] Update pkg/cmd/plugin_update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 6bffe1658..bbb12f0d4 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -57,7 +57,12 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command { Short: "update one or more Helm plugins", Long: pluginUpdateDesc, ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp + ignoredNames := make([]string, len(args)) + for i, arg := range args { + name, _ := parsePluginVersion(arg) + ignoredNames[i] = name + } + return compListPlugins(toComplete, ignoredNames), cobra.ShellCompDirectiveNoFileComp }, PreRunE: func(_ *cobra.Command, args []string) error { return o.complete(args) From 81320a311c67ab74e276270123550e05214dce88 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:18:23 +0100 Subject: [PATCH 11/18] Let's make copilot happy again Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index bbb12f0d4..3bb2a5c6c 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -36,13 +36,14 @@ type pluginUpdateOptions struct { const pluginUpdateDesc = `Update one or more Helm plugins. -An exact semver version can be supplied per-plugin using the @version syntax: +An exact semver version can be pinned per-plugin using the @version syntax. +Only exact versions (e.g. 1.2.3) are accepted; the "v" prefix and semver range +constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported for updates. +This ensures a deterministic, reproducible update to a known version: helm plugin update myplugin@1.2.3 otherplugin@2.0.0 helm plugin update myplugin@1.0.0 -Range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0, v1.0.0) are not supported. - If no version is given for a plugin it is updated to the latest version: helm plugin update myplugin otherplugin @@ -91,7 +92,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { } if version != "" { if _, err := semver.StrictNewVersion(version); err != nil { - return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the 'v' prefix is not allowed", version, name) + return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the \"v\" prefix is not allowed", version, name) } } o.plugins[name] = version From 946100a5881d7f9a2accfac02a67ad2382466896 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:49:11 +0100 Subject: [PATCH 12/18] Update pkg/cmd/plugin_update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 3bb2a5c6c..b9b64f118 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -92,7 +92,11 @@ func (o *pluginUpdateOptions) complete(args []string) error { } if version != "" { if _, err := semver.StrictNewVersion(version); err != nil { - return fmt.Errorf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); the \"v\" prefix is not allowed", version, name) + errMsg := fmt.Sprintf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported", version, name) + if strings.HasPrefix(version, "v") { + errMsg += `; the "v" prefix is not allowed` + } + return fmt.Errorf("%s", errMsg) } } o.plugins[name] = version From 717f475e768bbb59eb10536ab08ecd186b313dcc Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:31:13 +0100 Subject: [PATCH 13/18] fix(plugin): align test error expectations with context-sensitive validation messages Update plugin_update_test.go so expected errors match the implementation: non-v-prefixed versions no longer incorrectly expect the 'v' prefix warning, and v-prefixed versions now include the full message with both the range constraint notice and the 'v' prefix warning. Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/plugin_update_test.go b/pkg/cmd/plugin_update_test.go index ec7256d3a..78b5b1374 100644 --- a/pkg/cmd/plugin_update_test.go +++ b/pkg/cmd/plugin_update_test.go @@ -106,42 +106,42 @@ func TestPluginUpdateComplete(t *testing.T) { { name: "v-prefixed version rejected", args: []string{"myplugin@v1.2.3"}, - wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported; the "v" prefix is not allowed`, }, { name: "tilde range version rejected", args: []string{"myplugin@~1.2"}, - wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "caret range version rejected", args: []string{"myplugin@^1.2.3"}, - wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "gte constraint rejected", args: []string{"myplugin@>=1.0.0"}, - wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "wildcard version rejected", args: []string{"myplugin@1.x"}, - wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "range constraint rejected", args: []string{"myplugin@>=1.0.0, <2.0.0"}, - wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "garbage version rejected", args: []string{"myplugin@notaversion"}, - wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, { name: "range rejected among multiple plugins", args: []string{"plugin-a@1.0.0", "plugin-b@~2.0"}, - wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, + wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, }, } for _, tt := range tests { From 57827f85995249862a3e01bb515d642ecbf912d7 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:17:26 +0100 Subject: [PATCH 14/18] fix(plugin): simplify version validation error and align test expectations Remove verbose 'semver range constraints' suffix from the error message. The 'v' prefix hint is still appended only when the version starts with 'v'. Update test expectations to match the simplified messages. Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 2 +- pkg/cmd/plugin_update_test.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index b9b64f118..d63d63213 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -92,7 +92,7 @@ func (o *pluginUpdateOptions) complete(args []string) error { } if version != "" { if _, err := semver.StrictNewVersion(version); err != nil { - errMsg := fmt.Sprintf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported", version, name) + errMsg := fmt.Sprintf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3)", version, name) if strings.HasPrefix(version, "v") { errMsg += `; the "v" prefix is not allowed` } diff --git a/pkg/cmd/plugin_update_test.go b/pkg/cmd/plugin_update_test.go index 78b5b1374..4458457d2 100644 --- a/pkg/cmd/plugin_update_test.go +++ b/pkg/cmd/plugin_update_test.go @@ -106,42 +106,42 @@ func TestPluginUpdateComplete(t *testing.T) { { name: "v-prefixed version rejected", args: []string{"myplugin@v1.2.3"}, - wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported; the "v" prefix is not allowed`, + wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`, }, { name: "tilde range version rejected", args: []string{"myplugin@~1.2"}, - wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "caret range version rejected", args: []string{"myplugin@^1.2.3"}, - wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "gte constraint rejected", args: []string{"myplugin@>=1.0.0"}, - wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "wildcard version rejected", args: []string{"myplugin@1.x"}, - wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "range constraint rejected", args: []string{"myplugin@>=1.0.0, <2.0.0"}, - wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "garbage version rejected", args: []string{"myplugin@notaversion"}, - wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3)`, }, { name: "range rejected among multiple plugins", args: []string{"plugin-a@1.0.0", "plugin-b@~2.0"}, - wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact semver version (e.g. 1.2.3); semver range constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported`, + wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact semver version (e.g. 1.2.3)`, }, } for _, tt := range tests { From 00817781310cf7d9bf67816c9d8d2990da44de2f Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:27:19 +0200 Subject: [PATCH 15/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/plugin/installer/vcs_installer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/installer/vcs_installer_test.go b/internal/plugin/installer/vcs_installer_test.go index 54a344ad0..85e398fb1 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -152,7 +152,7 @@ func TestVCSInstallerUpdate(t *testing.T) { func TestVCSInstallerUpdateWithVersion(t *testing.T) { ensure.HelmHome(t) - if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0o755); err != nil { t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) } From 6022949f5c2bd53ae6e975ab4f981d46f4033030 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:21:05 +0200 Subject: [PATCH 16/18] Fix golangci-lint Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index d63d63213..0f3630afa 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -23,7 +23,6 @@ import ( "path/filepath" "strings" - "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "helm.sh/helm/v4/internal/plugin" @@ -116,7 +115,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error { for name, version := range o.plugins { if found := findPlugin(installed, name); found != nil { if err := updatePlugin(found, version); err != nil { - errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err)) + errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%w)", name, err)) } else { fmt.Fprintf(out, "Updated plugin: %s\n", name) } From cdfb7254bd19bc3415fac7b7fbd1800529f6aafc Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:37:08 +0200 Subject: [PATCH 17/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 0f3630afa..286b05930 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -82,6 +82,9 @@ func (o *pluginUpdateOptions) complete(args []string) error { o.plugins = make(map[string]string, len(args)) for _, arg := range args { + if strings.HasSuffix(arg, "@") { + return fmt.Errorf("invalid plugin reference %q: missing version after @", arg) + } name, version := parsePluginVersion(arg) if name == "" { return fmt.Errorf("invalid plugin reference %q: plugin name must not be empty", arg) From 2af984df88cdce040f5d949cf847dee5db14de85 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:38:29 +0200 Subject: [PATCH 18/18] Reimport semver Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/plugin_update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 286b05930..d7a279a34 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -23,6 +23,7 @@ import ( "path/filepath" "strings" + "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "helm.sh/helm/v4/internal/plugin"