Fix helmfile init failing to update outdated helm plugins with Helm v4 (#2554)

* Initial plan

* Fix helmfile init not updating outdated helm plugins with Helm v4

- UpdatePlugin now handles secrets plugin with Helm 4 by using the split
  plugin architecture (uninstall old + install via installHelmSecretsV4)
- UpdatePlugin falls back to uninstall + reinstall when helm plugin update
  fails (e.g., with Helm 4 or tarball-installed plugins)
- Fix string-based semver comparison for helm-secrets version check in
  both AddPlugin and UpdatePlugin using proper semver comparison
- Add helmSecretsRequiresSplitInstall helper for reuse and correctness
- Add tests for update failure fallback scenarios

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/533f1b1c-dda6-4934-af27-051e4eaa9927

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Address reviewer feedback: preserve update error context and add version assertions in tests

- exec.go: include original update error in fallback log message; wrap both
  errors (update + reinstall) when reinstall also fails so callers get full context
- init_test.go: add semver import and GetPluginVersion assertions after
  CheckHelmPlugins to verify plugins are at required versions on disk

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/c784db7d-7d4c-40a0-97f0-a31eb8901cd6

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Address second round of reviewer feedback

- exec.go: rename UpdatePlugin parameter path→repo for clarity
- exec.go: fix uninstallPlugin to only emit INFO log when err == nil
- exec_test.go: add Test_helmSecretsRequiresSplitInstall table-driven tests
  covering v4.6.9, v4.7.0, v4.8.0, v4.10.0, pre-release, invalid and empty
- exec_test.go: add Test_UpdatePlugin_Helm4SecretsUsesUninstallReinstall
  verifying that Helm 4 + secrets uses uninstall+reinstall (not plugin update)

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/cbd3f8c9-ec7d-4500-b168-cb1c2f7c87bc

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Add len(args) >= 3 guards in test mock for plugin update/uninstall cases

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/ea0f5afc-d52d-473b-b759-853a8f841a26

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Return early with combined error when uninstall fails in UpdatePlugin fallback

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/bb9a675c-309d-4b06-83d4-a6fe078dce64

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
Copilot
2026-04-25 11:04:23 +08:00
committed by GitHub
co-authored by yxxhero
parent 261c605305
commit c57134cda7
4 changed files with 285 additions and 5 deletions
+52 -4
View File
@@ -851,7 +851,7 @@ func (helm *execer) AddPlugin(name, path, version string) error {
helm.logger.Infof("Install helm plugin %v", name)
// Special handling for helm-secrets 4.7.0+ with Helm 4 which uses split plugin architecture
if name == "secrets" && version >= "v4.7.0" && helm.IsHelm4() {
if name == "secrets" && helmSecretsRequiresSplitInstall(version) && helm.IsHelm4() {
return helm.installHelmSecretsV4(version)
}
@@ -906,11 +906,59 @@ func (helm *execer) installHelmSecretsV4(version string) error {
return nil
}
func (helm *execer) UpdatePlugin(name string) error {
helm.logger.Infof("Update helm plugin %v", name)
// helmSecretsV4SplitMinVersion is the minimum helm-secrets version that uses the
// split plugin architecture (secrets, secrets-getter, secrets-post-renderer) with Helm 4.
var helmSecretsV4SplitMinVersion = semver.MustParse("4.7.0")
// helmSecretsRequiresSplitInstall returns true when the given helm-secrets version
// requires the split plugin architecture introduced in v4.7.0 for Helm 4.
func helmSecretsRequiresSplitInstall(version string) bool {
v, err := semver.NewVersion(version)
if err != nil {
return false
}
return !v.LessThan(helmSecretsV4SplitMinVersion)
}
func (helm *execer) uninstallPlugin(name string) error {
helm.logger.Infof("Uninstalling helm plugin %v", name)
out, err := helm.exec([]string{"plugin", "uninstall", name}, map[string]string{}, nil)
if err == nil {
helm.info(out)
}
return err
}
func (helm *execer) UpdatePlugin(name, repo, version string) error {
helm.logger.Infof("Updating helm plugin %v", name)
// Special handling for helm-secrets 4.7.0+ with Helm 4 which uses split plugin architecture
if name == "secrets" && helmSecretsRequiresSplitInstall(version) && helm.IsHelm4() {
// Uninstall existing secrets plugins; ignore errors as some may not exist
for _, secretsPlugin := range []string{"secrets", "secrets-getter", "secrets-post-renderer"} {
if err := helm.uninstallPlugin(secretsPlugin); err != nil {
helm.logger.Debugf("Failed to uninstall helm plugin %v (may not exist): %v", secretsPlugin, err)
}
}
return helm.installHelmSecretsV4(version)
}
// Try standard helm plugin update
out, err := helm.exec([]string{"plugin", "update", name}, map[string]string{}, nil)
helm.info(out)
return err
if err != nil {
// If standard update failed, fall back to uninstall + reinstall with specific version
updateErr := err
helm.logger.Infof("helm plugin update %v failed (%v), falling back to reinstall with version %v", name, updateErr, version)
if uninstallErr := helm.uninstallPlugin(name); uninstallErr != nil {
return fmt.Errorf("helm plugin update failed (%w) and uninstall for reinstall also failed: %w", updateErr, uninstallErr)
}
if reinstallErr := helm.AddPlugin(name, repo, version); reinstallErr != nil {
return fmt.Errorf("helm plugin update failed (%w) and reinstall also failed: %w", updateErr, reinstallErr)
}
return nil
}
return nil
}
func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {