From 1836c598f06377fd1571702fb2e0642f004cedef Mon Sep 17 00:00:00 2001 From: Andreas Sommer Date: Wed, 3 Jul 2024 12:45:08 +0200 Subject: [PATCH 001/132] Make error message instructional for the case of lock file being out of date Signed-off-by: Andreas Sommer --- pkg/cmd/install.go | 2 +- pkg/cmd/testdata/output/upgrade-with-missing-dependencies.txt | 2 +- pkg/cmd/upgrade.go | 2 +- pkg/downloader/manager.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 361d91e5f..8a27627bd 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -304,7 +304,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, fmt.Errorf("failed reloading chart after repo update: %w", err) } } else { - return nil, fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err) + return nil, fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: %w", err) } } } diff --git a/pkg/cmd/testdata/output/upgrade-with-missing-dependencies.txt b/pkg/cmd/testdata/output/upgrade-with-missing-dependencies.txt index b2c154a80..cb0a3a167 100644 --- a/pkg/cmd/testdata/output/upgrade-with-missing-dependencies.txt +++ b/pkg/cmd/testdata/output/upgrade-with-missing-dependencies.txt @@ -1 +1 @@ -Error: an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 +Error: an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 74061caf7..2e709a12d 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -200,7 +200,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if req := ch.Metadata.Dependencies; req != nil { if err := action.CheckDependencies(ch, req); err != nil { - err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err) + err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: %w", err) if client.DependencyUpdate { man := &downloader.Manager{ Out: out, diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 8b77a77c0..32d454e81 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -127,7 +127,7 @@ func (m *Manager) Build() error { return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") } } else { - return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies") + return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies with 'helm dependency update'") } } From 906f87ce1d6d7a46bc68601c1217623e40b78877 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 26 Nov 2025 21:59:49 +0000 Subject: [PATCH 002/132] feat: add ownership verification before deleting resources during uninstall Fixes #31333 Helm uninstall now verifies resource ownership before deletion by checking Helm labels and annotations. Resources not owned by the release being uninstalled are skipped with warnings, preventing accidental deletion of resources belonging to other releases. Signed-off-by: Evans Mungai --- pkg/action/uninstall.go | 75 ++++++++++++++++++++++++++++-- pkg/action/validate.go | 49 ++++++++++++++++++++ pkg/action/validate_test.go | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 4 deletions(-) diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index d5474490c..71db30a19 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -81,6 +81,46 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) if err != nil { return nil, err } + + // Verify ownership in dry-run mode to show what would actually be deleted + manifests := releaseutil.SplitManifests(r.Manifest) + _, files, err := releaseutil.SortManifests(manifests, nil, releaseutil.UninstallOrder) + if err == nil { + filesToKeep, filesToDelete := filterManifestsToKeep(files) + + var builder strings.Builder + for _, file := range filesToDelete { + builder.WriteString("\n---\n" + file.Content) + } + + resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) + if err == nil && len(resources) > 0 { + _, unownedResources, err := verifyOwnershipBeforeDelete(resources, r.Name, r.Namespace) + if err == nil && len(unownedResources) > 0 { + u.cfg.Logger().Warn("dry-run: resources would be skipped because they are not owned by this release", + "release", r.Name, + "count", len(unownedResources)) + for _, info := range unownedResources { + u.cfg.Logger().Warn("dry-run: would skip resource", + "kind", info.Mapping.GroupVersionKind.Kind, + "name", info.Name, + "namespace", info.Namespace) + } + } + } + + // Include kept resources in dry-run info + if len(filesToKeep) > 0 { + var kept strings.Builder + kept.WriteString("These resources were kept due to the resource policy:\n") + for _, f := range filesToKeep { + fmt.Fprintf(&kept, "[%s] %s\n", f.Head.Kind, f.Head.Metadata.Name) + } + res := &releasei.UninstallReleaseResponse{Release: r, Info: kept.String()} + return res, nil + } + } + return &releasei.UninstallReleaseResponse{Release: r}, nil } @@ -256,13 +296,40 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri if err != nil { return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)} } + + // Verify ownership before deleting resources + var ownedResources, unownedResources kube.ResourceList if len(resources) > 0 { - _, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation)) + ownedResources, unownedResources, err = verifyOwnershipBeforeDelete(resources, rel.Name, rel.Namespace) + if err != nil { + return nil, "", []error{fmt.Errorf("unable to verify resource ownership: %w", err)} + } + + // Log warnings for unowned resources + if len(unownedResources) > 0 { + for _, info := range unownedResources { + u.cfg.Logger().Warn("skipping delete of resource not owned by this release", + "kind", info.Mapping.GroupVersionKind.Kind, + "name", info.Name, + "namespace", info.Namespace, + "release", rel.Name) + } + kept.WriteString(fmt.Sprintf("\n%d resource(s) were not deleted because they are not owned by this release:\n", len(unownedResources))) + for _, info := range unownedResources { + fmt.Fprintf(&kept, "[%s] %s\n", info.Mapping.GroupVersionKind.Kind, info.Name) + } + } + + // Delete only owned resources + if len(ownedResources) > 0 { + u.cfg.Logger().Debug("deleting resources part of this release", "count", len(ownedResources), "propagation", u.DeletionPropagation) + _, errs = u.cfg.KubeClient.Delete(ownedResources, parseCascadingFlag(u.DeletionPropagation, u.cfg.Logger())) + } } - return resources, kept.String(), errs + return ownedResources, kept.String(), errs } -func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation { +func parseCascadingFlag(cascadingFlag string, logger *slog.Logger) v1.DeletionPropagation { switch cascadingFlag { case "orphan": return v1.DeletePropagationOrphan @@ -271,7 +338,7 @@ func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation { case "background": return v1.DeletePropagationBackground default: - slog.Debug("uninstall: given cascade value, defaulting to delete propagation background", "value", cascadingFlag) + logger.Debug("uninstall: given cascade value, defaulting to delete propagation background", "value", cascadingFlag) return v1.DeletePropagationBackground } } diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 1bef5a742..3a9d63253 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -93,6 +93,55 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN return requireUpdate, err } +// verifyOwnershipBeforeDelete checks that resources in the list are owned by the specified release. +// It returns two lists: owned resources (safe to delete) and unowned resources (should skip). +// Resources that are not found are considered owned (already deleted, safe to attempt delete). +func verifyOwnershipBeforeDelete(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, kube.ResourceList, error) { + var owned kube.ResourceList + var unowned kube.ResourceList + + err := resources.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + // If client is not available, skip verification (test scenario or build failure) + if info.Client == nil { + infoCopy := *info + owned.Append(&infoCopy) + return nil + } + + helper := resource.NewHelper(info.Client, info.Mapping) + existing, err := helper.Get(info.Namespace, info.Name) + if err != nil { + if apierrors.IsNotFound(err) { + // Resource already deleted, skip deletion + return nil + } + // Cannot fetch resource (network/permission issue), cannot verify ownership + infoCopy := *info + unowned.Append(&infoCopy) + return nil + } + + // Verify ownership of the existing resource + if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { + // Resource not owned by this release, cannot delete + infoCopy := *info + unowned.Append(&infoCopy) + return nil + } + + // Resource is owned by this release, can delete + infoCopy := *info + owned.Append(&infoCopy) + return nil + }) + + return owned, unowned, err +} + func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error { lbls, err := accessor.Labels(obj) if err != nil { diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go index 879a5fa4f..d3e0edcde 100644 --- a/pkg/action/validate_test.go +++ b/pkg/action/validate_test.go @@ -210,6 +210,98 @@ func TestCheckOwnership(t *testing.T) { assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`) } +func TestVerifyOwnershipBeforeDelete(t *testing.T) { + var ( + releaseName = "rel-a" + releaseNamespace = "ns-a" + labels = map[string]string{ + appManagedByLabel: appManagedByHelm, + } + annotations = map[string]string{ + helmReleaseNameAnnotation: releaseName, + helmReleaseNamespaceAnnotation: releaseNamespace, + } + wrongAnnotations = map[string]string{ + helmReleaseNameAnnotation: "rel-b", + helmReleaseNamespaceAnnotation: releaseNamespace, + } + ) + + // Test all resources properly owned + t.Run("all resources owned", func(t *testing.T) { + owned1 := newDeploymentWithOwner("owned1", "ns-a", labels, annotations) + owned2 := newDeploymentWithOwner("owned2", "ns-a", labels, annotations) + resources := kube.ResourceList{owned1, owned2} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 2) + assert.Len(t, unownedList, 0) + }) + + // Test mix of owned and unowned resources + t.Run("mixed ownership", func(t *testing.T) { + owned := newDeploymentWithOwner("owned", "ns-a", labels, annotations) + unowned := newDeploymentWithOwner("unowned", "ns-a", labels, wrongAnnotations) + resources := kube.ResourceList{owned, unowned} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 1) + assert.Len(t, unownedList, 1) + assert.Equal(t, "owned", ownedList[0].Name) + assert.Equal(t, "unowned", unownedList[0].Name) + }) + + // Test resource not found (should be skipped - not in either list) + t.Run("resource not found", func(t *testing.T) { + missing := newMissingDeployment("missing", "ns-a") + resources := kube.ResourceList{missing} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 0) + assert.Len(t, unownedList, 0) + }) + + // Test resource with no ownership metadata + t.Run("no ownership metadata", func(t *testing.T) { + noMeta := newDeploymentWithOwner("no-meta", "ns-a", nil, nil) + resources := kube.ResourceList{noMeta} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 0) + assert.Len(t, unownedList, 1) + }) + + // Test resource owned by different release + t.Run("owned by different release", func(t *testing.T) { + otherRelease := newDeploymentWithOwner("other", "ns-a", labels, wrongAnnotations) + resources := kube.ResourceList{otherRelease} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 0) + assert.Len(t, unownedList, 1) + }) + + // Test mixed scenario: owned, unowned, and missing resources + t.Run("mixed with missing resources", func(t *testing.T) { + owned := newDeploymentWithOwner("owned", "ns-a", labels, annotations) + unowned := newDeploymentWithOwner("unowned", "ns-a", labels, wrongAnnotations) + missing := newMissingDeployment("missing", "ns-a") + resources := kube.ResourceList{owned, unowned, missing} + + ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + assert.NoError(t, err) + assert.Len(t, ownedList, 1) + assert.Len(t, unownedList, 1) + assert.Equal(t, "owned", ownedList[0].Name) + assert.Equal(t, "unowned", unownedList[0].Name) + }) +} + func TestSetMetadataVisitor(t *testing.T) { var ( err error From 6d5b5aab32be039f8f56b0c497a7f4af28ce21f0 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 26 Nov 2025 22:22:57 +0000 Subject: [PATCH 003/132] Additional logging for ownership verification in dry-run mode Signed-off-by: Evans Mungai --- pkg/action/uninstall.go | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 71db30a19..7e78f820e 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -95,16 +95,30 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) if err == nil && len(resources) > 0 { - _, unownedResources, err := verifyOwnershipBeforeDelete(resources, r.Name, r.Namespace) - if err == nil && len(unownedResources) > 0 { - u.cfg.Logger().Warn("dry-run: resources would be skipped because they are not owned by this release", - "release", r.Name, - "count", len(unownedResources)) - for _, info := range unownedResources { - u.cfg.Logger().Warn("dry-run: would skip resource", - "kind", info.Mapping.GroupVersionKind.Kind, - "name", info.Name, - "namespace", info.Namespace) + ownedResources, unownedResources, err := verifyOwnershipBeforeDelete(resources, r.Name, r.Namespace) + if err == nil { + if len(unownedResources) > 0 { + u.cfg.Logger().Warn("dry-run: resources would be skipped because they are not owned by this release", + "release", r.Name, + "count", len(unownedResources)) + for _, info := range unownedResources { + u.cfg.Logger().Warn("dry-run: would skip resource", + "kind", info.Mapping.GroupVersionKind.Kind, + "name", info.Name, + "namespace", info.Namespace) + } + } + + if len(ownedResources) > 0 { + u.cfg.Logger().Debug("dry-run: resources would be deleted", + "release", r.Name, + "count", len(ownedResources)) + for _, info := range ownedResources { + u.cfg.Logger().Debug("dry-run: would delete resource", + "kind", info.Mapping.GroupVersionKind.Kind, + "name", info.Name, + "namespace", info.Namespace) + } } } } From f552950b05066d4d6a9d6cf9a91c26656f4a90fc Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 27 Nov 2025 15:32:37 +0000 Subject: [PATCH 004/132] Add ownership verification to uninstall action This commit adds ownership verification to the uninstall action. It checks that the resource to be deleted is owned by the chart before deleting it. Fixes: #31333 Signed-off-by: Evans Mungai --- pkg/action/uninstall.go | 8 +- pkg/action/uninstall_test.go | 186 ++++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 2 deletions(-) diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 7e78f820e..c1e5b00e7 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -336,7 +336,13 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri // Delete only owned resources if len(ownedResources) > 0 { - u.cfg.Logger().Debug("deleting resources part of this release", "count", len(ownedResources), "propagation", u.DeletionPropagation) + for _, info := range ownedResources { + u.cfg.Logger().Debug("deleting resource owned by this release", + "kind", info.Mapping.GroupVersionKind.Kind, + "name", info.Name, + "namespace", info.Namespace, + "release", rel.Name) + } _, errs = u.cfg.KubeClient.Delete(ownedResources, parseCascadingFlag(u.DeletionPropagation, u.cfg.Logger())) } } diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index fba1e391f..bed9c04ac 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -17,9 +17,11 @@ limitations under the License. package action import ( + "bytes" "errors" "fmt" "io" + "log/slog" "testing" "github.com/stretchr/testify/assert" @@ -147,9 +149,16 @@ func TestUninstallRelease_Cascade(t *testing.T) { } }` unAction.cfg.Releases.Create(rel) + + // Create dummy resources with Mapping but no Client - this skips ownership verification + // (nil Client is treated as owned) and goes directly to delete + dummyResources := kube.ResourceList{ + newDeploymentResource("secret", ""), + } + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer.DeleteError = fmt.Errorf("Uninstall with cascade failed") - failer.BuildDummy = true + failer.DummyResources = dummyResources unAction.cfg.KubeClient = failer _, err := unAction.Run(rel.Name) require.Error(t, err) @@ -169,3 +178,178 @@ func TestUninstallRun_UnreachableKubeClient(t *testing.T) { assert.Nil(t, result) assert.ErrorContains(t, err, "connection refused") } + +func TestUninstallRelease_OwnershipVerification(t *testing.T) { + is := assert.New(t) + + // Create a buffer to capture log output + logBuffer := &bytes.Buffer{} + handler := slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}) + + config := actionConfigFixture(t) + config.SetLogger(handler) + + unAction := NewUninstall(config) + unAction.DisableHooks = true + unAction.DryRun = false + unAction.KeepHistory = true + + rel := releaseStub() + rel.Name = "ownership-test" + rel.Namespace = "default" + rel.Manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap + labels: + app.kubernetes.io/managed-by: Helm + annotations: + meta.helm.sh/release-name: ownership-test + meta.helm.sh/release-namespace: default +data: + key: value` + config.Releases.Create(rel) + + // Create dummy resources with proper ownership metadata + labels := map[string]string{ + "app.kubernetes.io/managed-by": "Helm", + } + annotations := map[string]string{ + "meta.helm.sh/release-name": "ownership-test", + "meta.helm.sh/release-namespace": "default", + } + dummyResources := kube.ResourceList{ + newDeploymentWithOwner("owned-deploy", "default", labels, annotations), + } + failer := config.KubeClient.(*kubefake.FailingKubeClient) + failer.DummyResources = dummyResources + + resi, err := unAction.Run(rel.Name) + is.NoError(err) + is.NotNil(resi) + res, err := releaserToV1Release(resi.Release) + is.NoError(err) + is.Equal(common.StatusUninstalled, res.Info.Status) + + // Verify log contains debug message about deleting owned resource + logOutput := logBuffer.String() + is.Contains(logOutput, "deleting resource owned by this release") + is.Contains(logOutput, "owned-deploy") + is.Contains(logOutput, "Deployment") +} + +func TestUninstallRelease_OwnershipVerification_WithKeepPolicy(t *testing.T) { + is := assert.New(t) + + // Create a buffer to capture log output + logBuffer := &bytes.Buffer{} + handler := slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelWarn}) + + config := actionConfigFixture(t) + config.SetLogger(handler) + + unAction := NewUninstall(config) + unAction.DisableHooks = true + unAction.DryRun = false + unAction.KeepHistory = true + + rel := releaseStub() + rel.Name = "keep-and-ownership" + rel.Namespace = "default" + rel.Manifest = `apiVersion: v1 +kind: Secret +metadata: + name: kept-secret + annotations: + helm.sh/resource-policy: keep + meta.helm.sh/release-name: keep-and-ownership + meta.helm.sh/release-namespace: default + labels: + app.kubernetes.io/managed-by: Helm +type: Opaque +data: + password: cGFzc3dvcmQ= +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: deleted-configmap + labels: + app.kubernetes.io/managed-by: Helm + annotations: + meta.helm.sh/release-name: keep-and-ownership + meta.helm.sh/release-namespace: default +data: + key: value` + config.Releases.Create(rel) + + // Create dummy resources - one unowned to test logging + dummyResources := kube.ResourceList{ + newDeploymentWithOwner("unowned-deploy", "default", nil, nil), + } + failer := config.KubeClient.(*kubefake.FailingKubeClient) + failer.DummyResources = dummyResources + + res, err := unAction.Run(rel.Name) + is.NoError(err) + is.NotNil(res) + // Should contain info about kept resources + is.Contains(res.Info, "kept due to the resource policy") + + // Verify log contains warning about skipped unowned resource + logOutput := logBuffer.String() + is.Contains(logOutput, "skipping delete of resource not owned by this release") + is.Contains(logOutput, "unowned-deploy") +} + +func TestUninstallRelease_DryRun_OwnershipVerification(t *testing.T) { + is := assert.New(t) + + // Create a buffer to capture log output + logBuffer := &bytes.Buffer{} + handler := slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelWarn}) + + config := actionConfigFixture(t) + config.SetLogger(handler) + + unAction := NewUninstall(config) + unAction.DisableHooks = true + unAction.DryRun = true + + rel := releaseStub() + rel.Name = "dryrun-ownership" + rel.Namespace = "default" + rel.Manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap + labels: + app.kubernetes.io/managed-by: Helm + annotations: + meta.helm.sh/release-name: dryrun-ownership + meta.helm.sh/release-namespace: default +data: + key: value` + config.Releases.Create(rel) + + // Create dummy resources - one unowned to test dry-run logging + dummyResources := kube.ResourceList{ + newDeploymentWithOwner("dryrun-unowned-deploy", "default", nil, nil), + } + failer := config.KubeClient.(*kubefake.FailingKubeClient) + failer.DummyResources = dummyResources + + resi, err := unAction.Run(rel.Name) + is.NoError(err) + is.NotNil(resi) + is.NotNil(resi.Release) + res, err := releaserToV1Release(resi.Release) + is.NoError(err) + is.Equal("dryrun-ownership", res.Name) + + // Verify log contains dry-run warning about resources that would be skipped + logOutput := logBuffer.String() + is.Contains(logOutput, "dry-run: would skip resource") + is.Contains(logOutput, "dryrun-unowned-deploy") + is.Contains(logOutput, "Deployment") +} From 6d5f56fa6e7c8e4462d80895fcce87b926e4b8ce Mon Sep 17 00:00:00 2001 From: Scott Rigby Date: Thu, 22 Jan 2026 16:52:59 -0500 Subject: [PATCH 005/132] fix(template): deprecate unused --hide-notes and --render-subchart-notes flags The --hide-notes and --render-subchart-notes flags have no effect for `helm template` since template output never includes notes. Mark these flags as deprecated and hide them from help output. The flags are preserved for backwards compatibility in Helm 4 and will be removed in Helm 5. Fixes #31752 Signed-off-by: Scott Rigby --- pkg/cmd/install.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index d36cd9e34..9ca901d76 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -209,6 +209,26 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in install output. Does not affect presence in chart metadata") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, install will ignore the check for helm annotations and take ownership of the existing resources") + + // For `helm template`, these notes flags are legacy, unused, and should not show in help, but + // must remain accepted for backwards compatibility in Helm 4. Deprecate and hide them for now + // TODO remove these from template command in Helm 5 + if cmd.Name() == "template" { + if err := cmd.Flags().MarkDeprecated("hide-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil { + log.Fatal(err) + } + if err := cmd.Flags().MarkHidden("hide-notes"); err != nil { + log.Fatal(err) + } + + if err := cmd.Flags().MarkDeprecated("render-subchart-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil { + log.Fatal(err) + } + if err := cmd.Flags().MarkHidden("render-subchart-notes"); err != nil { + log.Fatal(err) + } + } + addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) AddWaitFlag(cmd, &client.WaitStrategy) From 300f71b1ebb1b383241a1806a813c6f30a8ff382 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:28:07 +0100 Subject: [PATCH 006/132] feat(history): add rollback revision column to helm history output Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/release/v2/info.go | 27 +++-- pkg/action/rollback.go | 9 +- pkg/action/rollback_test.go | 48 +++++++++ pkg/cmd/history.go | 67 ++++++------ pkg/cmd/history_test.go | 120 ++++++++++++++++++++++ pkg/cmd/testdata/output/history-limit.txt | 6 +- pkg/cmd/testdata/output/history.txt | 10 +- pkg/release/v1/info.go | 27 +++-- 8 files changed, 251 insertions(+), 63 deletions(-) diff --git a/internal/release/v2/info.go b/internal/release/v2/info.go index 038f19409..04a9580fe 100644 --- a/internal/release/v2/info.go +++ b/internal/release/v2/info.go @@ -36,6 +36,8 @@ type Info struct { Description string `json:"description,omitempty"` // Status is the current state of the release Status common.Status `json:"status,omitempty"` + // RollbackRevision is the revision that was rolled back to. Zero means not a rollback. + RollbackRevision int `json:"rollback_revision,omitempty"` // Contains the rendered templates/NOTES.txt if available Notes string `json:"notes,omitempty"` // Contains the deployed resources information @@ -44,13 +46,14 @@ type Info struct { // infoJSON is used for custom JSON marshaling/unmarshaling type infoJSON struct { - FirstDeployed *time.Time `json:"first_deployed,omitempty"` - LastDeployed *time.Time `json:"last_deployed,omitempty"` - Deleted *time.Time `json:"deleted,omitempty"` - Description string `json:"description,omitempty"` - Status common.Status `json:"status,omitempty"` - Notes string `json:"notes,omitempty"` - Resources map[string][]runtime.Object `json:"resources,omitempty"` + FirstDeployed *time.Time `json:"first_deployed,omitempty"` + LastDeployed *time.Time `json:"last_deployed,omitempty"` + Deleted *time.Time `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Status common.Status `json:"status,omitempty"` + RollbackRevision int `json:"rollback_revision,omitempty"` + Notes string `json:"notes,omitempty"` + Resources map[string][]runtime.Object `json:"resources,omitempty"` } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error { } i.Description = tmp.Description i.Status = tmp.Status + i.RollbackRevision = tmp.RollbackRevision i.Notes = tmp.Notes i.Resources = tmp.Resources @@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error { // It omits zero-value time fields from the JSON output. func (i Info) MarshalJSON() ([]byte, error) { tmp := infoJSON{ - Description: i.Description, - Status: i.Status, - Notes: i.Notes, - Resources: i.Resources, + Description: i.Description, + Status: i.Status, + RollbackRevision: i.RollbackRevision, + Notes: i.Notes, + Resources: i.Resources, } if !i.FirstDeployed.IsZero() { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 459569781..6fc449c30 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -176,10 +176,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele Chart: previousRelease.Chart, Config: previousRelease.Config, Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: time.Now(), - Status: common.StatusPendingRollback, - Notes: previousRelease.Info.Notes, + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: time.Now(), + Status: common.StatusPendingRollback, + Notes: previousRelease.Info.Notes, + RollbackRevision: previousVersion, // Because we lose the reference to previous version elsewhere, we set the // message here, and only override it later if we experience failure. Description: fmt.Sprintf("Rollback to %d", previousVersion), diff --git a/pkg/action/rollback_test.go b/pkg/action/rollback_test.go index deb6c7c80..b34adda91 100644 --- a/pkg/action/rollback_test.go +++ b/pkg/action/rollback_test.go @@ -83,3 +83,51 @@ func TestRollback_WaitOptionsPassedDownstream(t *testing.T) { // Verify that WaitOptions were passed to GetWaiter is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter") } + +func TestRollbackSetsRollbackRevision(t *testing.T) { + config := actionConfigFixture(t) + + rel1 := releaseStub() + rel1.Name = "rollback-rev-test" + rel1.Version = 1 + rel1.Info.Status = "superseded" + rel1.ApplyMethod = "csa" + require.NoError(t, config.Releases.Create(rel1)) + + rel2 := releaseStub() + rel2.Name = "rollback-rev-test" + rel2.Version = 2 + rel2.Info.Status = "deployed" + rel2.ApplyMethod = "csa" + require.NoError(t, config.Releases.Create(rel2)) + + client := NewRollback(config) + client.Version = 1 + client.ServerSideApply = "auto" + + require.NoError(t, client.Run("rollback-rev-test")) + + reli, err := config.Releases.Get("rollback-rev-test", 3) + require.NoError(t, err) + rel, err := releaserToV1Release(reli) + require.NoError(t, err) + + assert.Equal(t, 1, rel.Info.RollbackRevision) + assert.Equal(t, "Rollback to 1", rel.Info.Description) +} + +func TestRollbackRevisionZeroForNonRollback(t *testing.T) { + config := actionConfigFixture(t) + + rel := releaseStub() + rel.Name = "non-rollback" + rel.Info.Status = "deployed" + require.NoError(t, config.Releases.Create(rel)) + + reli, err := config.Releases.Get("non-rollback", 1) + require.NoError(t, err) + r, err := releaserToV1Release(reli) + require.NoError(t, err) + + assert.Equal(t, 0, r.Info.RollbackRevision) +} diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index b294a9da7..eb82da684 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -43,11 +43,11 @@ configures the maximum length of the revision list returned. The historical release set is printed as a formatted table, e.g: $ helm history angry-bird - REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION - 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install - 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully - 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 - 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully + REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION + 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install + 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully + 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 2 Rolled back to 2 + 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully ` func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -84,22 +84,24 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } type releaseInfo struct { - Revision int `json:"revision"` - Updated time.Time `json:"updated,omitzero"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + Revision int `json:"revision"` + Updated time.Time `json:"updated,omitzero"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + RollbackRevision int `json:"rollback_revision,omitempty"` + Description string `json:"description"` } // releaseInfoJSON is used for custom JSON marshaling/unmarshaling type releaseInfoJSON struct { - Revision int `json:"revision"` - Updated *time.Time `json:"updated,omitempty"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + Revision int `json:"revision"` + Updated *time.Time `json:"updated,omitempty"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + RollbackRevision int `json:"rollback_revision,omitempty"` + Description string `json:"description"` } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -138,6 +140,7 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error { r.Status = tmp.Status r.Chart = tmp.Chart r.AppVersion = tmp.AppVersion + r.RollbackRevision = tmp.RollbackRevision r.Description = tmp.Description return nil @@ -147,11 +150,12 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error { // It omits zero-value time fields from the JSON output. func (r releaseInfo) MarshalJSON() ([]byte, error) { tmp := releaseInfoJSON{ - Revision: r.Revision, - Status: r.Status, - Chart: r.Chart, - AppVersion: r.AppVersion, - Description: r.Description, + Revision: r.Revision, + Status: r.Status, + Chart: r.Chart, + AppVersion: r.AppVersion, + RollbackRevision: r.RollbackRevision, + Description: r.Description, } if !r.Updated.IsZero() { @@ -173,9 +177,13 @@ func (r releaseHistory) WriteYAML(out io.Writer) error { func (r releaseHistory) WriteTable(out io.Writer) error { tbl := uitable.New() - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "ROLLBACK", "DESCRIPTION") for _, item := range r { - tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) + rollback := "" + if item.RollbackRevision > 0 { + rollback = strconv.Itoa(item.RollbackRevision) + } + tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, rollback, item.Description) } return output.EncodeTable(out, tbl) } @@ -216,11 +224,12 @@ func getReleaseHistory(rls []*release.Release) (history releaseHistory) { a := formatAppVersion(r.Chart) rInfo := releaseInfo{ - Revision: v, - Status: s, - Chart: c, - AppVersion: a, - Description: d, + Revision: v, + Status: s, + Chart: c, + AppVersion: a, + RollbackRevision: r.Info.RollbackRevision, + Description: d, } if !r.Info.LastDeployed.IsZero() { rInfo.Updated = r.Info.LastDeployed diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index d8adc2d19..86fd47bf1 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -27,6 +27,8 @@ import ( "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" + + chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestHistoryCmd(t *testing.T) { @@ -76,6 +78,72 @@ func TestHistoryCmd(t *testing.T) { runTestCmd(t, tests) } +func TestHistoryWithRollback(t *testing.T) { + date := time.Unix(242085845, 0).UTC() + ch := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + AppVersion: "1.0", + }, + } + + rels := []*release.Release{ + { + Name: "angry-bird", + Version: 1, + Info: &release.Info{ + FirstDeployed: date, + LastDeployed: date, + Status: common.StatusSuperseded, + Description: "Install complete", + }, + Chart: ch, + }, + { + Name: "angry-bird", + Version: 2, + Info: &release.Info{ + FirstDeployed: date, + LastDeployed: date, + Status: common.StatusSuperseded, + Description: "Upgrade complete", + }, + Chart: ch, + }, + { + Name: "angry-bird", + Version: 3, + Info: &release.Info{ + FirstDeployed: date, + LastDeployed: date, + Status: common.StatusDeployed, + RollbackRevision: 1, + Description: "Rollback to 1", + }, + Chart: ch, + }, + } + + tests := []cmdTestCase{{ + name: "history with rollback revision", + cmd: "history angry-bird", + rels: rels, + golden: "output/history-with-rollback.txt", + }, { + name: "history with rollback revision json", + cmd: "history angry-bird --output json", + rels: rels, + golden: "output/history-with-rollback.json", + }, { + name: "history with rollback revision yaml", + cmd: "history angry-bird --output yaml", + rels: rels, + golden: "output/history-with-rollback.yaml", + }} + runTestCmd(t, tests) +} + func TestHistoryOutputCompletion(t *testing.T) { outputFlagCompletionTest(t, "history") } @@ -173,6 +241,31 @@ func TestReleaseInfoMarshalJSON(t *testing.T) { }, expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`, }, + { + name: "with rollback revision", + info: releaseInfo{ + Revision: 3, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + RollbackRevision: 1, + Description: "Rollback to 1", + }, + expected: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`, + }, + { + name: "without rollback revision", + info: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + }, + expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`, + }, } for _, tt := range tests { @@ -255,6 +348,31 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) { Description: "Installing", }, }, + { + name: "with rollback revision", + input: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`, + expected: releaseInfo{ + Revision: 3, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + RollbackRevision: 1, + Description: "Rollback to 1", + }, + }, + { + name: "without rollback revision field", + input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install"}`, + expected: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Install", + }, + }, } for _, tt := range tests { @@ -271,6 +389,7 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) { assert.Equal(t, tt.expected.Status, info.Status) assert.Equal(t, tt.expected.Chart, info.Chart) assert.Equal(t, tt.expected.AppVersion, info.AppVersion) + assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision) assert.Equal(t, tt.expected.Description, info.Description) }) } @@ -300,6 +419,7 @@ func TestReleaseInfoRoundTrip(t *testing.T) { assert.Equal(t, original.Status, decoded.Status) assert.Equal(t, original.Chart, decoded.Chart) assert.Equal(t, original.AppVersion, decoded.AppVersion) + assert.Equal(t, original.RollbackRevision, decoded.RollbackRevision) assert.Equal(t, original.Description, decoded.Description) } diff --git a/pkg/cmd/testdata/output/history-limit.txt b/pkg/cmd/testdata/output/history-limit.txt index aee0fadb2..f6f059fb9 100644 --- a/pkg/cmd/testdata/output/history-limit.txt +++ b/pkg/cmd/testdata/output/history-limit.txt @@ -1,3 +1,3 @@ -REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION -3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock +REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock diff --git a/pkg/cmd/testdata/output/history.txt b/pkg/cmd/testdata/output/history.txt index 2a5d69c11..4a51add7f 100644 --- a/pkg/cmd/testdata/output/history.txt +++ b/pkg/cmd/testdata/output/history.txt @@ -1,5 +1,5 @@ -REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION -1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock +REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index f895fdf6c..78f9a22fb 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -36,6 +36,8 @@ type Info struct { Description string `json:"description,omitempty"` // Status is the current state of the release Status common.Status `json:"status,omitempty"` + // RollbackRevision is the revision that was rolled back to. Zero means not a rollback. + RollbackRevision int `json:"rollback_revision,omitempty"` // Contains the rendered templates/NOTES.txt if available Notes string `json:"notes,omitempty"` // Contains the deployed resources information @@ -44,13 +46,14 @@ type Info struct { // infoJSON is used for custom JSON marshaling/unmarshaling type infoJSON struct { - FirstDeployed *time.Time `json:"first_deployed,omitempty"` - LastDeployed *time.Time `json:"last_deployed,omitempty"` - Deleted *time.Time `json:"deleted,omitempty"` - Description string `json:"description,omitempty"` - Status common.Status `json:"status,omitempty"` - Notes string `json:"notes,omitempty"` - Resources map[string][]runtime.Object `json:"resources,omitempty"` + FirstDeployed *time.Time `json:"first_deployed,omitempty"` + LastDeployed *time.Time `json:"last_deployed,omitempty"` + Deleted *time.Time `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Status common.Status `json:"status,omitempty"` + RollbackRevision int `json:"rollback_revision,omitempty"` + Notes string `json:"notes,omitempty"` + Resources map[string][]runtime.Object `json:"resources,omitempty"` } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error { } i.Description = tmp.Description i.Status = tmp.Status + i.RollbackRevision = tmp.RollbackRevision i.Notes = tmp.Notes i.Resources = tmp.Resources @@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error { // It omits zero-value time fields from the JSON output. func (i Info) MarshalJSON() ([]byte, error) { tmp := infoJSON{ - Description: i.Description, - Status: i.Status, - Notes: i.Notes, - Resources: i.Resources, + Description: i.Description, + Status: i.Status, + RollbackRevision: i.RollbackRevision, + Notes: i.Notes, + Resources: i.Resources, } if !i.FirstDeployed.IsZero() { From d52b489459d51d67d9db877f169bd09273b6cd6c Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:35:22 +0100 Subject: [PATCH 007/132] Added missing pkg/cmd/testdata/output files Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- .../output/history-with-rollback.json | 1 + .../testdata/output/history-with-rollback.txt | 4 ++++ .../output/history-with-rollback.yaml | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 pkg/cmd/testdata/output/history-with-rollback.json create mode 100644 pkg/cmd/testdata/output/history-with-rollback.txt create mode 100644 pkg/cmd/testdata/output/history-with-rollback.yaml diff --git a/pkg/cmd/testdata/output/history-with-rollback.json b/pkg/cmd/testdata/output/history-with-rollback.json new file mode 100644 index 000000000..b4175b062 --- /dev/null +++ b/pkg/cmd/testdata/output/history-with-rollback.json @@ -0,0 +1 @@ +[{"revision":1,"updated":"1977-09-02T22:04:05Z","status":"superseded","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Install complete"},{"revision":2,"updated":"1977-09-02T22:04:05Z","status":"superseded","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Upgrade complete"},{"revision":3,"updated":"1977-09-02T22:04:05Z","status":"deployed","chart":"foo-0.1.0-beta.1","app_version":"1.0","rollback_revision":1,"description":"Rollback to 1"}] diff --git a/pkg/cmd/testdata/output/history-with-rollback.txt b/pkg/cmd/testdata/output/history-with-rollback.txt new file mode 100644 index 000000000..c6dd62b8a --- /dev/null +++ b/pkg/cmd/testdata/output/history-with-rollback.txt @@ -0,0 +1,4 @@ +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete +3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1 diff --git a/pkg/cmd/testdata/output/history-with-rollback.yaml b/pkg/cmd/testdata/output/history-with-rollback.yaml new file mode 100644 index 000000000..19dc681c4 --- /dev/null +++ b/pkg/cmd/testdata/output/history-with-rollback.yaml @@ -0,0 +1,19 @@ +- app_version: "1.0" + chart: foo-0.1.0-beta.1 + description: Install complete + revision: 1 + status: superseded + updated: "1977-09-02T22:04:05Z" +- app_version: "1.0" + chart: foo-0.1.0-beta.1 + description: Upgrade complete + revision: 2 + status: superseded + updated: "1977-09-02T22:04:05Z" +- app_version: "1.0" + chart: foo-0.1.0-beta.1 + description: Rollback to 1 + revision: 3 + rollback_revision: 1 + status: deployed + updated: "1977-09-02T22:04:05Z" From 0737e438aae04fca872b66f185b0f2aab8bd0d86 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:49:05 +0100 Subject: [PATCH 008/132] Update pkg/cmd/history_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/history_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index 1b07b1679..e76a1f63d 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -27,7 +27,6 @@ import ( "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" - chart "helm.sh/helm/v4/pkg/chart/v2" ) From 681ccb19a68dab98b75e407ba38b37c45ee9c5d1 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:45:55 +0100 Subject: [PATCH 009/132] Update pkg/cmd/testdata/output/history-with-rollback.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/testdata/output/history-with-rollback.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/testdata/output/history-with-rollback.txt b/pkg/cmd/testdata/output/history-with-rollback.txt index c6dd62b8a..4fcb957dc 100644 --- a/pkg/cmd/testdata/output/history-with-rollback.txt +++ b/pkg/cmd/testdata/output/history-with-rollback.txt @@ -1,4 +1,4 @@ -REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION -1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete -2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete -3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1 +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION ROLLBACK +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete 0 +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete 0 +3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1 1 From e889cff0896d842c7e1854729611ac520a925d0b Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:45:04 +0100 Subject: [PATCH 010/132] feat(history): add --show-rollback flag for opt-in rollback column Replace the always-visible ROLLBACK column with an opt-in --show-rollback flag to avoid breaking the default table output (HIP-0004). JSON and YAML formats continue to include rollback_revision when present via omitempty. Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/history.go | 34 +++++++++++++++++++ pkg/cmd/history_test.go | 9 +++-- pkg/cmd/testdata/output/history-limit.txt | 6 ++-- .../output/history-with-rollback-no-flag.txt | 4 +++ .../testdata/output/history-with-rollback.txt | 8 ++--- pkg/cmd/testdata/output/history.txt | 10 +++--- 6 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 pkg/cmd/testdata/output/history-with-rollback-no-flag.txt diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index a040cd16a..eb1c88d66 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -43,6 +43,15 @@ configures the maximum length of the revision list returned. The historical release set is printed as a formatted table, e.g: $ helm history angry-bird + REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION + 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install + 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully + 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 + 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully + +Use '--show-rollback' to include a column showing the revision that was rolled back to: + + $ helm history angry-bird --show-rollback REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully @@ -53,6 +62,7 @@ The historical release set is printed as a formatted table, e.g: func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewHistory(cfg) var outfmt output.Format + var showRollback bool cmd := &cobra.Command{ Use: "history RELEASE_NAME", @@ -72,12 +82,16 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } + if showRollback { + return outfmt.Write(out, releaseHistoryWithRollback(history)) + } return outfmt.Write(out, history) }, } f := cmd.Flags() f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") + f.BoolVar(&showRollback, "show-rollback", false, "show the rollback revision column in the output") bindOutputFlag(cmd, &outfmt) return cmd @@ -176,6 +190,26 @@ func (r releaseHistory) WriteYAML(out io.Writer) error { } func (r releaseHistory) WriteTable(out io.Writer) error { + tbl := uitable.New() + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") + for _, item := range r { + tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) + } + return output.EncodeTable(out, tbl) +} + +// releaseHistoryWithRollback wraps releaseHistory to include the rollback column in table output. +type releaseHistoryWithRollback releaseHistory + +func (r releaseHistoryWithRollback) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, releaseHistory(r)) +} + +func (r releaseHistoryWithRollback) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, releaseHistory(r)) +} + +func (r releaseHistoryWithRollback) WriteTable(out io.Writer) error { tbl := uitable.New() tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "ROLLBACK", "DESCRIPTION") for _, item := range r { diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index e76a1f63d..b25eaba7f 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -25,9 +25,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" - chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestHistoryCmd(t *testing.T) { @@ -125,9 +125,14 @@ func TestHistoryWithRollback(t *testing.T) { } tests := []cmdTestCase{{ - name: "history with rollback revision", + name: "history with rollback revision (default, no rollback column)", cmd: "history angry-bird", rels: rels, + golden: "output/history-with-rollback-no-flag.txt", + }, { + name: "history with rollback revision and --show-rollback flag", + cmd: "history angry-bird --show-rollback", + rels: rels, golden: "output/history-with-rollback.txt", }, { name: "history with rollback revision json", diff --git a/pkg/cmd/testdata/output/history-limit.txt b/pkg/cmd/testdata/output/history-limit.txt index f6f059fb9..aee0fadb2 100644 --- a/pkg/cmd/testdata/output/history-limit.txt +++ b/pkg/cmd/testdata/output/history-limit.txt @@ -1,3 +1,3 @@ -REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION -3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock diff --git a/pkg/cmd/testdata/output/history-with-rollback-no-flag.txt b/pkg/cmd/testdata/output/history-with-rollback-no-flag.txt new file mode 100644 index 000000000..8861b7572 --- /dev/null +++ b/pkg/cmd/testdata/output/history-with-rollback-no-flag.txt @@ -0,0 +1,4 @@ +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete +3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1 diff --git a/pkg/cmd/testdata/output/history-with-rollback.txt b/pkg/cmd/testdata/output/history-with-rollback.txt index 4fcb957dc..26dd210af 100644 --- a/pkg/cmd/testdata/output/history-with-rollback.txt +++ b/pkg/cmd/testdata/output/history-with-rollback.txt @@ -1,4 +1,4 @@ -REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION ROLLBACK -1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete 0 -2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete 0 -3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1 1 +REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete +3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 1 Rollback to 1 diff --git a/pkg/cmd/testdata/output/history.txt b/pkg/cmd/testdata/output/history.txt index 4a51add7f..2a5d69c11 100644 --- a/pkg/cmd/testdata/output/history.txt +++ b/pkg/cmd/testdata/output/history.txt @@ -1,5 +1,5 @@ -REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION -1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock -4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock From 6927cde3f98d9e5f248418805e40c7408e629746 Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:43:27 +0100 Subject: [PATCH 011/132] fix: address Copilot review feedback on rollback revision PR - Clarify --show-rollback flag help text to specify it only affects table output - Add RollbackRevision JSON round-trip tests for pkg/release/v1 and internal/release/v2 - Add omitempty behavior verification for zero rollback_revision Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- internal/release/v2/info_test.go | 98 ++++++++++++++++++++++++++++++++ pkg/cmd/history.go | 2 +- pkg/release/v1/info_test.go | 98 ++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) diff --git a/internal/release/v2/info_test.go b/internal/release/v2/info_test.go index 560861e06..5812c6560 100644 --- a/internal/release/v2/info_test.go +++ b/internal/release/v2/info_test.go @@ -87,6 +87,27 @@ func TestInfoMarshalJSON(t *testing.T) { }, expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`, }, + { + name: "with rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + RollbackRevision: 2, + Description: "Rollback to 2", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`, + }, + { + name: "zero rollback revision omitted", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + Description: "Normal install", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`, + }, } for _, tt := range tests { @@ -203,6 +224,27 @@ func TestInfoUnmarshalJSON(t *testing.T) { Status: "", }, }, + { + name: "with rollback revision", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + RollbackRevision: 2, + Description: "Rollback to 2", + }, + }, + { + name: "zero rollback revision omitted", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + Description: "Normal install", + }, + }, } for _, tt := range tests { @@ -219,6 +261,7 @@ func TestInfoUnmarshalJSON(t *testing.T) { assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix()) assert.Equal(t, tt.expected.Description, info.Description) assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision) assert.Equal(t, tt.expected.Notes, info.Notes) assert.Equal(t, tt.expected.Resources, info.Resources) }) @@ -252,6 +295,61 @@ func TestInfoRoundTrip(t *testing.T) { assert.Equal(t, original.Notes, decoded.Notes) } +func TestInfoRollbackRevisionRoundTrip(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info Info + }{ + { + name: "with rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Rollback to 2", + Status: common.StatusDeployed, + RollbackRevision: 2, + }, + }, + { + name: "zero rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Normal install", + Status: common.StatusDeployed, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + + var decoded Info + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision) + assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix()) + assert.Equal(t, tt.info.LastDeployed.Unix(), decoded.LastDeployed.Unix()) + assert.Equal(t, tt.info.Status, decoded.Status) + assert.Equal(t, tt.info.Description, decoded.Description) + + // Verify omitempty behavior: zero rollback_revision should not appear in JSON + if tt.info.RollbackRevision == 0 { + var raw map[string]any + err = json.Unmarshal(data, &raw) + require.NoError(t, err) + assert.NotContains(t, raw, "rollback_revision") + } + }) + } +} + func TestInfoEmptyStringRoundTrip(t *testing.T) { // This test specifically verifies that empty string time fields // are handled correctly during parsing diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index eb1c88d66..49de85a06 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -91,7 +91,7 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f := cmd.Flags() f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") - f.BoolVar(&showRollback, "show-rollback", false, "show the rollback revision column in the output") + f.BoolVar(&showRollback, "show-rollback", false, "show the rollback revision column in table output") bindOutputFlag(cmd, &outfmt) return cmd diff --git a/pkg/release/v1/info_test.go b/pkg/release/v1/info_test.go index 6cff4db64..12b3014f6 100644 --- a/pkg/release/v1/info_test.go +++ b/pkg/release/v1/info_test.go @@ -87,6 +87,27 @@ func TestInfoMarshalJSON(t *testing.T) { }, expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`, }, + { + name: "with rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + RollbackRevision: 2, + Description: "Rollback to 2", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`, + }, + { + name: "zero rollback revision omitted", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + Description: "Normal install", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`, + }, } for _, tt := range tests { @@ -203,6 +224,27 @@ func TestInfoUnmarshalJSON(t *testing.T) { Status: "", }, }, + { + name: "with rollback revision", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + RollbackRevision: 2, + Description: "Rollback to 2", + }, + }, + { + name: "zero rollback revision omitted", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + Description: "Normal install", + }, + }, } for _, tt := range tests { @@ -219,6 +261,7 @@ func TestInfoUnmarshalJSON(t *testing.T) { assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix()) assert.Equal(t, tt.expected.Description, info.Description) assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision) assert.Equal(t, tt.expected.Notes, info.Notes) assert.Equal(t, tt.expected.Resources, info.Resources) }) @@ -252,6 +295,61 @@ func TestInfoRoundTrip(t *testing.T) { assert.Equal(t, original.Notes, decoded.Notes) } +func TestInfoRollbackRevisionRoundTrip(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info Info + }{ + { + name: "with rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Rollback to 2", + Status: common.StatusDeployed, + RollbackRevision: 2, + }, + }, + { + name: "zero rollback revision", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Normal install", + Status: common.StatusDeployed, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + + var decoded Info + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision) + assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix()) + assert.Equal(t, tt.info.LastDeployed.Unix(), decoded.LastDeployed.Unix()) + assert.Equal(t, tt.info.Status, decoded.Status) + assert.Equal(t, tt.info.Description, decoded.Description) + + // Verify omitempty behavior: zero rollback_revision should not appear in JSON + if tt.info.RollbackRevision == 0 { + var raw map[string]any + err = json.Unmarshal(data, &raw) + require.NoError(t, err) + assert.NotContains(t, raw, "rollback_revision") + } + }) + } +} + func TestInfoEmptyStringRoundTrip(t *testing.T) { // This test specifically verifies that empty string time fields // are handled correctly during parsing From 1e0f702f00d5fd36c9de9448e3271787bc588273 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Fri, 27 Feb 2026 19:10:53 +0100 Subject: [PATCH 012/132] chore: fix whitespace linter Signed-off-by: Matthieu MOREL --- .golangci.yml | 1 + internal/chart/v3/lint/lint.go | 1 - internal/chart/v3/lint/rules/template_test.go | 3 --- internal/chart/v3/loader/load_test.go | 2 -- internal/chart/v3/util/create.go | 1 - internal/chart/v3/util/dependencies_test.go | 1 - internal/chart/v3/util/validate_name.go | 1 - internal/cli/output/color_test.go | 3 --- internal/monocular/client.go | 2 -- internal/monocular/search.go | 1 - internal/monocular/search_test.go | 1 - internal/plugin/installer/base_test.go | 1 - internal/plugin/installer/extractor.go | 1 - internal/plugin/installer/http_installer_test.go | 5 ----- internal/plugin/installer/installer.go | 1 - internal/plugin/installer/vcs_installer_test.go | 1 - internal/plugin/loader.go | 5 ----- internal/plugin/loader_test.go | 1 - internal/plugin/metadata_test.go | 1 - internal/plugin/plugin_type_registry_test.go | 1 - internal/plugin/runtime_extismv1.go | 2 -- internal/plugin/subprocess_commands_test.go | 1 - internal/release/v2/util/manifest_sorter_test.go | 2 -- internal/resolver/resolver.go | 2 -- internal/third_party/dep/fs/fs_test.go | 3 --- internal/version/version.go | 1 - pkg/action/action.go | 1 - pkg/action/dependency.go | 1 - pkg/action/get_metadata.go | 1 - pkg/action/hooks.go | 4 ---- pkg/action/install_test.go | 3 --- pkg/action/package_test.go | 1 - pkg/action/resource_policy.go | 1 - pkg/action/upgrade.go | 1 - pkg/action/upgrade_test.go | 2 -- pkg/action/validate_test.go | 1 - pkg/chart/common/capabilities.go | 1 - pkg/chart/common/util/coalesce_test.go | 2 -- pkg/chart/common/util/values_test.go | 1 - pkg/chart/loader/load.go | 1 - pkg/chart/v2/lint/lint.go | 1 - pkg/chart/v2/lint/rules/template.go | 1 - pkg/chart/v2/lint/rules/template_test.go | 3 --- pkg/chart/v2/loader/load_test.go | 2 -- pkg/chart/v2/util/create.go | 1 - pkg/chart/v2/util/dependencies_test.go | 1 - pkg/chart/v2/util/validate_name.go | 1 - pkg/cmd/completion_test.go | 1 - pkg/cmd/flags.go | 1 - pkg/cmd/get_metadata.go | 1 - pkg/cmd/helpers.go | 1 - pkg/cmd/helpers_test.go | 2 -- pkg/cmd/history.go | 1 - pkg/cmd/lint_test.go | 1 - pkg/cmd/load_plugins.go | 1 - pkg/cmd/pull_test.go | 1 - pkg/cmd/repo_index_test.go | 1 - pkg/cmd/repo_remove_test.go | 1 - pkg/cmd/root.go | 2 -- pkg/cmd/search.go | 1 - pkg/cmd/search/search.go | 1 - pkg/cmd/search/search_test.go | 3 --- pkg/cmd/search_hub.go | 1 - pkg/cmd/search_hub_test.go | 2 -- pkg/cmd/template.go | 1 - pkg/cmd/uninstall.go | 1 - pkg/cmd/upgrade_test.go | 10 ---------- pkg/cmd/verify_test.go | 1 - pkg/downloader/chart_downloader.go | 2 -- pkg/downloader/manager.go | 3 --- pkg/engine/engine_test.go | 5 ----- pkg/getter/httpgetter_test.go | 1 - pkg/getter/plugingetter.go | 1 - pkg/helmpath/lazypath.go | 1 - pkg/kube/client.go | 8 -------- pkg/kube/client_test.go | 10 ---------- pkg/registry/chart.go | 6 ------ pkg/registry/chart_test.go | 9 --------- pkg/registry/client.go | 2 -- pkg/registry/registry_test.go | 1 - pkg/release/v1/util/manifest_sorter_test.go | 2 -- pkg/repo/v1/chartrepo.go | 1 - pkg/repo/v1/chartrepo_test.go | 2 -- pkg/repo/v1/index_test.go | 3 --- pkg/repo/v1/repotest/server_test.go | 2 -- pkg/storage/driver/memory_test.go | 1 - pkg/storage/driver/sql.go | 1 - pkg/strvals/parser.go | 1 - 88 files changed, 1 insertion(+), 172 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 536b4b212..fa021ade9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,6 +36,7 @@ linters: - unused - usestdlibvars - usetesting + - whitespace exclusions: diff --git a/internal/chart/v3/lint/lint.go b/internal/chart/v3/lint/lint.go index e98edfabe..ea8ca3d60 100644 --- a/internal/chart/v3/lint/lint.go +++ b/internal/chart/v3/lint/lint.go @@ -44,7 +44,6 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { } func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter { - chartDir, _ := filepath.Abs(baseDir) lo := linterOptions{} diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go index b1371659f..88343b330 100644 --- a/internal/chart/v3/lint/rules/template_test.go +++ b/internal/chart/v3/lint/rules/template_test.go @@ -242,7 +242,6 @@ data: // // See https://github.com/helm/helm/issues/7483 func TestStrictTemplateParsingMapError(t *testing.T) { - ch := chart.Chart{ Metadata: &chart.Metadata{ Name: "regression7483", @@ -371,7 +370,6 @@ func TestValidateTopIndentLevel(t *testing.T) { t.Errorf("Expected %t for %q", shouldFail, doc) } } - } // TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments @@ -463,5 +461,4 @@ func TestIsYamlFileExtension(t *testing.T) { t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) } } - } diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index c32f79aff..0ede21cfb 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -337,7 +337,6 @@ icon: https://example.com/64x64.png if text.String() != "" { t.Errorf("Expected no message to Stderr, got %s", text.String()) } - } // Packaging the chart on a Windows machine will produce an @@ -607,7 +606,6 @@ func verifyChart(t *testing.T, c *chart.Chart) { t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) } } - } func verifyDependencies(t *testing.T, c *chart.Chart) { diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index 48d2120e5..b3d75ac2d 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -702,7 +702,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { // error. In such a case, this will attempt to clean up by removing the // new chart directory. func Create(name, dir string) (string, error) { - // Sanity-check the name of a chart so user doesn't create one that causes problems. if err := validateChartName(name); err != nil { return "", err diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go index c8a176725..0a0937e4a 100644 --- a/internal/chart/v3/util/dependencies_test.go +++ b/internal/chart/v3/util/dependencies_test.go @@ -459,7 +459,6 @@ func TestDependentChartAliases(t *testing.T) { if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { t.Fatalf("expected no chart but got %s", aliasChart.Name()) } - } func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { diff --git a/internal/chart/v3/util/validate_name.go b/internal/chart/v3/util/validate_name.go index 6595e085d..7d85a5b88 100644 --- a/internal/chart/v3/util/validate_name.go +++ b/internal/chart/v3/util/validate_name.go @@ -79,7 +79,6 @@ func ValidateReleaseName(name string) error { // This case is preserved for backwards compatibility if name == "" { return errMissingName - } if len(name) > maxReleaseNameLen || !validName.MatchString(name) { return errInvalidName diff --git a/internal/cli/output/color_test.go b/internal/cli/output/color_test.go index 3b8de39e8..f0484fc3a 100644 --- a/internal/cli/output/color_test.go +++ b/internal/cli/output/color_test.go @@ -24,7 +24,6 @@ import ( ) func TestColorizeStatus(t *testing.T) { - tests := []struct { name string status common.Status @@ -107,7 +106,6 @@ func TestColorizeStatus(t *testing.T) { } func TestColorizeHeader(t *testing.T) { - tests := []struct { name string header string @@ -149,7 +147,6 @@ func TestColorizeHeader(t *testing.T) { } func TestColorizeNamespace(t *testing.T) { - tests := []struct { name string namespace string diff --git a/internal/monocular/client.go b/internal/monocular/client.go index f4ef5d647..cd1a0d666 100644 --- a/internal/monocular/client.go +++ b/internal/monocular/client.go @@ -33,7 +33,6 @@ type Client struct { // New creates a new client func New(u string) (*Client, error) { - // Validate we have a URL if err := validate(u); err != nil { return nil, err @@ -46,7 +45,6 @@ func New(u string) (*Client, error) { // Validate if the base URL for monocular is valid. func validate(u string) error { - // Check if it is parsable p, err := url.Parse(u) if err != nil { diff --git a/internal/monocular/search.go b/internal/monocular/search.go index fcf04b7a4..cfae87ded 100644 --- a/internal/monocular/search.go +++ b/internal/monocular/search.go @@ -99,7 +99,6 @@ type ChartVersion struct { // Search performs a search against the monocular search API func (c *Client) Search(term string) ([]SearchResult, error) { - // Create the URL to the search endpoint // Note, this is currently an internal API for the Hub. This should be // formatted without showing how monocular operates. diff --git a/internal/monocular/search_test.go b/internal/monocular/search_test.go index fc82ef4b4..cdf67f69e 100644 --- a/internal/monocular/search_test.go +++ b/internal/monocular/search_test.go @@ -27,7 +27,6 @@ import ( var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` func TestSearch(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, searchResult) })) diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index 62b77bde5..6df8ec8a1 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -35,7 +35,6 @@ func TestPath(t *testing.T) { } for _, tt := range tests { - t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) baseIns := newBase(tt.source) baseInsPath := baseIns.Path() diff --git a/internal/plugin/installer/extractor.go b/internal/plugin/installer/extractor.go index b753dfbca..3af56ab56 100644 --- a/internal/plugin/installer/extractor.go +++ b/internal/plugin/installer/extractor.go @@ -90,7 +90,6 @@ func NewExtractor(source string) (Extractor, error) { // - Beginning a path with a path separator is illegal // - Rudimentary symlink protections are offered by SecureJoin. func cleanJoin(root, dest string) (string, error) { - // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. // In neither case do we want to trust a TAR that contains these. if strings.Contains(dest, ":") { diff --git a/internal/plugin/installer/http_installer_test.go b/internal/plugin/installer/http_installer_test.go index 85a84ee31..9ae8fe7b5 100644 --- a/internal/plugin/installer/http_installer_test.go +++ b/internal/plugin/installer/http_installer_test.go @@ -124,7 +124,6 @@ func TestHTTPInstaller(t *testing.T) { } else if err.Error() != "plugin already exists" { t.Fatalf("expected error for plugin exists, got (%v)", err) } - } func TestHTTPInstallerNonExistentVersion(t *testing.T) { @@ -157,7 +156,6 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) { if err := Install(i); err == nil { t.Fatal("expected error from http client") } - } func TestHTTPInstallerUpdate(t *testing.T) { @@ -297,7 +295,6 @@ func TestExtract(t *testing.T) { t.Fatalf("Expected %s to have %o mode but has %o (umask: %o)", readmeFullPath, expectedReadmePerm, info.Mode().Perm(), currentUmask) } - } func TestCleanJoin(t *testing.T) { @@ -327,11 +324,9 @@ func TestCleanJoin(t *testing.T) { t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out) } } - } func TestMediaTypeToExtension(t *testing.T) { - for mt, shouldPass := range map[string]bool{ "": false, "application/gzip": true, diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index f0870dcc5..54effb1cb 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -72,7 +72,6 @@ type VerificationResult struct { // InstallWithOptions installs a plugin with options. func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) { - if err := os.MkdirAll(filepath.Dir(i.Path()), 0755); err != nil { return nil, err } diff --git a/internal/plugin/installer/vcs_installer_test.go b/internal/plugin/installer/vcs_installer_test.go index 7fe627b59..54f94c724 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -185,5 +185,4 @@ func TestVCSInstallerUpdate(t *testing.T) { } else if err.Error() != "plugin repo was modified" { t.Fatalf("expected error for plugin modified, got (%v)", err) } - } diff --git a/internal/plugin/loader.go b/internal/plugin/loader.go index 2f051b99e..bd7edbc2c 100644 --- a/internal/plugin/loader.go +++ b/internal/plugin/loader.go @@ -44,7 +44,6 @@ func peekAPIVersion(r io.Reader) (string, error) { } func loadMetadataLegacy(metadataData []byte) (*Metadata, error) { - var ml MetadataLegacy d := yaml.NewDecoder(bytes.NewReader(metadataData)) // NOTE: No strict unmarshalling for legacy plugins - maintain backwards compatibility @@ -64,7 +63,6 @@ func loadMetadataLegacy(metadataData []byte) (*Metadata, error) { } func loadMetadataV1(metadataData []byte) (*Metadata, error) { - var mv1 MetadataV1 d := yaml.NewDecoder(bytes.NewReader(metadataData)) d.KnownFields(true) @@ -108,7 +106,6 @@ type prototypePluginManager struct { } func newPrototypePluginManager() (*prototypePluginManager, error) { - cc, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build")) if err != nil { return nil, fmt.Errorf("failed to create wazero compilation cache: %w", err) @@ -212,7 +209,6 @@ func findPlugins(pluginsDirs []string, findFn findFunc, filterFn filterFunc) ([] found = append(found, p) } } - } return found, nil @@ -225,7 +221,6 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc { // If name is specified, it must match if descriptor.Name != "" && p.Metadata().Name != descriptor.Name { return false - } // If type is specified, it must match if descriptor.Type != "" && p.Metadata().Type != descriptor.Type { diff --git a/internal/plugin/loader_test.go b/internal/plugin/loader_test.go index 03ef02c85..c8531a490 100644 --- a/internal/plugin/loader_test.go +++ b/internal/plugin/loader_test.go @@ -62,7 +62,6 @@ name: "test-plugin" } func TestLoadDir(t *testing.T) { - makeMetadata := func(apiVersion string) Metadata { usage := "hello [params]..." if apiVersion == "legacy" { diff --git a/internal/plugin/metadata_test.go b/internal/plugin/metadata_test.go index 145ef5101..f59b6a78d 100644 --- a/internal/plugin/metadata_test.go +++ b/internal/plugin/metadata_test.go @@ -21,7 +21,6 @@ import ( ) func TestValidatePluginData(t *testing.T) { - // A mock plugin with no commands mockNoCommand := mockSubprocessCLIPlugin(t, "foo") mockNoCommand.metadata.RuntimeConfig = &RuntimeConfigSubprocess{ diff --git a/internal/plugin/plugin_type_registry_test.go b/internal/plugin/plugin_type_registry_test.go index 22f26262d..157c2f1d7 100644 --- a/internal/plugin/plugin_type_registry_test.go +++ b/internal/plugin/plugin_type_registry_test.go @@ -28,7 +28,6 @@ func TestMakeOutputMessage(t *testing.T) { ptm := pluginTypesIndex["getter/v1"] outputType := reflect.Zero(ptm.outputType).Interface() assert.IsType(t, schema.OutputMessageGetterV1{}, outputType) - } func TestMakeConfig(t *testing.T) { diff --git a/internal/plugin/runtime_extismv1.go b/internal/plugin/runtime_extismv1.go index cd9a02535..ffa108a08 100644 --- a/internal/plugin/runtime_extismv1.go +++ b/internal/plugin/runtime_extismv1.go @@ -99,7 +99,6 @@ type RuntimeExtismV1 struct { var _ Runtime = (*RuntimeExtismV1)(nil) func (r *RuntimeExtismV1) CreatePlugin(pluginDir string, metadata *Metadata) (Plugin, error) { - rc, ok := metadata.RuntimeConfig.(*RuntimeConfigExtismV1) if !ok { return nil, fmt.Errorf("invalid extism/v1 plugin runtime config type: %T", metadata.RuntimeConfig) @@ -139,7 +138,6 @@ func (p *ExtismV1PluginRuntime) Dir() string { } func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) { - var tmpDir string if p.rc.FileSystem.CreateTempDir { tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*") diff --git a/internal/plugin/subprocess_commands_test.go b/internal/plugin/subprocess_commands_test.go index df854b4ca..dbce42f6a 100644 --- a/internal/plugin/subprocess_commands_test.go +++ b/internal/plugin/subprocess_commands_test.go @@ -48,7 +48,6 @@ func TestPrepareCommand(t *testing.T) { } func TestPrepareCommandExtraArgs(t *testing.T) { - cmdMain := "sh" cmdArgs := []string{"-c", "echo \"test\""} platformCommand := []PlatformCommand{ diff --git a/internal/release/v2/util/manifest_sorter_test.go b/internal/release/v2/util/manifest_sorter_test.go index 28f0b34cc..c8851d678 100644 --- a/internal/release/v2/util/manifest_sorter_test.go +++ b/internal/release/v2/util/manifest_sorter_test.go @@ -26,7 +26,6 @@ import ( ) func TestSortManifests(t *testing.T) { - data := []struct { name []string path string @@ -183,7 +182,6 @@ metadata: if !reflect.DeepEqual(expectedHooks, out.Events) { t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) } - } } if !found { diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 184c8404b..5f0c5b148 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -54,7 +54,6 @@ func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver // Resolve resolves dependencies and returns a lock file with the resolution. func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { - // Now we clone the dependencies, locking as we go. locked := make([]*chart.Dependency, len(reqs)) missing := []string{} @@ -146,7 +145,6 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string Version: version, }, }} - } else { // Retrieve list of tags for repository ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, registry.OCIScheme+"://"), d.Name) diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go index 8f28c3af7..806e33c28 100644 --- a/internal/third_party/dep/fs/fs_test.go +++ b/internal/third_party/dep/fs/fs_test.go @@ -238,7 +238,6 @@ func TestCopyDirFail_SrcIsNotDir(t *testing.T) { if !errors.Is(err, errSrcNotDir) { t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err) } - } func TestCopyDirFail_DstExists(t *testing.T) { @@ -491,7 +490,6 @@ func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() { } func TestIsDir(t *testing.T) { - var currentUID = os.Getuid() if currentUID == 0 { @@ -544,7 +542,6 @@ func TestIsDir(t *testing.T) { } func TestIsSymlink(t *testing.T) { - var currentUID = os.Getuid() if currentUID == 0 { diff --git a/internal/version/version.go b/internal/version/version.go index 3daf80893..f37a97425 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -77,7 +77,6 @@ func GetUserAgent() string { // Get returns build info func Get() BuildInfo { - makeKubeClientVersionString := func() string { // Test builds don't include debug info / module info // (And even if they did, we probably want a stable version during tests anyway) diff --git a/pkg/action/action.go b/pkg/action/action.go index 75b6cf8a0..550d113da 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -521,7 +521,6 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet var ok bool for _, r := range resources { for _, rl := range r.APIResources { - // A Kind at a GroupVersion can show up more than once. We only want // it displayed once in the final output. id = path.Join(r.GroupVersion, rl.Kind) diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 5c87f7cba..8ceca1433 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -120,7 +120,6 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p if r := statArchiveForStatus(archive, dep); r != "" { return r } - } // End unnecessary code. diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go index 5312dac7f..7d5a3f5f9 100644 --- a/pkg/action/get_metadata.go +++ b/pkg/action/get_metadata.go @@ -119,7 +119,6 @@ func (m *Metadata) FormattedDepNames() string { continue } depsNames = append(depsNames, ac.Name()) - } sort.StringSlice(depsNames).Sort() diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index a4a8da7a6..9bd5ed788 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -35,7 +35,6 @@ import ( func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration, serverSideApply bool) error { - shutdown, err := cfg.execHookWithDelayedShutdown(rl, hook, waitStrategy, waitOptions, timeout, serverSideApply) if shutdown == nil { return err @@ -59,7 +58,6 @@ func shutdownNoOp() error { func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook release.HookEvent, waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration, serverSideApply bool) (ExecuteShutdownFunc, error) { - executingHooks := []*release.Hook{} for _, h := range rl.Hooks { @@ -179,7 +177,6 @@ func (x hookByWeight) Less(i, j int) bool { // deleteHookByPolicy deletes a hook if the hook policy instructs it to func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration) error { - // Never delete CustomResourceDefinitions; this could cause lots of // cascading garbage collection. if h.Kind == "CustomResourceDefinition" { @@ -214,7 +211,6 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo // deleteHooksByPolicy deletes all hooks if the hook policy instructs it to func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy, waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration) error { - for _, h := range hooks { if err := cfg.deleteHookByPolicy(h, policy, waitStrategy, waitOptions, timeout); err != nil { return err diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index e4d2b7376..5e5c5b227 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -737,7 +737,6 @@ func TestInstallRelease_RollbackOnFailure(t *testing.T) { }) } func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { - is := assert.New(t) instAction := installAction(t) instAction.ReleaseName = "interrupted-release" @@ -767,7 +766,6 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background time.Sleep(10 * time.Second) // wait for goroutine to finish is.Equal(goroutines, instAction.getGoroutineCount()) - } func TestNameTemplate(t *testing.T) { testCases := []nameTemplateTestCase{ @@ -804,7 +802,6 @@ func TestNameTemplate(t *testing.T) { } for _, tc := range testCases { - n, err := TemplateName(tc.tpl) if err != nil { if tc.expectedErrorStr == "" { diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go index 2e1d4ff07..f8a55c0e0 100644 --- a/pkg/action/package_test.go +++ b/pkg/action/package_test.go @@ -150,7 +150,6 @@ func TestValidateVersion(t *testing.T) { if !errors.Is(err, tt.wantErr) { t.Errorf("Expected {%v}, got {%v}", tt.wantErr, err) } - } }) } diff --git a/pkg/action/resource_policy.go b/pkg/action/resource_policy.go index fcea98ad6..4f4e2ff55 100644 --- a/pkg/action/resource_policy.go +++ b/pkg/action/resource_policy.go @@ -40,7 +40,6 @@ func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining [] if resourcePolicyType == kube.KeepPolicy { keep = append(keep, m) } - } return keep, remaining } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 0f360fe37..05ff171db 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -263,7 +263,6 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } } - } // determine if values will be reused diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 393692976..48721c1ae 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -446,7 +446,6 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { } func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) { - is := assert.New(t) req := require.New(t) @@ -741,7 +740,6 @@ func TestGetUpgradeServerSideValue(t *testing.T) { assert.ErrorContains(t, err, tt.expectedErrorMsg) }) } - } func TestUpgradeRun_UnreachableKubeClient(t *testing.T) { diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go index fd59abcb1..2223f57d5 100644 --- a/pkg/action/validate_test.go +++ b/pkg/action/validate_test.go @@ -273,7 +273,6 @@ func TestValidateNameAndGenerateName(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - skip, err := validateNameAndGenerateName(tc.info) if tc.wantErr { diff --git a/pkg/chart/common/capabilities.go b/pkg/chart/common/capabilities.go index 20f4953cf..16910acaa 100644 --- a/pkg/chart/common/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -167,7 +167,6 @@ func makeDefaultCapabilities() (*Capabilities, error) { } func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities, error) { - version := fmt.Sprintf("v%d.%d.0", kubeVersionMajor, kubeVersionMinor) return &Capabilities{ KubeVersion: KubeVersion{ diff --git a/pkg/chart/common/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go index 1d0baa84d..8ccdcebcf 100644 --- a/pkg/chart/common/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -666,7 +666,6 @@ func TestMergeTables(t *testing.T) { } func TestCoalesceValuesWarnings(t *testing.T) { - c := withDeps(&chart.Chart{ Metadata: &chart.Metadata{Name: "level1"}, Values: map[string]any{ @@ -724,7 +723,6 @@ func TestCoalesceValuesWarnings(t *testing.T) { assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.") assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)") assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])") - } func TestConcatPrefix(t *testing.T) { diff --git a/pkg/chart/common/util/values_test.go b/pkg/chart/common/util/values_test.go index 1dea04132..899ce3571 100644 --- a/pkg/chart/common/util/values_test.go +++ b/pkg/chart/common/util/values_test.go @@ -25,7 +25,6 @@ import ( ) func TestToRenderValues(t *testing.T) { - chartValues := map[string]any{ "name": "al Rashid", "where": map[string]any{ diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go index 3fd381825..6fe246474 100644 --- a/pkg/chart/loader/load.go +++ b/pkg/chart/loader/load.go @@ -100,7 +100,6 @@ func LoadDir(dir string) (chart.Charter, error) { default: return nil, errors.New("unsupported chart version") } - } // FileLoader loads a chart from a file diff --git a/pkg/chart/v2/lint/lint.go b/pkg/chart/v2/lint/lint.go index 7f6f26320..23a93c551 100644 --- a/pkg/chart/v2/lint/lint.go +++ b/pkg/chart/v2/lint/lint.go @@ -44,7 +44,6 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { } func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter { - chartDir, _ := filepath.Abs(baseDir) lo := linterOptions{} diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 43665aa3a..ac5f740a0 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -62,7 +62,6 @@ func TemplateLinterSkipSchemaValidation(skipSchemaValidation bool) TemplateLinte } func newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter { - result := templateLinter{ linter: linter, values: values, diff --git a/pkg/chart/v2/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go index b6a62e8fd..f0eb008b3 100644 --- a/pkg/chart/v2/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -257,7 +257,6 @@ data: // // See https://github.com/helm/helm/issues/7483 func TestStrictTemplateParsingMapError(t *testing.T) { - ch := chart.Chart{ Metadata: &chart.Metadata{ Name: "regression7483", @@ -390,7 +389,6 @@ func TestValidateTopIndentLevel(t *testing.T) { t.Errorf("Expected %t for %q", shouldFail, doc) } } - } // TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments @@ -486,5 +484,4 @@ func TestIsYamlFileExtension(t *testing.T) { t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) } } - } diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index dad988605..e14b68dc7 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -390,7 +390,6 @@ icon: https://example.com/64x64.png if text.String() != "" { t.Errorf("Expected no message to Stderr, got %s", text.String()) } - } // Packaging the chart on a Windows machine will produce an @@ -660,7 +659,6 @@ func verifyChart(t *testing.T, c *chart.Chart) { t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) } } - } func verifyDependencies(t *testing.T, c *chart.Chart) { diff --git a/pkg/chart/v2/util/create.go b/pkg/chart/v2/util/create.go index 0d7ae8d5c..36e3bfe72 100644 --- a/pkg/chart/v2/util/create.go +++ b/pkg/chart/v2/util/create.go @@ -701,7 +701,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { // error. In such a case, this will attempt to clean up by removing the // new chart directory. func Create(name, dir string) (string, error) { - // Sanity-check the name of a chart so user doesn't create one that causes problems. if err := validateChartName(name); err != nil { return "", err diff --git a/pkg/chart/v2/util/dependencies_test.go b/pkg/chart/v2/util/dependencies_test.go index 0e4df8528..90a8806ec 100644 --- a/pkg/chart/v2/util/dependencies_test.go +++ b/pkg/chart/v2/util/dependencies_test.go @@ -459,7 +459,6 @@ func TestDependentChartAliases(t *testing.T) { if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { t.Fatalf("expected no chart but got %s", aliasChart.Name()) } - } func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { diff --git a/pkg/chart/v2/util/validate_name.go b/pkg/chart/v2/util/validate_name.go index 6595e085d..7d85a5b88 100644 --- a/pkg/chart/v2/util/validate_name.go +++ b/pkg/chart/v2/util/validate_name.go @@ -79,7 +79,6 @@ func ValidateReleaseName(name string) error { // This case is preserved for backwards compatibility if name == "" { return errMissingName - } if len(name) > maxReleaseNameLen || !validName.MatchString(name) { return errInvalidName diff --git a/pkg/cmd/completion_test.go b/pkg/cmd/completion_test.go index 81c1ee2ad..399ff1f0c 100644 --- a/pkg/cmd/completion_test.go +++ b/pkg/cmd/completion_test.go @@ -51,7 +51,6 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { if shouldBePerformed { t.Errorf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName) } else { - t.Errorf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName) } t.Log(out) diff --git a/pkg/cmd/flags.go b/pkg/cmd/flags.go index 5a220d1ce..e7db3dde2 100644 --- a/pkg/cmd/flags.go +++ b/pkg/cmd/flags.go @@ -220,7 +220,6 @@ func (p *postRendererArgsSlice) Type() string { } func (p *postRendererArgsSlice) Set(val string) error { - // a post-renderer defined by a user may accept empty arguments p.options.args = append(p.options.args, val) diff --git a/pkg/cmd/get_metadata.go b/pkg/cmd/get_metadata.go index eb90b6e44..c26af646f 100644 --- a/pkg/cmd/get_metadata.go +++ b/pkg/cmd/get_metadata.go @@ -77,7 +77,6 @@ func newGetMetadataCmd(cfg *action.Configuration, out io.Writer) *cobra.Command } func (w metadataWriter) WriteTable(out io.Writer) error { - formatApplyMethod := func(applyMethod string) string { switch applyMethod { case "": diff --git a/pkg/cmd/helpers.go b/pkg/cmd/helpers.go index e555dd18b..8866b8650 100644 --- a/pkg/cmd/helpers.go +++ b/pkg/cmd/helpers.go @@ -43,7 +43,6 @@ func addDryRunFlag(cmd *cobra.Command) { // Determine the `action.DryRunStrategy` given -dry-run=` flag (or absence of) // Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { - f := cmd.Flag("dry-run") v := f.Value.String() diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 08065499e..8710085e6 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -157,7 +157,6 @@ func resetEnv() func() { } func TestCmdGetDryRunFlagStrategy(t *testing.T) { - type testCaseExpectedLog struct { Level string Msg string @@ -274,7 +273,6 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) { } for name, tc := range testCases { - logBuf := new(bytes.Buffer) logger := slog.New(slog.NewJSONHandler(logBuf, nil)) slog.SetDefault(logger) diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index 3349b7bc1..77b5bd9b7 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -224,7 +224,6 @@ func getReleaseHistory(rls []*release.Release) (history releaseHistory) { } if !r.Info.LastDeployed.IsZero() { rInfo.Updated = r.Info.LastDeployed - } history = append(history, rInfo) } diff --git a/pkg/cmd/lint_test.go b/pkg/cmd/lint_test.go index a13ec423b..ec6d18de8 100644 --- a/pkg/cmd/lint_test.go +++ b/pkg/cmd/lint_test.go @@ -60,7 +60,6 @@ func TestLintCmdWithQuietFlag(t *testing.T) { wantError: true, }} runTestCmd(t, tests) - } func TestLintCmdWithKubeVersionFlag(t *testing.T) { diff --git a/pkg/cmd/load_plugins.go b/pkg/cmd/load_plugins.go index 029dd04f5..b6cc38ce6 100644 --- a/pkg/cmd/load_plugins.go +++ b/pkg/cmd/load_plugins.go @@ -333,7 +333,6 @@ func loadFile(path string) (*pluginCommand, error) { // to obtain the dynamic completion choices. It must pass all the flags and sub-commands // specified in the command-line to the plugin.complete executable (except helm's global flags) func pluginDynamicComp(plug plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - subprocessPlug, ok := plug.(*plugin.SubprocessPluginRuntime) if !ok { // Completion only supported for subprocess plugins (TODO: fix this) diff --git a/pkg/cmd/pull_test.go b/pkg/cmd/pull_test.go index f749c218c..511061dcd 100644 --- a/pkg/cmd/pull_test.go +++ b/pkg/cmd/pull_test.go @@ -260,7 +260,6 @@ func TestPullCmd(t *testing.T) { if out != outString { t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out) } - } ef := filepath.Join(outdir, tt.expectFile) diff --git a/pkg/cmd/repo_index_test.go b/pkg/cmd/repo_index_test.go index c8959f21e..68f105d6d 100644 --- a/pkg/cmd/repo_index_test.go +++ b/pkg/cmd/repo_index_test.go @@ -28,7 +28,6 @@ import ( ) func TestRepoIndexCmd(t *testing.T) { - dir := t.TempDir() comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") diff --git a/pkg/cmd/repo_remove_test.go b/pkg/cmd/repo_remove_test.go index f2641ccf0..79778e75e 100644 --- a/pkg/cmd/repo_remove_test.go +++ b/pkg/cmd/repo_remove_test.go @@ -103,7 +103,6 @@ func TestRepoRemove(t *testing.T) { cacheIndex, cacheChart := createCacheFiles(rootDir, repoName) cacheFiles[repoName] = []string{cacheIndex, cacheChart} - } // Create repo remove command diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 04ba91c1f..1fa01ca0e 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -355,7 +355,6 @@ func hookOutputWriter(_, _, _ string) io.Writer { } func checkForExpiredRepos(repofile string) { - expiredRepos := []struct { name string old string @@ -399,7 +398,6 @@ func checkForExpiredRepos(repofile string) { ) } } - } func newRegistryClient( diff --git a/pkg/cmd/search.go b/pkg/cmd/search.go index 4d110286d..eb6c6e0b5 100644 --- a/pkg/cmd/search.go +++ b/pkg/cmd/search.go @@ -29,7 +29,6 @@ Use search subcommands to search different locations for charts. ` func newSearchCmd(out io.Writer) *cobra.Command { - cmd := &cobra.Command{ Use: "search [keyword]", Short: "search for a keyword in charts", diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go index 1c7bb1d06..cb630709f 100644 --- a/pkg/cmd/search/search.go +++ b/pkg/cmd/search/search.go @@ -123,7 +123,6 @@ func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, erro // calcScore calculates a score for a match. func (i *Index) calcScore(index int, matchline string) int { - // This is currently tied to the fact that sep is a single char. splits := []int{} s := rune(sep[0]) diff --git a/pkg/cmd/search/search_test.go b/pkg/cmd/search/search_test.go index b3220394f..46394e77f 100644 --- a/pkg/cmd/search/search_test.go +++ b/pkg/cmd/search/search_test.go @@ -149,7 +149,6 @@ func TestAddRepo_Sort(t *testing.T) { } func TestSearchByName(t *testing.T) { - tests := []struct { name string query string @@ -245,7 +244,6 @@ func TestSearchByName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - charts, err := i.Search(tt.query, 100, tt.regexp) if err != nil { if tt.fail { @@ -274,7 +272,6 @@ func TestSearchByName(t *testing.T) { t.Errorf("[%d]: Expected name %q, got %q", i, ex.Name, got.Name) } } - }) } } diff --git a/pkg/cmd/search_hub.go b/pkg/cmd/search_hub.go index f9adb73f4..3081961be 100644 --- a/pkg/cmd/search_hub.go +++ b/pkg/cmd/search_hub.go @@ -196,5 +196,4 @@ func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) er // WriteJSON and WriteYAML, we shouldn't get invalid types return nil } - } diff --git a/pkg/cmd/search_hub_test.go b/pkg/cmd/search_hub_test.go index 8e056f771..4826181d2 100644 --- a/pkg/cmd/search_hub_test.go +++ b/pkg/cmd/search_hub_test.go @@ -24,7 +24,6 @@ import ( ) func TestSearchHubCmd(t *testing.T) { - // Setup a mock search service var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -54,7 +53,6 @@ func TestSearchHubCmd(t *testing.T) { } func TestSearchHubListRepoCmd(t *testing.T) { - // Setup a mock search service var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 047fd60df..e5789db8b 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -142,7 +142,6 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } } - } } diff --git a/pkg/cmd/uninstall.go b/pkg/cmd/uninstall.go index 49f7bd19d..bfecd4741 100644 --- a/pkg/cmd/uninstall.go +++ b/pkg/cmd/uninstall.go @@ -59,7 +59,6 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return validationErr } for i := range args { - res, err := client.Run(args[i]) if err != nil { return err diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index f96f6ec0d..30188d3d9 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -34,7 +34,6 @@ import ( ) func TestUpgradeCmd(t *testing.T) { - tmpChart := t.TempDir() cfile := &chart.Chart{ Metadata: &chart.Metadata{ @@ -222,7 +221,6 @@ func TestUpgradeWithValue(t *testing.T) { if !strings.Contains(updatedRel.Manifest, "drink: tea") { t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) } - } func TestUpgradeWithStringValue(t *testing.T) { @@ -253,11 +251,9 @@ func TestUpgradeWithStringValue(t *testing.T) { if !strings.Contains(updatedRel.Manifest, "drink: coffee") { t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) } - } func TestUpgradeInstallWithSubchartNotes(t *testing.T) { - releaseName := "wacky-bunny-v1" relMock, ch, _ := prepareMockRelease(t, releaseName) @@ -289,11 +285,9 @@ func TestUpgradeInstallWithSubchartNotes(t *testing.T) { if !strings.Contains(upgradedRel.Info.Notes, "SUBCHART NOTES") { t.Errorf("The subchart notes are not set correctly. NOTES: %s", upgradedRel.Info.Notes) } - } func TestUpgradeWithValuesFile(t *testing.T) { - releaseName := "funny-bunny-v4" relMock, ch, chartPath := prepareMockRelease(t, releaseName) @@ -321,11 +315,9 @@ func TestUpgradeWithValuesFile(t *testing.T) { if !strings.Contains(updatedRel.Manifest, "drink: beer") { t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) } - } func TestUpgradeWithValuesFromStdin(t *testing.T) { - releaseName := "funny-bunny-v5" relMock, ch, chartPath := prepareMockRelease(t, releaseName) @@ -361,7 +353,6 @@ func TestUpgradeWithValuesFromStdin(t *testing.T) { } func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { - releaseName := "funny-bunny-v6" _, _, chartPath := prepareMockRelease(t, releaseName) @@ -392,7 +383,6 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { if !strings.Contains(updatedRel.Manifest, "drink: beer") { t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) } - } func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { diff --git a/pkg/cmd/verify_test.go b/pkg/cmd/verify_test.go index ae373afd2..050d799c9 100644 --- a/pkg/cmd/verify_test.go +++ b/pkg/cmd/verify_test.go @@ -22,7 +22,6 @@ import ( ) func TestVerifyCmd(t *testing.T) { - statExe := "stat" statPathMsg := "no such file or directory" statFileMsg := statPathMsg diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 9c26f925e..22c6c71a3 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -282,7 +282,6 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena // If provenance is requested, verify it. ver := &provenance.Verification{} if c.Verify > VerifyNever { - ppth, err := c.Cache.Get(digest32, CacheProv) if err == nil { slog.Debug("found provenance in cache", "id", digestString) @@ -308,7 +307,6 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena } if c.Verify != VerifyLater { - // provenance files pin to a specific name so this needs to be accounted for // when verifying. // Note, this does make an assumption that the name/version is unique to a diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index fd4815cc4..3a999738e 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -502,11 +502,9 @@ Loop: // in a known repo and attempt to ensure the data is present for steps like // version resolution. func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.Dependency) (map[string]string, error) { - var ru []*repo.Entry for _, dd := range deps { - // If the chart is in the local charts directory no repository needs // to be specified. if dd.Repository == "" { @@ -679,7 +677,6 @@ func dedupeRepos(repos []*repo.Entry) []*repo.Entry { } func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { - var wg sync.WaitGroup localRepos := dedupeRepos(repos) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 09edc3337..8f8d4494c 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -657,7 +657,6 @@ func TestRenderDependency(t *testing.T) { if out["outerchart/templates/outer"] != expect { t.Errorf("Expected %q, got %q", expect, out["outer"]) } - } func TestRenderNestedValues(t *testing.T) { @@ -813,7 +812,6 @@ func TestRenderBuiltinValues(t *testing.T) { t.Errorf("Expected %q, got %q", expect, out[file]) } } - } func TestAlterFuncMap_include(t *testing.T) { @@ -1009,7 +1007,6 @@ func TestAlterFuncMap_tplinclude(t *testing.T) { if got := out["TplFunction/templates/base"]; got != expect { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } - } func TestRenderRecursionLimit(t *testing.T) { @@ -1068,7 +1065,6 @@ func TestRenderRecursionLimit(t *testing.T) { if got := out["overlook/templates/quote"]; got != expect { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } - } func TestRenderLoadTemplateForTplFromFile(t *testing.T) { @@ -1319,7 +1315,6 @@ func TestRenderTplMissingKeyString(t *testing.T) { if !strings.Contains(errTxt, "noSuchKey") { t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt) } - } func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index 7d4581233..f3116d626 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -510,7 +510,6 @@ func TestDownloadInsecureSkipTLSVerify(t *testing.T) { if _, err = g.Get(u.String()); err != nil { t.Error(err) } - } func TestHTTPGetterTarDownload(t *testing.T) { diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index ef8b87503..c683a1090 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -41,7 +41,6 @@ func collectGetterPlugins(settings *cli.EnvSettings) (Providers, error) { env := plugin.FormatEnv(settings.EnvVars()) pluginConstructorBuilder := func(plg plugin.Plugin) Constructor { return func(option ...Option) (Getter, error) { - return &getterPlugin{ options: append([]Option{}, option...), plg: plg, diff --git a/pkg/helmpath/lazypath.go b/pkg/helmpath/lazypath.go index c1f868754..bf6d71aa7 100644 --- a/pkg/helmpath/lazypath.go +++ b/pkg/helmpath/lazypath.go @@ -38,7 +38,6 @@ const ( type lazypath string func (l lazypath) path(helmEnvVar, xdgEnvVar string, defaultFn func() string, elem ...string) string { - // There is an order to checking for a path. // 1. See if a Helm specific environment variable has been set. // 2. Check if an XDG environment variable is set diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 44f31cdbe..96722bcd0 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -521,7 +521,6 @@ func determineFieldValidationDirective(validate bool) FieldValidationDirective { } func buildResourceList(f Factory, namespace string, validationDirective FieldValidationDirective, reader io.Reader, transformRequest resource.RequestTransform) (ResourceList, error) { - schema, err := f.Validator(string(validationDirective)) if err != nil { return nil, err @@ -859,7 +858,6 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)), slog.Bool("upgradeClientSideFieldManager", updateOptions.upgradeClientSideFieldManager)) return func(original, target *resource.Info) error { - logger := c.Logger().With( slog.String("namespace", target.Namespace), slog.String("name", target.Name), @@ -954,7 +952,6 @@ func isIncompatibleServerError(err error) bool { // getManagedFieldsManager returns the manager string. If one was set it will be returned. // Otherwise, one is calculated based on the name of the binary. func getManagedFieldsManager() string { - // When a manager is explicitly set use it if ManagedFieldsManager != "" { return ManagedFieldsManager @@ -1102,7 +1099,6 @@ func createPatch(original runtime.Object, target *resource.Info, threeWayMergeFo } func replaceResource(target *resource.Info, fieldValidationDirective FieldValidationDirective) error { - helper := resource.NewHelper(target.Client, target.Mapping). WithFieldValidation(string(fieldValidationDirective)). WithFieldManager(getManagedFieldsManager()) @@ -1117,11 +1113,9 @@ func replaceResource(target *resource.Info, fieldValidationDirective FieldValida } return nil - } func patchResourceClientSide(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) error { - patch, patchType, err := createPatch(original, target, threeWayMergeForUnstructured) if err != nil { return fmt.Errorf("failed to create patch: %w", err) @@ -1155,14 +1149,12 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr // that upgrade CSA managed fields to SSA apply // see: https://github.com/kubernetes/kubernetes/pull/112905 func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) { - fieldManagerName := getManagedFieldsManager() patched := false err := retry.RetryOnConflict( retry.DefaultRetry, func() error { - if err := info.Get(); err != nil { return fmt.Errorf("failed to get object %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 31894f68e..9b47a2df2 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -282,7 +282,6 @@ func TestCreate(t *testing.T) { c := newTestClient(t) for name, tc := range testCases { t.Run(name, func(t *testing.T) { - client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) { return tc.Callback(t, tc, previous, req) }) @@ -317,7 +316,6 @@ func TestCreate(t *testing.T) { } assert.Equal(t, tc.ExpectedActions, actions) - }) } } @@ -444,7 +442,6 @@ func TestUpdate(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - listOriginal := tc.OriginalPods listTarget := tc.TargetPods @@ -1409,7 +1406,6 @@ func TestIsReachable(t *testing.T) { if !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("expected error message to contain '%s', got: %v", tt.errorContains, err) } - } else { if err != nil { t.Errorf("expected no error but got: %v", err) @@ -1487,7 +1483,6 @@ func TestReplaceResource(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - testFactory := cmdtesting.NewTestFactory() t.Cleanup(testFactory.Cleanup) @@ -1610,7 +1605,6 @@ func TestPatchResourceClientSide(t *testing.T) { t.Fail() return nil, nil - }, ExpectedErrorContains: "cannot patch \"whale\" with kind Pod: the server reported a conflict", }, @@ -1632,14 +1626,12 @@ func TestPatchResourceClientSide(t *testing.T) { t.Fail() return nil, nil // newResponse(http.StatusOK, &tc.TargetPods.Items[0]) - }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - testFactory := cmdtesting.NewTestFactory() t.Cleanup(testFactory.Cleanup) @@ -1806,7 +1798,6 @@ func TestPatchResourceServerSide(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - testFactory := cmdtesting.NewTestFactory() t.Cleanup(testFactory.Cleanup) @@ -1837,7 +1828,6 @@ func TestPatchResourceServerSide(t *testing.T) { } func TestDetermineFieldValidationDirective(t *testing.T) { - assert.Equal(t, FieldValidationDirectiveIgnore, determineFieldValidationDirective(false)) assert.Equal(t, FieldValidationDirectiveStrict, determineFieldValidationDirective(true)) } diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index b00fc616d..d70b7e912 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -43,14 +43,12 @@ func extractChartMeta(chartData []byte) (*chart.Metadata, error) { // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { - // Get annotations from Chart attributes ociAnnotations := generateChartOCIAnnotations(meta, creationTime) // Copy Chart annotations annotations: for chartAnnotationKey, chartAnnotationValue := range meta.Annotations { - // Avoid overriding key properties for _, immutableOciKey := range immutableOciAnnotations { if immutableOciKey == chartAnnotationKey { @@ -88,7 +86,6 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ var maintainerSb strings.Builder for maintainerIdx, maintainer := range meta.Maintainers { - if len(maintainer.Name) > 0 { maintainerSb.WriteString(maintainer.Name) } @@ -102,11 +99,9 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ if maintainerIdx < len(meta.Maintainers)-1 { maintainerSb.WriteString(", ") } - } chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String()) - } return chartOCIAnnotations @@ -114,7 +109,6 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ // addToMap takes an existing map and adds an item if the value is not empty func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string { - // Add item to map if its if len(strings.TrimSpace(newValue)) > 0 { inputMap[newKey] = newValue diff --git a/pkg/registry/chart_test.go b/pkg/registry/chart_test.go index 77ccdaab7..0b247601b 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -27,7 +27,6 @@ import ( ) func TestGenerateOCIChartAnnotations(t *testing.T) { - nowString := time.Now().Format(time.RFC3339) tests := []struct { @@ -147,18 +146,15 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { } for _, tt := range tests { - result := generateChartOCIAnnotations(tt.chart, nowString) if !reflect.DeepEqual(tt.expect, result) { t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) } - } } func TestGenerateOCIAnnotations(t *testing.T) { - nowString := time.Now().Format(time.RFC3339) tests := []struct { @@ -221,18 +217,15 @@ func TestGenerateOCIAnnotations(t *testing.T) { } for _, tt := range tests { - result := generateOCIAnnotations(tt.chart, nowString) if !reflect.DeepEqual(tt.expect, result) { t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) } - } } func TestGenerateOCICreatedAnnotations(t *testing.T) { - nowTime := time.Now() nowTimeString := nowTime.Format(time.RFC3339) @@ -268,7 +261,5 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { if !nowTime.Before(createdTimeAnnotation) { t.Errorf("%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString) } - } - } diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 62e560dba..2f3b344aa 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -816,7 +816,6 @@ func (c *Client) Tags(ref string) ([]string, error) { } return tags, nil - } // Resolve a reference to a descriptor. @@ -910,7 +909,6 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *ur func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store, configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor, ociAnnotations map[string]string, parsedRef reference) (ocispec.Descriptor, error) { - manifest := ocispec.Manifest{ Versioned: specs.Versioned{SchemaVersion: 2}, Config: configDescriptor, diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index b0c08fa3c..1e944025f 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -380,7 +380,6 @@ func initFakeRegistryTestServer() string { } func testPush(suite *TestRegistry) { - testingChartCreationTime := "1977-09-02T22:04:05Z" // Bad bytes diff --git a/pkg/release/v1/util/manifest_sorter_test.go b/pkg/release/v1/util/manifest_sorter_test.go index 4360013e5..baa320be0 100644 --- a/pkg/release/v1/util/manifest_sorter_test.go +++ b/pkg/release/v1/util/manifest_sorter_test.go @@ -26,7 +26,6 @@ import ( ) func TestSortManifests(t *testing.T) { - data := []struct { name []string path string @@ -183,7 +182,6 @@ metadata: if !reflect.DeepEqual(expectedHooks, out.Events) { t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) } - } } if !found { diff --git a/pkg/repo/v1/chartrepo.go b/pkg/repo/v1/chartrepo.go index deef7474e..09b74d602 100644 --- a/pkg/repo/v1/chartrepo.go +++ b/pkg/repo/v1/chartrepo.go @@ -173,7 +173,6 @@ func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) FindChartInRepoURLOpt // FindChartInRepoURL finds chart in chart repository pointed by repoURL // without adding repo to repositories func FindChartInRepoURL(repoURL string, chartName string, getters getter.Providers, options ...FindChartInRepoURLOption) (string, error) { - opts := findChartInRepoURLOptions{} for _, option := range options { option(&opts) diff --git a/pkg/repo/v1/chartrepo_test.go b/pkg/repo/v1/chartrepo_test.go index 7cffc04b6..fbd081e1b 100644 --- a/pkg/repo/v1/chartrepo_test.go +++ b/pkg/repo/v1/chartrepo_test.go @@ -126,7 +126,6 @@ func TestConcurrencyDownloadIndex(t *testing.T) { // 2) read index.yaml via LoadIndexFile (read operation). // This checks for race conditions and ensures correct behavior under concurrent read/write access. for range 150 { - wg.Go(func() { idx, err := repo.DownloadIndexFile() if err != nil { @@ -234,7 +233,6 @@ func TestFindChartInRepoURL(t *testing.T) { } func TestErrorFindChartInRepoURL(t *testing.T) { - g := getter.All(&cli.EnvSettings{ RepositoryCache: t.TempDir(), }) diff --git a/pkg/repo/v1/index_test.go b/pkg/repo/v1/index_test.go index 6c6bb7835..3cacc95d4 100644 --- a/pkg/repo/v1/index_test.go +++ b/pkg/repo/v1/index_test.go @@ -140,7 +140,6 @@ func TestIndexFile(t *testing.T) { } func TestLoadIndex(t *testing.T) { - tests := []struct { Name string Filename string @@ -250,7 +249,6 @@ func TestMerge(t *testing.T) { if v := vs[1]; v.Version != "0.2.0" { t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version) } - } func TestDownloadIndexFile(t *testing.T) { @@ -642,7 +640,6 @@ func TestIgnoreSkippableChartValidationError(t *testing.T) { if !errors.Is(tc.Input, result) { t.Error("expected the result equal to input") } - }) } } diff --git a/pkg/repo/v1/repotest/server_test.go b/pkg/repo/v1/repotest/server_test.go index f0e374fc0..499091a57 100644 --- a/pkg/repo/v1/repotest/server_test.go +++ b/pkg/repo/v1/repotest/server_test.go @@ -143,7 +143,6 @@ func TestNewTempServer(t *testing.T) { if res.StatusCode != http.StatusOK { t.Errorf("Expected 200, got %d", res.StatusCode) } - } { @@ -203,7 +202,6 @@ func TestNewTempServer(t *testing.T) { } }) } - } func TestNewTempServer_TLS(t *testing.T) { diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 329b82b2f..da3e02f42 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -300,5 +300,4 @@ func TestMemoryDelete(t *testing.T) { t.Logf("Name: %s, Version: %d", rac.Name(), rac.Version()) } } - } diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 21d9f6679..ea85756eb 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -134,7 +134,6 @@ func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { } func (s *SQL) ensureDBSetup() error { - migrations := &migrate.MemoryMigrationSource{ Migrations: []*migrate.Migration{ { diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index cecaa2453..1dbbd0e1b 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -330,7 +330,6 @@ func (t *parser) keyIndex() (int, error) { } // v should be the index return strconv.Atoi(string(v)) - } func (t *parser) listItem(list []any, i, nestedNameLevel int) ([]any, error) { From af31a679244b584317fd8053aee498c4d5ec301c Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Sat, 14 Mar 2026 09:06:55 +0100 Subject: [PATCH 013/132] chore: enable errorlint linter Signed-off-by: Matthieu MOREL --- .golangci.yml | 7 +++++++ pkg/action/validate.go | 14 +++++++------- pkg/cmd/lint.go | 2 +- pkg/cmd/plugin_uninstall.go | 2 +- pkg/cmd/plugin_update.go | 2 +- pkg/downloader/manager.go | 8 ++++---- pkg/kube/client.go | 4 ++-- pkg/storage/driver/sql.go | 4 ++-- 8 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 536b4b212..f6fd1b02f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,6 +21,7 @@ linters: enable: - depguard - dupl + - errorlint - exhaustive - gomodguard - govet @@ -63,6 +64,12 @@ linters: dupl: threshold: 400 + errorlint: + # Check for plain type assertions and type switches. + asserts: false + # Check for plain error comparisons. + comparison: false + exhaustive: default-signifies-exhaustive: true diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 94bf4906b..8a127a512 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -93,7 +93,7 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { - return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) + return fmt.Errorf("%s exists and cannot be imported into the current release: %w", resourceString(info), err) } infoCopy := *info @@ -116,13 +116,13 @@ func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) er var errs []error if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil { - errs = append(errs, fmt.Errorf("label validation error: %s", err)) + errs = append(errs, fmt.Errorf("label validation error: %w", err)) } if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil { - errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + errs = append(errs, fmt.Errorf("annotation validation error: %w", err)) } if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil { - errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + errs = append(errs, fmt.Errorf("annotation validation error: %w", err)) } if len(errs) > 0 { @@ -154,7 +154,7 @@ func setMetadataVisitor(releaseName, releaseNamespace string, forceOwnership boo if !forceOwnership { if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil { - return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err) + return fmt.Errorf("%s cannot be owned: %w", resourceString(info), err) } } @@ -162,7 +162,7 @@ func setMetadataVisitor(releaseName, releaseNamespace string, forceOwnership boo appManagedByLabel: appManagedByHelm, }); err != nil { return fmt.Errorf( - "%s labels could not be updated: %s", + "%s labels could not be updated: %w", resourceString(info), err, ) } @@ -172,7 +172,7 @@ func setMetadataVisitor(releaseName, releaseNamespace string, forceOwnership boo helmReleaseNamespaceAnnotation: releaseNamespace, }); err != nil { return fmt.Errorf( - "%s annotations could not be updated: %s", + "%s annotations could not be updated: %w", resourceString(info), err, ) } diff --git a/pkg/cmd/lint.go b/pkg/cmd/lint.go index ccc53ddd0..1b5c3b212 100644 --- a/pkg/cmd/lint.go +++ b/pkg/cmd/lint.go @@ -59,7 +59,7 @@ func newLintCmd(out io.Writer) *cobra.Command { if kubeVersion != "" { parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { - return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) + return fmt.Errorf("invalid kube version '%s': %w", kubeVersion, err) } client.KubeVersion = parsedKubeVersion } diff --git a/pkg/cmd/plugin_uninstall.go b/pkg/cmd/plugin_uninstall.go index 85eb46219..88c46fe85 100644 --- a/pkg/cmd/plugin_uninstall.go +++ b/pkg/cmd/plugin_uninstall.go @@ -70,7 +70,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error { for _, name := range o.names { if found := findPlugin(plugins, name); found != nil { if err := uninstallPlugin(found); err != nil { - errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%v)", name, err)) + errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%w)", name, err)) } else { fmt.Fprintf(out, "Uninstalled plugin: %s\n", name) } diff --git a/pkg/cmd/plugin_update.go b/pkg/cmd/plugin_update.go index 6cc2729fc..f7c2e5c2d 100644 --- a/pkg/cmd/plugin_update.go +++ b/pkg/cmd/plugin_update.go @@ -71,7 +71,7 @@ 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 (%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) } diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index fd4815cc4..39112ef03 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -261,7 +261,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { return err } } else { - return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err) + return fmt.Errorf("unable to retrieve file info for '%s': %w", destPath, err) } // Prepare tmpPath @@ -281,17 +281,17 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { chartPath := filepath.Join(destPath, dep.Name) ch, err := loader.LoadDir(chartPath) if err != nil { - return fmt.Errorf("unable to load chart '%s': %v", chartPath, err) + return fmt.Errorf("unable to load chart '%s': %w", chartPath, err) } constraint, err := semver.NewConstraint(dep.Version) if err != nil { - return fmt.Errorf("dependency %s has an invalid version/constraint format: %s", dep.Name, err) + return fmt.Errorf("dependency %s has an invalid version/constraint format: %w", dep.Name, err) } v, err := semver.NewVersion(ch.Metadata.Version) if err != nil { - return fmt.Errorf("invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) + return fmt.Errorf("invalid version %s for dependency %s: %w", dep.Version, dep.Name, err) } if !constraint.Check(v) { diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 44f31cdbe..81eae66e3 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1234,7 +1234,7 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts ) if err != nil { if isIncompatibleServerError(err) { - return fmt.Errorf("server-side apply not available on the server: %v", err) + return fmt.Errorf("server-side apply not available on the server: %w", err) } if apierrors.IsConflict(err) { @@ -1251,7 +1251,7 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) { podList, err := c.kubeClient.CoreV1().Pods(namespace).List(context.Background(), listOptions) if err != nil { - return nil, fmt.Errorf("failed to get pod list with options: %+v with error: %v", listOptions, err) + return nil, fmt.Errorf("failed to get pod list with options: %+v with error: %w", listOptions, err) } return podList, nil } diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 21d9f6679..f274be1fb 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -485,7 +485,7 @@ func (s *SQL) Create(key string, rel release.Releaser) error { transaction, err := s.db.Beginx() if err != nil { s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) - return fmt.Errorf("error beginning transaction: %v", err) + return fmt.Errorf("error beginning transaction: %w", err) } insertQuery, args, err := s.statementBuilder. @@ -623,7 +623,7 @@ func (s *SQL) Delete(key string) (release.Releaser, error) { transaction, err := s.db.Beginx() if err != nil { s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) - return nil, fmt.Errorf("error beginning transaction: %v", err) + return nil, fmt.Errorf("error beginning transaction: %w", err) } selectQuery, args, err := s.statementBuilder. From 83a0138172e22f5efb7d9852b3ed0fb5fc8117b2 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Sat, 14 Mar 2026 09:16:26 +0100 Subject: [PATCH 014/132] chore: fix deprecatedComment issues from gocritic Signed-off-by: Matthieu MOREL --- .golangci.yml | 43 ++++++++++++++++++++++++++++++ internal/chart/v3/chart.go | 1 + internal/plugin/metadata_legacy.go | 6 +++-- pkg/chart/v2/chart.go | 1 + 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 536b4b212..ccd721a00 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,6 +22,7 @@ linters: - depguard - dupl - exhaustive + - gocritic - gomodguard - govet - ineffassign @@ -66,6 +67,48 @@ linters: exhaustive: default-signifies-exhaustive: true + gocritic: + disabled-checks: + - appendAssign + - appendCombine + - assignOp + - badCall + - commentedOutCode + - commentFormatting + - deferInLoop + - elseif + - emptyStringTest + - exposedSyncMutex + - filepathJoin + - httpNoBody + - hugeParam + - ifElseChain + - importShadow + - nestingReduce + - nilValReturn + - octalLiteral + - paramTypeCombine + - preferStringWriter + - ptrToRefParam + - rangeValCopy + - regexpSimplify + - singleCaseSwitch + - sloppyReassign + - sloppyLen + - sprintfQuotedString + - stringConcatSimplify + - stringsCompare + - stringXbytes + - todoCommentWithoutDetail + - tooManyResultsChecker + - typeAssertChain + - uncheckedInlineErr + - unnamedResult + - unnecessaryDefer + - unslice + - whyNoLint + enable-all: true + gomodguard: blocked: modules: diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go index d76a5688c..71a90c855 100644 --- a/internal/chart/v3/chart.go +++ b/internal/chart/v3/chart.go @@ -140,6 +140,7 @@ func (ch *Chart) AppVersion() string { } // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. +// // Deprecated: use CRDObjects() func (ch *Chart) CRDs() []*common.File { files := []*common.File{} diff --git a/internal/plugin/metadata_legacy.go b/internal/plugin/metadata_legacy.go index b5849edeb..47db9fe7f 100644 --- a/internal/plugin/metadata_legacy.go +++ b/internal/plugin/metadata_legacy.go @@ -50,7 +50,8 @@ type MetadataLegacy struct { PlatformCommand []PlatformCommand `yaml:"platformCommand"` // Command is the plugin command, as a single string. - // DEPRECATED: Use PlatformCommand instead. Removed in subprocess/v1 plugins. + // + // Deprecated: Use PlatformCommand instead. Removed in subprocess/v1 plugins. Command string `yaml:"command"` // IgnoreFlags ignores any flags passed in from Helm @@ -60,7 +61,8 @@ type MetadataLegacy struct { PlatformHooks PlatformHooks `yaml:"platformHooks"` // Hooks are commands that will run on plugin events, as a single string. - // DEPRECATED: Use PlatformHooks instead. Removed in subprocess/v1 plugins. + // + // Deprecated: Use PlatformHooks instead. Removed in subprocess/v1 plugins. Hooks Hooks `yaml:"hooks"` // Downloaders field is used if the plugin supply downloader mechanism diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index 4cfc2b890..9d798b4de 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -143,6 +143,7 @@ func (ch *Chart) AppVersion() string { } // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. +// // Deprecated: use CRDObjects() func (ch *Chart) CRDs() []*common.File { files := []*common.File{} From 071558d69ffbb408dcb56403d387a1aa90a7d3a8 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Sat, 14 Mar 2026 09:32:42 +0100 Subject: [PATCH 015/132] chore: fix bool-compare issues from testifylint Signed-off-by: Matthieu MOREL --- .golangci.yml | 19 +++++++++++++++++++ internal/chart/v3/chart_test.go | 4 ++-- pkg/action/get_values_test.go | 2 +- pkg/action/push_test.go | 4 ++-- pkg/action/registry_login_test.go | 4 ++-- pkg/chart/v2/chart_test.go | 4 ++-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 536b4b212..bbd7a18b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,6 +32,7 @@ linters: - revive - sloglint - staticcheck + - testifylint - thelper - unused - usestdlibvars @@ -92,6 +93,24 @@ linters: - helpers - models + testifylint: + disable: + - empty + - encoded-compare + - equal-values + - error-is-as + - error-nil + - expected-actual + - float-compare + - go-require + - len + - nil-compare + - require-error + - suite-dont-use-pkg + - suite-extra-assert-call + # Intentionally enable all testifylint rules so new checks are adopted automatically. + enable-all: true + run: timeout: 10m diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index 07cbf4b39..5f6ca548d 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) { is := assert.New(t) - is.Equal(false, chrt1.IsRoot()) - is.Equal(true, chrt2.IsRoot()) + is.False(chrt1.IsRoot()) + is.True(chrt2.IsRoot()) } func TestChartPath(t *testing.T) { diff --git a/pkg/action/get_values_test.go b/pkg/action/get_values_test.go index 01ee4c3f1..c4cbdf2d5 100644 --- a/pkg/action/get_values_test.go +++ b/pkg/action/get_values_test.go @@ -37,7 +37,7 @@ func TestNewGetValues(t *testing.T) { assert.NotNil(t, client) assert.Equal(t, cfg, client.cfg) assert.Equal(t, 0, client.Version) - assert.Equal(t, false, client.AllValues) + assert.False(t, client.AllValues) } func TestGetValues_Run_UserConfigOnly(t *testing.T) { diff --git a/pkg/action/push_test.go b/pkg/action/push_test.go index 35c6f3efc..125799252 100644 --- a/pkg/action/push_test.go +++ b/pkg/action/push_test.go @@ -47,14 +47,14 @@ func TestNewPushWithInsecureSkipTLSVerify(t *testing.T) { client := NewPushWithOpts(WithInsecureSkipTLSVerify(true)) assert.NotNil(t, client) - assert.Equal(t, true, client.insecureSkipTLSVerify) + assert.True(t, client.insecureSkipTLSVerify) } func TestNewPushWithPlainHTTP(t *testing.T) { client := NewPushWithOpts(WithPlainHTTP(true)) assert.NotNil(t, client) - assert.Equal(t, true, client.plainHTTP) + assert.True(t, client.plainHTTP) } func TestNewPushWithPushOptWriter(t *testing.T) { diff --git a/pkg/action/registry_login_test.go b/pkg/action/registry_login_test.go index de2450d9d..590673b3a 100644 --- a/pkg/action/registry_login_test.go +++ b/pkg/action/registry_login_test.go @@ -48,7 +48,7 @@ func TestWithInsecure(t *testing.T) { opt := WithInsecure(true) assert.Nil(t, opt(client)) - assert.Equal(t, true, client.insecure) + assert.True(t, client.insecure) } func TestWithKeyFile(t *testing.T) { @@ -80,5 +80,5 @@ func TestWithPlainHTTPLogin(t *testing.T) { opt := WithPlainHTTPLogin(true) assert.Nil(t, opt(client)) - assert.Equal(t, true, client.plainHTTP) + assert.True(t, client.plainHTTP) } diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index d0837eb16..d44e7251b 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) { is := assert.New(t) - is.Equal(false, chrt1.IsRoot()) - is.Equal(true, chrt2.IsRoot()) + is.False(chrt1.IsRoot()) + is.True(chrt2.IsRoot()) } func TestChartPath(t *testing.T) { From 154993723aadf45601d124c6750e8f4ae3b9f2fd Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Thu, 26 Mar 2026 20:42:56 +0530 Subject: [PATCH 016/132] refactor(cli): decouple EnvSettings from pkg/kube to avoid import cycles Move the retrying round tripper used by EnvSettings into pkg/cli so pkg/cli no longer imports pkg/kube. This preserves retry behavior while breaking the import edge that triggers cycles for Helm library consumers (such as the kustomize integration described in #31965). Signed-off-by: Sumit Solanki --- pkg/cli/environment.go | 3 +- pkg/cli/roundtripper.go | 82 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 pkg/cli/roundtripper.go diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 83b6bdbba..8faa628fc 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -35,7 +35,6 @@ import ( "helm.sh/helm/v4/internal/version" "helm.sh/helm/v4/pkg/helmpath" - "helm.sh/helm/v4/pkg/kube" ) // defaultMaxHistory sets the maximum number of releases to 0: unlimited @@ -134,7 +133,7 @@ func New() *EnvSettings { config.Burst = env.BurstLimit config.QPS = env.QPS config.Wrap(func(rt http.RoundTripper) http.RoundTripper { - return &kube.RetryingRoundTripper{Wrapped: rt} + return &retryingRoundTripper{wrapped: rt} }) config.UserAgent = version.GetUserAgent() return config diff --git a/pkg/cli/roundtripper.go b/pkg/cli/roundtripper.go new file mode 100644 index 000000000..040ecd48f --- /dev/null +++ b/pkg/cli/roundtripper.go @@ -0,0 +1,82 @@ +/* +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 cli + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" +) + +// retryingRoundTripper retries selected transient Kubernetes API server failures. +// Keeping this in pkg/cli avoids importing pkg/kube from EnvSettings. +type retryingRoundTripper struct { + wrapped http.RoundTripper +} + +func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt.roundTrip(req, 1, nil) +} + +func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { + if retry < 0 { + return prevResp, nil + } + resp, rtErr := rt.wrapped.RoundTrip(req) + if rtErr != nil { + return resp, rtErr + } + if resp.StatusCode < 500 { + return resp, rtErr + } + if resp.Header.Get("content-type") != "application/json" { + return resp, rtErr + } + b, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp, err + } + + var ke kubernetesError + r := bytes.NewReader(b) + err = json.NewDecoder(r).Decode(&ke) + r.Seek(0, io.SeekStart) + resp.Body = io.NopCloser(r) + if err != nil { + return resp, err + } + if ke.Code < 500 { + return resp, nil + } + // Matches messages like "etcdserver: leader changed" + if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { + return rt.roundTrip(req, retry-1, resp) + } + // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped" + if strings.HasSuffix(ke.Message, "raft proposal dropped") { + return rt.roundTrip(req, retry-1, resp) + } + return resp, nil +} + +type kubernetesError struct { + Message string `json:"message"` + Code int `json:"code"` +} From 64f1d0af5b53f0a9292af2ba1efc42a46a57ed00 Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Fri, 27 Mar 2026 11:56:33 +0530 Subject: [PATCH 017/132] refactor(cli): decouple EnvSettings from pkg/kube Move the retrying HTTP round-tripper used by EnvSettings into pkg/cli so pkg/cli no longer imports pkg/kube, avoiding import cycles for Helm library consumers while preserving retry behavior for transient API server errors. Add pkg/cli/roundtripper_test.go with parity coverage for the moved logic. Signed-off-by: Sumit Solanki Made-with: Cursor --- pkg/cli/roundtripper_test.go | 161 +++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 pkg/cli/roundtripper_test.go diff --git a/pkg/cli/roundtripper_test.go b/pkg/cli/roundtripper_test.go new file mode 100644 index 000000000..68fc075b0 --- /dev/null +++ b/pkg/cli/roundtripper_test.go @@ -0,0 +1,161 @@ +/* +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 cli + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type fakeRoundTripper struct { + resp *http.Response + err error + calls int +} + +func (f *fakeRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { + f.calls++ + return f.resp, f.err +} + +func newRespWithBody(statusCode int, contentType, body string) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Header: http.Header{"Content-Type": []string{contentType}}, + Body: io.NopCloser(strings.NewReader(body)), + } +} + +func TestRetryingRoundTripper_RoundTrip(t *testing.T) { + marshalErr := func(code int, msg string) string { + b, _ := json.Marshal(kubernetesError{ + Code: code, + Message: msg, + }) + return string(b) + } + + tests := []struct { + name string + resp *http.Response + err error + expectedCalls int + expectedErr string + expectedCode int + }{ + { + name: "no retry, status < 500 returns response", + resp: newRespWithBody(200, "application/json", `{"message":"ok","code":200}`), + err: nil, + expectedCalls: 1, + expectedCode: 200, + }, + { + name: "error from wrapped RoundTripper propagates", + resp: nil, + err: errors.New("wrapped error"), + expectedCalls: 1, + expectedErr: "wrapped error", + }, + { + name: "no retry, content-type not application/json", + resp: newRespWithBody(500, "text/plain", "server error"), + err: nil, + expectedCalls: 1, + expectedCode: 500, + }, + { + name: "error reading body returns error", + resp: &http.Response{ + StatusCode: http.StatusInternalServerError, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: &errReader{}, + }, + err: nil, + expectedCalls: 1, + expectedErr: "read error", + }, + { + name: "error decoding JSON returns error", + resp: newRespWithBody(500, "application/json", `invalid-json`), + err: nil, + expectedCalls: 1, + expectedErr: "invalid character", + }, + { + name: "retry on etcdserver leader changed message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "some error etcdserver: leader changed")), + err: nil, + expectedCalls: 2, + expectedCode: 500, + }, + { + name: "retry on raft proposal dropped message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "rpc error: code = Unknown desc = raft proposal dropped")), + err: nil, + expectedCalls: 2, + expectedCode: 500, + }, + { + name: "no retry on other error message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "other server error")), + err: nil, + expectedCalls: 1, + expectedCode: 500, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeRT := &fakeRoundTripper{ + resp: tt.resp, + err: tt.err, + } + rt := retryingRoundTripper{ + wrapped: fakeRT, + } + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + resp, err := rt.RoundTrip(req) + + if tt.expectedErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + return + } + assert.NoError(t, err) + + assert.Equal(t, tt.expectedCode, resp.StatusCode) + assert.Equal(t, tt.expectedCalls, fakeRT.calls) + }) + } +} + +type errReader struct{} + +func (e *errReader) Read(_ []byte) (int, error) { + return 0, errors.New("read error") +} + +func (e *errReader) Close() error { + return nil +} From f257c95c783f5595e36cb5a7dcc862cc1f6266b5 Mon Sep 17 00:00:00 2001 From: abhay1999 Date: Mon, 30 Mar 2026 19:13:32 +0530 Subject: [PATCH 018/132] fix(kube): clarify server-side apply patch errors Signed-off-by: abhay1999 --- pkg/kube/client.go | 2 +- pkg/kube/client_test.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 44f31cdbe..c955e8875 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1241,7 +1241,7 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts return fmt.Errorf("conflict occurred while applying object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) } - return err + return fmt.Errorf("server-side apply failed for object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) } return target.Refresh(obj, true) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 31894f68e..e98d87520 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -1802,6 +1802,23 @@ func TestPatchResourceServerSide(t *testing.T) { }, ExpectedErrorContains: "the server reported a conflict", }, + "generic server-side apply error": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) { + t.Helper() + + return newResponse(http.StatusBadRequest, &metav1.Status{ + Status: metav1.StatusFailure, + Message: `failed to create typed patch object: .spec.template.spec.containers[name="test"].env: duplicate entries for key [name="SERVER_CONTEXT_PATH"]`, + Reason: metav1.StatusReasonBadRequest, + Code: http.StatusBadRequest, + }) + }, + ExpectedErrorContains: "server-side apply failed for object default/whale /v1, Kind=Pod: failed to create typed patch object", + }, } for name, tc := range testCases { From 8ef2d45934ba1b9ca341818f1157112fcf7cdf1d Mon Sep 17 00:00:00 2001 From: Rhys McNeill Date: Sun, 5 Apr 2026 15:55:56 +0100 Subject: [PATCH 019/132] fix(action): return correct error variable in prepareUpgrade When Deployed() succeeds but releaserToV1Release() fails, prepareUpgrade returned err (nil) instead of cerr (the conversion error), causing a silent nil return that could lead to nil pointer dereferences downstream. Closes #32007 Signed-off-by: Rhys McNeill --- pkg/action/upgrade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 0f360fe37..103ab4fdb 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -253,7 +253,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str var cerr error currentRelease, cerr = releaserToV1Release(currentReleasei) if cerr != nil { - return nil, nil, false, err + return nil, nil, false, cerr } if err != nil { if errors.Is(err, driver.ErrNoDeployedReleases) && From 213c869a988f2c7390c65673e3d677970d6220fd Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Thu, 9 Apr 2026 22:46:41 +0530 Subject: [PATCH 020/132] refactor(cli): share RetryingRoundTripper via pkg/kubeenv Move implementation to pkg/kubeenv per review; kube.RetryingRoundTripper remains a type alias for API compatibility. pkg/cli uses kubeenv only. Signed-off-by: Sumit Solanki --- pkg/cli/environment.go | 3 +- pkg/cli/roundtripper_test.go | 161 --------------------- pkg/kube/roundtripper.go | 66 +-------- pkg/{cli => kubeenv}/roundtripper.go | 18 ++- pkg/{kube => kubeenv}/roundtripper_test.go | 2 +- 5 files changed, 18 insertions(+), 232 deletions(-) delete mode 100644 pkg/cli/roundtripper_test.go rename pkg/{cli => kubeenv}/roundtripper.go (77%) rename pkg/{kube => kubeenv}/roundtripper_test.go (99%) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 8faa628fc..45d773eb4 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -35,6 +35,7 @@ import ( "helm.sh/helm/v4/internal/version" "helm.sh/helm/v4/pkg/helmpath" + "helm.sh/helm/v4/pkg/kubeenv" ) // defaultMaxHistory sets the maximum number of releases to 0: unlimited @@ -133,7 +134,7 @@ func New() *EnvSettings { config.Burst = env.BurstLimit config.QPS = env.QPS config.Wrap(func(rt http.RoundTripper) http.RoundTripper { - return &retryingRoundTripper{wrapped: rt} + return &kubeenv.RetryingRoundTripper{Wrapped: rt} }) config.UserAgent = version.GetUserAgent() return config diff --git a/pkg/cli/roundtripper_test.go b/pkg/cli/roundtripper_test.go deleted file mode 100644 index 68fc075b0..000000000 --- a/pkg/cli/roundtripper_test.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -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 cli - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -type fakeRoundTripper struct { - resp *http.Response - err error - calls int -} - -func (f *fakeRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { - f.calls++ - return f.resp, f.err -} - -func newRespWithBody(statusCode int, contentType, body string) *http.Response { - return &http.Response{ - StatusCode: statusCode, - Header: http.Header{"Content-Type": []string{contentType}}, - Body: io.NopCloser(strings.NewReader(body)), - } -} - -func TestRetryingRoundTripper_RoundTrip(t *testing.T) { - marshalErr := func(code int, msg string) string { - b, _ := json.Marshal(kubernetesError{ - Code: code, - Message: msg, - }) - return string(b) - } - - tests := []struct { - name string - resp *http.Response - err error - expectedCalls int - expectedErr string - expectedCode int - }{ - { - name: "no retry, status < 500 returns response", - resp: newRespWithBody(200, "application/json", `{"message":"ok","code":200}`), - err: nil, - expectedCalls: 1, - expectedCode: 200, - }, - { - name: "error from wrapped RoundTripper propagates", - resp: nil, - err: errors.New("wrapped error"), - expectedCalls: 1, - expectedErr: "wrapped error", - }, - { - name: "no retry, content-type not application/json", - resp: newRespWithBody(500, "text/plain", "server error"), - err: nil, - expectedCalls: 1, - expectedCode: 500, - }, - { - name: "error reading body returns error", - resp: &http.Response{ - StatusCode: http.StatusInternalServerError, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: &errReader{}, - }, - err: nil, - expectedCalls: 1, - expectedErr: "read error", - }, - { - name: "error decoding JSON returns error", - resp: newRespWithBody(500, "application/json", `invalid-json`), - err: nil, - expectedCalls: 1, - expectedErr: "invalid character", - }, - { - name: "retry on etcdserver leader changed message", - resp: newRespWithBody(500, "application/json", marshalErr(500, "some error etcdserver: leader changed")), - err: nil, - expectedCalls: 2, - expectedCode: 500, - }, - { - name: "retry on raft proposal dropped message", - resp: newRespWithBody(500, "application/json", marshalErr(500, "rpc error: code = Unknown desc = raft proposal dropped")), - err: nil, - expectedCalls: 2, - expectedCode: 500, - }, - { - name: "no retry on other error message", - resp: newRespWithBody(500, "application/json", marshalErr(500, "other server error")), - err: nil, - expectedCalls: 1, - expectedCode: 500, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fakeRT := &fakeRoundTripper{ - resp: tt.resp, - err: tt.err, - } - rt := retryingRoundTripper{ - wrapped: fakeRT, - } - req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) - resp, err := rt.RoundTrip(req) - - if tt.expectedErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedErr) - return - } - assert.NoError(t, err) - - assert.Equal(t, tt.expectedCode, resp.StatusCode) - assert.Equal(t, tt.expectedCalls, fakeRT.calls) - }) - } -} - -type errReader struct{} - -func (e *errReader) Read(_ []byte) (int, error) { - return 0, errors.New("read error") -} - -func (e *errReader) Close() error { - return nil -} diff --git a/pkg/kube/roundtripper.go b/pkg/kube/roundtripper.go index 52cb5bad2..e13f2103a 100644 --- a/pkg/kube/roundtripper.go +++ b/pkg/kube/roundtripper.go @@ -16,65 +16,9 @@ limitations under the License. package kube -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strings" -) +import "helm.sh/helm/v4/pkg/kubeenv" -type RetryingRoundTripper struct { - Wrapped http.RoundTripper -} - -func (rt *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return rt.roundTrip(req, 1, nil) -} - -func (rt *RetryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { - if retry < 0 { - return prevResp, nil - } - resp, rtErr := rt.Wrapped.RoundTrip(req) - if rtErr != nil { - return resp, rtErr - } - if resp.StatusCode < 500 { - return resp, rtErr - } - if resp.Header.Get("content-type") != "application/json" { - return resp, rtErr - } - b, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return resp, err - } - - var ke kubernetesError - r := bytes.NewReader(b) - err = json.NewDecoder(r).Decode(&ke) - r.Seek(0, io.SeekStart) - resp.Body = io.NopCloser(r) - if err != nil { - return resp, err - } - if ke.Code < 500 { - return resp, nil - } - // Matches messages like "etcdserver: leader changed" - if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { - return rt.roundTrip(req, retry-1, resp) - } - // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped" - if strings.HasSuffix(ke.Message, "raft proposal dropped") { - return rt.roundTrip(req, retry-1, resp) - } - return resp, nil -} - -type kubernetesError struct { - Message string `json:"message"` - Code int `json:"code"` -} +// RetryingRoundTripper retries transient Kubernetes API server errors on a +// wrapped [http.RoundTripper]. The implementation lives in [kubeenv] so +// consumers can depend on that package without importing all of kube. +type RetryingRoundTripper = kubeenv.RetryingRoundTripper diff --git a/pkg/cli/roundtripper.go b/pkg/kubeenv/roundtripper.go similarity index 77% rename from pkg/cli/roundtripper.go rename to pkg/kubeenv/roundtripper.go index 040ecd48f..e00f93984 100644 --- a/pkg/cli/roundtripper.go +++ b/pkg/kubeenv/roundtripper.go @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli +// Package kubeenv holds small, cycle-free Kubernetes client helpers shared by +// higher-level packages (for example pkg/cli and pkg/kube). +package kubeenv import ( "bytes" @@ -24,21 +26,21 @@ import ( "strings" ) -// retryingRoundTripper retries selected transient Kubernetes API server failures. -// Keeping this in pkg/cli avoids importing pkg/kube from EnvSettings. -type retryingRoundTripper struct { - wrapped http.RoundTripper +// RetryingRoundTripper retries transient Kubernetes API server errors on a +// wrapped [http.RoundTripper]. +type RetryingRoundTripper struct { + Wrapped http.RoundTripper } -func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +func (rt *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return rt.roundTrip(req, 1, nil) } -func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { +func (rt *RetryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { if retry < 0 { return prevResp, nil } - resp, rtErr := rt.wrapped.RoundTrip(req) + resp, rtErr := rt.Wrapped.RoundTrip(req) if rtErr != nil { return resp, rtErr } diff --git a/pkg/kube/roundtripper_test.go b/pkg/kubeenv/roundtripper_test.go similarity index 99% rename from pkg/kube/roundtripper_test.go rename to pkg/kubeenv/roundtripper_test.go index 96602c1f4..b921eac82 100644 --- a/pkg/kube/roundtripper_test.go +++ b/pkg/kubeenv/roundtripper_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube +package kubeenv import ( "encoding/json" From c26be60d81e5cb6a147d6088477cf86fd5aaf1f0 Mon Sep 17 00:00:00 2001 From: Cairon Date: Fri, 10 Apr 2026 11:11:38 -0400 Subject: [PATCH 021/132] fix: add debug logging to HTTP getter for helm pull When running 'helm pull --debug', no debug output was printed because the HTTP getter did not emit any slog.Debug messages. This adds slog.Debug calls to log the URL being fetched and the response status when debug-level logging is enabled. Fixes helm/helm#31098 Signed-off-by: Cairon --- pkg/getter/httpgetter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 2bc12bdbf..2eb2d5d8c 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "fmt" "io" + "log/slog" "net/http" "net/url" "sync" @@ -87,11 +88,13 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, err } + slog.Debug("fetching", "url", href) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() + slog.Debug("fetch complete", "url", href, "status", resp.Status, "content-length", resp.ContentLength) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } From 722dd3e3a82b06fbda60ebf24edc653e7b74e3f9 Mon Sep 17 00:00:00 2001 From: Jorge Rocamora <33847633+aeroyorch@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:03:12 +0200 Subject: [PATCH 022/132] Add duration functions Signed-off-by: Jorge Rocamora <33847633+aeroyorch@users.noreply.github.com> --- pkg/engine/funcs.go | 226 +++++++++++++++++++++++++++++++++++ pkg/engine/funcs_test.go | 247 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 473 insertions(+) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e03c13b38..431f82f63 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -19,9 +19,15 @@ package engine import ( "bytes" "encoding/json" + "errors" + "fmt" "maps" + "math" + "reflect" + "strconv" "strings" "text/template" + "time" "github.com/BurntSushi/toml" "github.com/Masterminds/sprig/v3" @@ -62,6 +68,19 @@ func funcMap() template.FuncMap { "fromJson": fromJSON, "fromJsonArray": fromJSONArray, + // Duration helpers + "mustToDuration": mustToDuration, + "durationSeconds": durationSeconds, + "durationMilliseconds": durationMilliseconds, + "durationMicroseconds": durationMicroseconds, + "durationNanoseconds": durationNanoseconds, + "durationMinutes": durationMinutes, + "durationHours": durationHours, + "durationDays": durationDays, + "durationWeeks": durationWeeks, + "durationRoundTo": durationRoundTo, + "durationTruncateTo": durationTruncateTo, + // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the // integrity of the linter. @@ -249,3 +268,210 @@ func fromJSONArray(str string) []any { } return a } + +// ----------------------------------------------------------------------------- +// Duration helpers (numeric and time.Duration returns) +// ----------------------------------------------------------------------------- + +const ( + maxDurationSeconds = int64(math.MaxInt64 / int64(time.Second)) + minDurationSeconds = int64(math.MinInt64 / int64(time.Second)) + maxDurationSecondsFloat = float64(math.MaxInt64) / float64(time.Second) + minDurationSecondsFloat = float64(math.MinInt64) / float64(time.Second) +) + +func durationFromSecondsInt(seconds int64) (time.Duration, error) { + if seconds > maxDurationSeconds || seconds < minDurationSeconds { + return 0, fmt.Errorf("duration seconds overflow: %d", seconds) + } + return time.Duration(seconds) * time.Second, nil +} + +func durationFromSecondsUint(seconds uint64) (time.Duration, error) { + if seconds > uint64(maxDurationSeconds) { + return 0, fmt.Errorf("duration seconds overflow: %d", seconds) + } + return time.Duration(int64(seconds)) * time.Second, nil +} + +func durationFromSecondsFloat(seconds float64) (time.Duration, error) { + if math.IsNaN(seconds) || math.IsInf(seconds, 0) { + return 0, fmt.Errorf("invalid duration seconds: %v", seconds) + } + if seconds > maxDurationSecondsFloat || seconds < minDurationSecondsFloat { + return 0, fmt.Errorf("duration seconds overflow: %v", seconds) + } + nanos := seconds * float64(time.Second) + if nanos > float64(math.MaxInt64) || nanos < float64(math.MinInt64) { + return 0, fmt.Errorf("duration nanoseconds overflow: %v", nanos) + } + return time.Duration(nanos), nil +} + +// asDuration converts common template values into a time.Duration. +// +// Supported inputs: +// - time.Duration +// - string duration values parsed by time.ParseDuration (e.g. "1h2m3s") +// - numeric strings treated as seconds (e.g. "2.5") +// - ints and uints treated as seconds +// - floats treated as seconds +func asDuration(v any) (time.Duration, error) { + switch x := v.(type) { + case time.Duration: + return x, nil + + case string: + s := strings.TrimSpace(x) + if s == "" { + return 0, errors.New("empty duration") + } + if d, err := time.ParseDuration(s); err == nil { + return d, nil + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + return durationFromSecondsFloat(f) + } + return 0, fmt.Errorf("could not parse duration %q", x) + + case nil: + return 0, errors.New("invalid duration") + } + + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return durationFromSecondsInt(rv.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return durationFromSecondsUint(rv.Uint()) + case reflect.Float32, reflect.Float64: + return durationFromSecondsFloat(rv.Float()) + default: + return 0, fmt.Errorf("unsupported duration type %T", v) + } +} + +// mustToDuration takes anything and attempts to parse as a duration returning a time.Duration. +// +// This is designed to be called from a template when need to ensure that a +// duration is valid. +func mustToDuration(v any) time.Duration { + d, err := asDuration(v) + if err != nil { + panic(err) + } + return d +} + +// durationSeconds converts a duration to seconds (float64). +// On error it returns 0. +func durationSeconds(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Seconds() +} + +// durationMilliseconds converts a duration to milliseconds (int64). +// On error it returns 0. +func durationMilliseconds(v any) int64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Milliseconds() +} + +// durationMicroseconds converts a duration to microseconds (int64). +// On error it returns 0. +func durationMicroseconds(v any) int64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Microseconds() +} + +// durationNanoseconds converts a duration to nanoseconds (int64). +// On error it returns 0. +func durationNanoseconds(v any) int64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Nanoseconds() +} + +// durationMinutes converts a duration to minutes (float64). +// On error it returns 0. +func durationMinutes(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Minutes() +} + +// durationHours converts a duration to hours (float64). +// On error it returns 0. +func durationHours(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Hours() +} + +// durationDays converts a duration to days (float64). (Not in Go's stdlib; handy in templates.) +// On error it returns 0. +func durationDays(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Hours() / 24.0 +} + +// durationWeeks converts a duration to weeks (float64). (Not in Go's stdlib; handy in templates.) +// On error it returns 0. +func durationWeeks(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Hours() / 24.0 / 7.0 +} + +// durationRoundTo rounds v to the nearest multiple of m. +// Returns a time.Duration. +// +// v and m accept the same forms as asDuration (e.g. "2h13m", "30s"). +// On error, it returns time.Duration(0). If m is invalid, it returns v. +func durationRoundTo(v any, m any) time.Duration { + d, err := asDuration(v) + if err != nil { + return 0 + } + mul, err := asDuration(m) + if err != nil { + return d + } + return d.Round(mul) +} + +// durationTruncateTo truncates v toward zero to a multiple of m. +// Returns a time.Duration. +// +// On error, it returns time.Duration(0). If m is invalid, it returns v. +func durationTruncateTo(v any, m any) time.Duration { + d, err := asDuration(v) + if err != nil { + return 0 + } + mul, err := asDuration(m) + if err != nil { + return d + } + return d.Truncate(mul) +} diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index be9d0153f..cf6a8d5c9 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -17,11 +17,14 @@ limitations under the License. package engine import ( + "math" "strings" "testing" "text/template" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFuncs(t *testing.T) { @@ -151,6 +154,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToDuration 30 }}`, + expect: `30s`, + vars: nil, + }, { + tpl: `{{ mustToDuration "1m30s" }}`, + expect: `1m30s`, + vars: nil, + }, { + tpl: `{{ mustToDuration "foo" }}`, + vars: nil, }, { tpl: `{{ toYaml . }}`, expect: "", // should return empty string and swallow error @@ -181,6 +195,239 @@ keyInElement1 = "valueInElement1"`, } } +func TestDurationHelpers(t *testing.T) { + tests := []struct { + name string + tpl string + vars any + expect string + }{{ + name: "durationSeconds parses duration string", + tpl: `{{ durationSeconds "1m30s" }}`, + expect: `90`, + }, { + name: "durationSeconds parses numeric string as seconds", + tpl: `{{ durationSeconds "2.5" }}`, + expect: `2.5`, + }, { + name: "durationSeconds trims whitespace around numeric string", + tpl: `{{ durationSeconds " 2.5 " }}`, + expect: `2.5`, + }, { + name: "durationSeconds int treated as seconds", + tpl: `{{ durationSeconds 2 }}`, + expect: `2`, + }, { + name: "durationSeconds float treated as seconds", + tpl: `{{ durationSeconds 2.5 }}`, + expect: `2.5`, + }, { + name: "durationSeconds uint treated as seconds", + tpl: `{{ durationSeconds . }}`, + vars: uint(2), + expect: `2`, + }, { + name: "durationSeconds time.Duration passthrough", + tpl: `{{ durationSeconds . }}`, + vars: 1500 * time.Millisecond, + expect: `1.5`, + }, { + name: "invalid duration string returns 0", + tpl: `{{ durationSeconds "nope" }}`, + expect: `0`, + }, { + name: "empty duration string returns 0", + tpl: `{{ durationSeconds "" }}`, + expect: `0`, + }, { + name: "whitespace-only duration string returns 0", + tpl: `{{ durationSeconds " " }}`, + expect: `0`, + }, { + name: "nil returns 0", + tpl: `{{ durationSeconds . }}`, + vars: nil, + expect: `0`, + }, { + name: "durationSeconds uint overflow returns 0", + tpl: `{{ durationSeconds . }}`, + vars: uint64(math.MaxInt64) + 1, + expect: `0`, + }, { + name: "durationSeconds int overflow returns 0", + tpl: `{{ durationSeconds . }}`, + vars: maxDurationSeconds + 1, + expect: `0`, + }, { + name: "durationSeconds int underflow returns 0", + tpl: `{{ durationSeconds . }}`, + vars: minDurationSeconds - 1, + expect: `0`, + }, { + name: "durationSeconds float overflow returns 0", + tpl: `{{ durationSeconds . }}`, + vars: maxDurationSecondsFloat + 0.5, + expect: `0`, + }, { + name: "durationSeconds float underflow returns 0", + tpl: `{{ durationSeconds . }}`, + vars: minDurationSecondsFloat - 0.5, + expect: `0`, + }, { + name: "durationSeconds NaN returns 0", + tpl: `{{ durationSeconds . }}`, + vars: math.NaN(), + expect: `0`, + }, { + name: "durationSeconds Inf returns 0", + tpl: `{{ durationSeconds . }}`, + vars: math.Inf(1), + expect: `0`, + }, { + name: "durationMilliseconds int seconds", + tpl: `{{ durationMilliseconds 2 }}`, + expect: `2000`, + }, { + name: "durationMilliseconds float seconds", + tpl: `{{ durationMilliseconds 1.5 }}`, + expect: `1500`, + }, { + name: "durationMicroseconds int seconds", + tpl: `{{ durationMicroseconds 2 }}`, + expect: `2000000`, + }, { + name: "durationNanoseconds int seconds", + tpl: `{{ durationNanoseconds 2 }}`, + expect: `2000000000`, + }, { + name: "durationMinutes parses duration string", + tpl: `{{ durationMinutes "90s" }}`, + expect: `1.5`, + }, { + name: "durationHours parses duration string", + tpl: `{{ durationHours "90m" }}`, + expect: `1.5`, + }, { + name: "durationDays parses duration string", + tpl: `{{ durationDays "36h" }}`, + expect: `1.5`, + }, { + name: "durationDays numeric seconds", + tpl: `{{ durationDays 86400 }}`, + expect: `1`, + }, { + name: "durationWeeks parses duration string", + tpl: `{{ durationWeeks "168h" }}`, + expect: `1`, + }, { + name: "durationWeeks parses fractional weeks", + tpl: `{{ durationWeeks "252h" }}`, + expect: `1.5`, + }, { + name: "durationRoundTo numeric seconds", + tpl: `{{ durationRoundTo 93 60 }}`, // 93s rounded to 60s = 120s + expect: `2m0s`, + }, { + name: "durationTruncateTo numeric seconds", + tpl: `{{ durationTruncateTo 93 60 }}`, // 93s truncated to 60s = 60s + expect: `1m0s`, + }, { + name: "durationRoundTo accepts duration-string multiplier", + tpl: `{{ durationRoundTo "93s" "1m" }}`, + expect: `2m0s`, + }, { + name: "durationTruncateTo accepts duration-string multiplier", + tpl: `{{ durationTruncateTo "93s" "1m" }}`, + expect: `1m0s`, + }, { + name: "durationRoundTo invalid m returns v unchanged", + tpl: `{{ durationRoundTo "93s" "nope" }}`, + expect: `1m33s`, + }, { + name: "durationTruncateTo invalid m returns v unchanged", + tpl: `{{ durationTruncateTo "93s" "nope" }}`, + expect: `1m33s`, + }, { + name: "durationRoundTo zero m returns v unchanged", + tpl: `{{ durationRoundTo "93s" 0 }}`, + expect: `1m33s`, + }, { + name: "durationTruncateTo negative m returns v unchanged", + tpl: `{{ durationTruncateTo "93s" -1 }}`, + expect: `1m33s`, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars) + require.NoError(t, err, tt.tpl) + assert.Equal(t, tt.expect, b.String(), tt.tpl) + }) + } + + mustErrTests := []struct { + name string + tpl string + vars any + }{{ + name: "mustToDuration invalid string", + tpl: `{{ mustToDuration "nope" }}`, + }, { + name: "mustToDuration empty string", + tpl: `{{ mustToDuration "" }}`, + }, { + name: "mustToDuration whitespace string", + tpl: `{{ mustToDuration " " }}`, + }, { + name: "mustToDuration unsupported type", + tpl: `{{ mustToDuration . }}`, + vars: []int{1, 2, 3}, + }, { + name: "mustToDuration uint overflow", + tpl: `{{ mustToDuration . }}`, + vars: uint64(math.MaxInt64) + 1, + }, { + name: "mustToDuration int overflow", + tpl: `{{ mustToDuration . }}`, + vars: maxDurationSeconds + 1, + }, { + name: "mustToDuration int underflow", + tpl: `{{ mustToDuration . }}`, + vars: minDurationSeconds - 1, + }, { + name: "mustToDuration float overflow", + tpl: `{{ mustToDuration . }}`, + vars: maxDurationSecondsFloat + 0.5, + }, { + name: "mustToDuration float underflow", + tpl: `{{ mustToDuration . }}`, + vars: minDurationSecondsFloat - 0.5, + }, { + name: "mustToDuration NaN", + tpl: `{{ mustToDuration . }}`, + vars: math.NaN(), + }, { + name: "mustToDuration Inf", + tpl: `{{ mustToDuration . }}`, + vars: math.Inf(-1), + }, + } + + for _, tt := range mustErrTests { + t.Run(tt.name, func(t *testing.T) { + var b strings.Builder + tmpl := template.Must( + template.New("test"). + Funcs(funcMap()). + Parse(tt.tpl), + ) + err := tmpl.Execute(&b, tt.vars) + require.Error(t, err, tt.tpl) + }) + } +} + // This test to check a function provided by sprig is due to a change in a // dependency of sprig. mergo in v0.3.9 changed the way it merges and only does // public fields (i.e. those starting with a capital letter). This test, from From 5cb4e7d992d85d372f5d86c238330102d936bfe5 Mon Sep 17 00:00:00 2001 From: Johannes Lohmer Date: Sat, 28 Mar 2026 10:23:17 +0000 Subject: [PATCH 023/132] test(values): add tests for subchart nil value regressions Three test cases that cover the regression scenarios introduced by the #31644 nil preservation fix: - subchart default nils should be cleaned up when parent doesn't set those keys (#31919) - user-supplied null should erase subchart defaults (#31919) - subchart default nil should not shadow global values via pluck (#31971) Tests are expected to fail until the regression is fixed. Signed-off-by: Johannes Lohmer --- .gitignore | 1 + pkg/chart/common/util/coalesce_test.go | 118 +++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/.gitignore b/.gitignore index 0fd2c6bda..2209e9809 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .DS_Store .coverage/ .idea +.claude .vimrc .vscode/ .devcontainer/ diff --git a/pkg/chart/common/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go index 1d0baa84d..f7bf541a2 100644 --- a/pkg/chart/common/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -765,3 +765,121 @@ func TestCoalesceValuesEmptyMapWithNils(t *testing.T) { is.True(ok, "Expected data.baz key to be present but it was removed") is.Nil(data["baz"], "Expected data.baz key to be nil but it is not") } + +// TestCoalesceValuesSubchartDefaultNilsCleaned tests that nil values in subchart defaults +// are cleaned up during coalescing when the parent doesn't set those keys. +// Regression test for issue #31919. +func TestCoalesceValuesSubchartDefaultNilsCleaned(t *testing.T) { + is := assert.New(t) + + // Subchart has a default with nil values (e.g. keyMapping: {password: null}) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Values: map[string]any{ + "keyMapping": map[string]any{ + "password": nil, + }, + }, + } + + parent := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Values: map[string]any{}, + }, subchart) + + // Parent user values don't mention keyMapping at all + vals := map[string]any{} + + v, err := CoalesceValues(parent, vals) + is.NoError(err) + + childVals, ok := v["child"].(map[string]any) + is.True(ok, "child values should be a map") + + keyMapping, ok := childVals["keyMapping"].(map[string]any) + is.True(ok, "keyMapping should be a map") + + // The nil "password" key from chart defaults should be cleaned up + _, ok = keyMapping["password"] + is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed, but it is still present") +} + +// TestCoalesceValuesUserNullErasesSubchartDefault tests that a user-supplied null +// value erases a subchart's default value during coalescing. +// Regression test for issue #31919. +func TestCoalesceValuesUserNullErasesSubchartDefault(t *testing.T) { + is := assert.New(t) + + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Values: map[string]any{ + "someKey": "default", + }, + } + + parent := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Values: map[string]any{}, + }, subchart) + + // User explicitly nullifies the subchart key via parent values + vals := map[string]any{ + "child": map[string]any{ + "someKey": nil, + }, + } + + v, err := CoalesceValues(parent, vals) + is.NoError(err) + + childVals, ok := v["child"].(map[string]any) + is.True(ok, "child values should be a map") + + // someKey should be erased — user null overrides subchart default + _, ok = childVals["someKey"] + is.False(ok, "Expected someKey to be removed by user null override, but it is still present") +} + +// TestCoalesceValuesSubchartNilDoesNotShadowGlobal tests that a nil value in +// subchart defaults doesn't shadow a global value accessible via pluck-like access. +// Regression test for issue #31971. +func TestCoalesceValuesSubchartNilDoesNotShadowGlobal(t *testing.T) { + is := assert.New(t) + + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Values: map[string]any{ + "ingress": map[string]any{ + "feature": nil, // nil in subchart defaults + }, + }, + } + + parent := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Values: map[string]any{}, + }, subchart) + + // Parent sets the global value + vals := map[string]any{ + "global": map[string]any{ + "ingress": map[string]any{ + "feature": true, + }, + }, + } + + v, err := CoalesceValues(parent, vals) + is.NoError(err) + + childVals, ok := v["child"].(map[string]any) + is.True(ok, "child values should be a map") + + ingress, ok := childVals["ingress"].(map[string]any) + is.True(ok, "ingress should be a map") + + // The nil "feature" from subchart defaults should be cleaned up, + // so that pluck can fall through to the global value + _, ok = ingress["feature"] + is.False(ok, "Expected ingress.feature (nil from chart defaults) to be removed so global can be used via pluck, but it is still present") +} From 6eb4ebf0e1afb0c63d748bf116145a5b9e0842b7 Mon Sep 17 00:00:00 2001 From: Johannes Lohmer Date: Sat, 28 Mar 2026 10:23:21 +0000 Subject: [PATCH 024/132] test(values): add test for subchart nil producing %!s() Regression test for the Bitnami common.secrets.key issue. Signed-off-by: Johannes Lohmer --- pkg/engine/engine_test.go | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index c674a11ec..869b5d202 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1505,3 +1505,65 @@ func TestTraceableError_NoTemplateForm(t *testing.T) { } } } + +// TestRenderSubchartDefaultNilNoStringify tests the full pipeline: subchart default +// nil values should not produce "%!s()" in rendered template output. +// Regression test for the Bitnami common.secrets.key issue. +func TestRenderSubchartDefaultNilNoStringify(t *testing.T) { + modTime := time.Now() + + // Subchart has a default with nil values + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Templates: []*common.File{ + { + Name: "templates/test.yaml", + ModTime: modTime, + Data: []byte(`{{- if hasKey .Values.keyMapping "password" -}}{{- printf "subPath: %s" (index .Values.keyMapping "password") -}}{{- else -}}subPath: fallback{{- end -}}`), + }, + }, + Values: map[string]any{ + "keyMapping": map[string]any{ + "password": nil, // nil in chart defaults + }, + }, + } + + parent := &chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Values: map[string]any{}, + } + parent.AddDependency(subchart) + + // Parent user values don't set keyMapping + injValues := map[string]any{} + + tmp, err := util.CoalesceValues(parent, injValues) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + inject := common.Values{ + "Values": tmp, + "Chart": parent.Metadata, + "Release": common.Values{ + "Name": "test-release", + }, + } + + out, err := Render(parent, inject) + if err != nil { + t.Fatalf("Failed to render templates: %s", err) + } + + rendered := out["parent/charts/child/templates/test.yaml"] + + if strings.Contains(rendered, "%!s()") { + t.Errorf("Rendered output contains %%!s(), got: %q", rendered) + } + + expected := "subPath: fallback" + if rendered != expected { + t.Errorf("Expected %q, got %q", expected, rendered) + } +} From 00638773d1366dc962c785de3d297cf0279b9a0d Mon Sep 17 00:00:00 2001 From: Johannes Lohmer Date: Sat, 28 Mar 2026 20:59:31 +0100 Subject: [PATCH 025/132] fix(values): do not copy chart-default nils into coalesced values Only user-supplied nils should survive coalescing. Chart-default nils defaults, not just user overrides. This caused: - %!s() in templates using Bitnami common.secrets.key (#31919) - pluck fallbacks returning nil instead of falling through to globals (#31971) Fixes #31919 Fixes #31971 Signed-off-by: Johannes Lohmer --- pkg/chart/common/util/coalesce.go | 29 +++++++++++++++++++++++++- pkg/cmd/testdata/output/issue-9027.txt | 10 +++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/pkg/chart/common/util/coalesce.go b/pkg/chart/common/util/coalesce.go index 5994febbc..90e6640e9 100644 --- a/pkg/chart/common/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -251,6 +251,12 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st // If the key is a child chart, coalesce tables with Merge set to true merge := childChartMergeTrue(c, key, merge) + // When coalescing, clean nils from chart defaults before merging + // so they don't leak into the result. + if !merge { + cleanNilValues(src) + } + // Because v has higher precedence than nv, dest values override src // values. coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) @@ -258,6 +264,16 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st } } else { // If the key is not in v, copy it from nv. + // When coalescing, skip chart default nils and clean nils from + // nested maps so they don't shadow globals or produce %!s(). + if !merge { + if val == nil { + continue + } + if sub, ok := val.(map[string]any); ok { + cleanNilValues(sub) + } + } v[key] = val } } @@ -326,7 +342,6 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin // But if src also has nil (or key not in src), preserve the nil delete(dst, key) } else if !ok { - // key not in user values, preserve src value (including nil) dst[key] = val } else if istable(val) { if istable(dv) { @@ -341,6 +356,18 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin return dst } +// cleanNilValues recursively removes nil entries from a map so that chart +// default nils don't leak into the coalesced result. +func cleanNilValues(m map[string]any) { + for key, val := range m { + if val == nil { + delete(m, key) + } else if sub, ok := val.(map[string]any); ok { + cleanNilValues(sub) + } + } +} + // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. func istable(v any) bool { _, ok := v.(map[string]any) diff --git a/pkg/cmd/testdata/output/issue-9027.txt b/pkg/cmd/testdata/output/issue-9027.txt index eb19fc383..1227336e4 100644 --- a/pkg/cmd/testdata/output/issue-9027.txt +++ b/pkg/cmd/testdata/output/issue-9027.txt @@ -2,11 +2,15 @@ # Source: issue-9027/charts/subchart/templates/values.yaml global: hash: + key1: 1 + key2: 2 key3: 13 key4: 4 key5: 5 key6: 6 hash: + key1: 1 + key2: 2 key3: 13 key4: 4 key5: 5 @@ -15,17 +19,19 @@ hash: # Source: issue-9027/templates/values.yaml global: hash: - key1: null - key2: null key3: 13 subchart: global: hash: + key1: 1 + key2: 2 key3: 13 key4: 4 key5: 5 key6: 6 hash: + key1: 1 + key2: 2 key3: 13 key4: 4 key5: 5 From 52fc971da37cf34aa26e7d7c460f2430dfb01b26 Mon Sep 17 00:00:00 2001 From: Johannes Lohmer Date: Mon, 30 Mar 2026 10:15:12 +0200 Subject: [PATCH 026/132] test(values): Add test for nil cleanup in partially overridden subchart maps Signed-off-by: Johannes Lohmer --- pkg/chart/common/util/coalesce_test.go | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pkg/chart/common/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go index f7bf541a2..252ef11ec 100644 --- a/pkg/chart/common/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -883,3 +883,48 @@ func TestCoalesceValuesSubchartNilDoesNotShadowGlobal(t *testing.T) { _, ok = ingress["feature"] is.False(ok, "Expected ingress.feature (nil from chart defaults) to be removed so global can be used via pluck, but it is still present") } + +// TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides tests that nil +// values in subchart defaults are cleaned even when the user partially overrides +// the same map. Regression test for the coalesceTablesFullKey merge path. +func TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides(t *testing.T) { + is := assert.New(t) + + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Values: map[string]any{ + "keyMapping": map[string]any{ + "password": nil, + "format": "bcrypt", + }, + }, + } + + parent := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Values: map[string]any{}, + }, subchart) + + // User overrides format but doesn't mention password + vals := map[string]any{ + "child": map[string]any{ + "keyMapping": map[string]any{ + "format": "sha256", + }, + }, + } + + v, err := CoalesceValues(parent, vals) + is.NoError(err) + + childVals, ok := v["child"].(map[string]any) + is.True(ok, "child values should be a map") + + keyMapping, ok := childVals["keyMapping"].(map[string]any) + is.True(ok, "keyMapping should be a map") + + is.Equal("sha256", keyMapping["format"], "User override should be preserved") + + _, ok = keyMapping["password"] + is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed even when user partially overrides the map") +} From a8e249714f5311b9aff44c4bd2bfc433ab1ab952 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 1 Apr 2026 19:25:09 +0100 Subject: [PATCH 027/132] Update pkg/chart/common/util/coalesce.go Signed-off-by: Evans Mungai --- pkg/chart/common/util/coalesce.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/chart/common/util/coalesce.go b/pkg/chart/common/util/coalesce.go index 90e6640e9..999eeb208 100644 --- a/pkg/chart/common/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -356,7 +356,7 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin return dst } -// cleanNilValues recursively removes nil entries from a map so that chart +// cleanNilValues recursively removes nil entries in-place from a map so that chart // default nils don't leak into the coalesced result. func cleanNilValues(m map[string]any) { for key, val := range m { From 48e2b7ddd4e960b768fe5daee34a33cb89852a6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:12:06 +0000 Subject: [PATCH 028/132] chore(deps): bump github.com/mattn/go-shellwords from 1.0.12 to 1.0.13 Bumps [github.com/mattn/go-shellwords](https://github.com/mattn/go-shellwords) from 1.0.12 to 1.0.13. - [Commits](https://github.com/mattn/go-shellwords/compare/v1.0.12...v1.0.13) --- updated-dependencies: - dependency-name: github.com/mattn/go-shellwords dependency-version: 1.0.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e5fcf6c02..1b1f9b4e3 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/gosuri/uitable v0.0.4 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.12.3 - github.com/mattn/go-shellwords v1.0.12 + github.com/mattn/go-shellwords v1.0.13 github.com/moby/term v0.5.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 diff --git a/go.sum b/go.sum index 51f77f70b..ab994625e 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= +github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= From 265c5eb530a36ec651e79ecf4d37ba2f098b7e59 Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Wed, 15 Apr 2026 14:33:27 +0100 Subject: [PATCH 029/132] fix(templating): SplitManifests must preserve line endings for downstream YAML parsers (#31952) * fix(templating): SplitManifests must preserve line endings for downstream YAML parsers Signed-off-by: Matheus Pimenta * Address copilot comment about skipping empty docs Signed-off-by: Matheus Pimenta * Port fix to release v2 Signed-off-by: Matheus Pimenta * Address copilot comments Signed-off-by: Matheus Pimenta --------- Signed-off-by: Matheus Pimenta --- internal/release/v2/util/manifest.go | 18 +- internal/release/v2/util/manifest_test.go | 480 ++++++++++++- pkg/action/action_test.go | 670 +++++++++++++++++- pkg/action/testdata/rbac.txt | 1 + .../install-dry-run-with-secret-hidden.txt | 1 + .../output/install-dry-run-with-secret.txt | 2 + pkg/cmd/testdata/output/issue-9027.txt | 1 + pkg/cmd/testdata/output/object-order.txt | 1 + .../output/template-name-template.txt | 7 + pkg/cmd/testdata/output/template-set.txt | 7 + .../testdata/output/template-skip-tests.txt | 5 + .../output/template-subchart-cm-set-file.txt | 7 + .../output/template-subchart-cm-set.txt | 7 + .../testdata/output/template-subchart-cm.txt | 7 + .../testdata/output/template-values-files.txt | 7 + .../output/template-with-api-version.txt | 7 + .../testdata/output/template-with-crds.txt | 7 + .../output/template-with-kube-version.txt | 7 + pkg/cmd/testdata/output/template.txt | 7 + pkg/release/v1/util/manifest.go | 9 +- pkg/release/v1/util/manifest_test.go | 480 ++++++++++++- 21 files changed, 1691 insertions(+), 47 deletions(-) diff --git a/internal/release/v2/util/manifest.go b/internal/release/v2/util/manifest.go index 20d097d9b..5dbcdaea5 100644 --- a/internal/release/v2/util/manifest.go +++ b/internal/release/v2/util/manifest.go @@ -21,6 +21,7 @@ import ( "regexp" "strconv" "strings" + "unicode" ) // SimpleHead defines what the structure of the head of a manifest file @@ -35,7 +36,16 @@ type SimpleHead struct { var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") -// SplitManifests takes a string of manifest and returns a map contains individual manifests +// SplitManifests takes a manifest string and returns a map containing individual manifests. +// +// **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_ +// Go templates to be defined inside charts across the years. The generated text from Go +// templates may contain `---apiVersion: v1`, and this function magically splits this back +// to `---\napiVersion: v1`. This has caused issues recently after Helm 4 introduced +// kio.ParseAll to inject annotations when post-renderers are used. In Chart API v3, +// we should kill this regex with fire (or change it) and expose charts doing the wrong +// thing Go template-wise. Helm should say a big _NO_ to charts doing the wrong thing, +// with or without post-renderers. func SplitManifests(bigFile string) map[string]string { // Basically, we're quickly splitting a stream of YAML documents into an // array of YAML docs. The file name is just a place holder, but should be @@ -44,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string { tpl := "manifest-%d" res := map[string]string{} // Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. - bigFileTmp := strings.TrimSpace(bigFile) + bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace) docs := sep.Split(bigFileTmp, -1) var count int for _, d := range docs { - if d == "" { + if strings.TrimSpace(d) == "" { continue } - d = strings.TrimSpace(d) + d = strings.TrimLeftFunc(d, unicode.IsSpace) res[fmt.Sprintf(tpl, count)] = d count = count + 1 } diff --git a/internal/release/v2/util/manifest_test.go b/internal/release/v2/util/manifest_test.go index 7fd332fbc..72b095390 100644 --- a/internal/release/v2/util/manifest_test.go +++ b/internal/release/v2/util/manifest_test.go @@ -21,7 +21,15 @@ import ( "testing" ) -const mockManifestFile = ` +func TestSplitManifests(t *testing.T) { + tests := []struct { + name string + input string + expected map[string]string + }{ + { + name: "single doc with leading separator and whitespace", + input: ` --- apiVersion: v1 @@ -35,9 +43,9 @@ spec: - name: nemo-test image: fake-image cmd: fake-command -` - -const expectedManifest = `apiVersion: v1 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 kind: Pod metadata: name: finding-nemo, @@ -47,15 +55,463 @@ spec: containers: - name: nemo-test image: fake-image - cmd: fake-command` + cmd: fake-command +`, + }, + }, + { + name: "empty input", + input: "", + expected: map[string]string{}, + }, + { + name: "whitespace only", + input: " \n\n \n", + expected: map[string]string{}, + }, + { + name: "whitespace-only doc after separator is skipped", + input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n", + expected: map[string]string{ + "manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1", + }, + }, + { + name: "single doc no separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + }, + }, + { + name: "two docs with proper separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + }, + }, -func TestSplitManifest(t *testing.T) { - manifests := SplitManifests(mockManifestFile) - if len(manifests) != 1 { - t.Errorf("Expected 1 manifest, got %v", len(manifests)) + // Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep) + // inputs with 0, 1, and 2 trailing newlines after the block content. + // Note: the emitter may normalize the output chomping indicator when the + // trailing newline count makes another indicator equivalent for the result. + + // | (clip) input — clips trailing newlines to exactly one, though with + // 0 trailing newlines the emitted output may normalize to |-. + { + name: "block scalar clip (|) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + }, + }, + { + name: "block scalar clip (|) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +`, + }, + }, + { + name: "block scalar clip (|) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +`, + }, + }, + + // |- (strip) + { + name: "block scalar strip (|-) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello`, + }, + }, + { + name: "block scalar strip (|-) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +`, + }, + }, + { + name: "block scalar strip (|-) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +`, + }, + }, + + // |+ (keep) + { + name: "block scalar keep (|+) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + }, + }, + { + name: "block scalar keep (|+) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +`, + }, + }, + { + name: "block scalar keep (|+) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +`, + }, + }, + + // Multi-doc with block scalars: the regex consumes \s*\n before ---, + // so trailing newlines from non-last docs are stripped. + { + name: "multi-doc block scalar clip (|) before separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + }, + }, + { + name: "multi-doc block scalar keep (|+) with 2 trailing newlines before separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + }, + }, + + // **Note for Chart API v3**: The following tests exercise the lenient + // regex that splits `---apiVersion` back into separate documents. + // In Chart API v3, these inputs should return an _ERROR_ instead. + // See the comment on the SplitManifests function for more details. + { + name: "leading glued separator (---apiVersion)", + input: ` +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +`, + }, + }, + { + name: "mid-content glued separator (---apiVersion)", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + }, + }, + { + name: "multiple glued separators", + input: ` +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2`, + "manifest-2": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + }, + }, + { + name: "mixed glued and proper separators", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2`, + "manifest-2": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + }, + }, } - expected := map[string]string{"manifest-0": expectedManifest} - if !reflect.DeepEqual(manifests, expected) { - t.Errorf("Expected %v, got %v", expected, manifests) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SplitManifests(tt.input) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) + } + }) } } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index a2b170206..d6575a791 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -418,7 +418,8 @@ func TestAnnotateAndMerge(t *testing.T) { { name: "single file with single manifest", files: map[string]string{ - "templates/configmap.yaml": `apiVersion: v1 + "templates/configmap.yaml": ` +apiVersion: v1 kind: ConfigMap metadata: name: test-cm @@ -438,13 +439,15 @@ data: { name: "multiple files with multiple manifests", files: map[string]string{ - "templates/configmap.yaml": `apiVersion: v1 + "templates/configmap.yaml": ` +apiVersion: v1 kind: ConfigMap metadata: name: test-cm data: key: value`, - "templates/secret.yaml": `apiVersion: v1 + "templates/secret.yaml": ` +apiVersion: v1 kind: Secret metadata: name: test-secret @@ -473,7 +476,8 @@ data: { name: "file with multiple manifests", files: map[string]string{ - "templates/multi.yaml": `apiVersion: v1 + "templates/multi.yaml": ` +apiVersion: v1 kind: ConfigMap metadata: name: test-cm1 @@ -509,7 +513,8 @@ data: { name: "partials and empty files are removed", files: map[string]string{ - "templates/cm.yaml": `apiVersion: v1 + "templates/cm.yaml": ` +apiVersion: v1 kind: ConfigMap metadata: name: test-cm1 @@ -531,14 +536,16 @@ metadata: { name: "empty file", files: map[string]string{ - "templates/empty.yaml": "", + "templates/empty.yaml": ` +`, }, expected: ``, }, { name: "invalid yaml", files: map[string]string{ - "templates/invalid.yaml": `invalid: yaml: content: + "templates/invalid.yaml": ` +invalid: yaml: content: - malformed`, }, expectedError: "parsing templates/invalid.yaml", @@ -546,7 +553,12 @@ metadata: { name: "leading doc separator glued to content by template whitespace trimming", files: map[string]string{ - "templates/service.yaml": "---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + "templates/service.yaml": ` +---apiVersion: v1 +kind: Service +metadata: + name: test-svc +`, }, expected: `apiVersion: v1 kind: Service @@ -559,7 +571,13 @@ metadata: { name: "leading doc separator on its own line", files: map[string]string{ - "templates/service.yaml": "---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + "templates/service.yaml": ` +--- +apiVersion: v1 +kind: Service +metadata: + name: test-svc +`, }, expected: `apiVersion: v1 kind: Service @@ -572,7 +590,14 @@ metadata: { name: "multiple leading doc separators", files: map[string]string{ - "templates/service.yaml": "---\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + "templates/service.yaml": ` +--- +--- +apiVersion: v1 +kind: Service +metadata: + name: test-svc +`, }, expected: `apiVersion: v1 kind: Service @@ -585,7 +610,16 @@ metadata: { name: "mid-content doc separator glued to content by template whitespace trimming", files: map[string]string{ - "templates/all.yaml": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-cm\n---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + "templates/all.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +---apiVersion: v1 +kind: Service +metadata: + name: test-svc +`, }, expected: `apiVersion: v1 kind: ConfigMap @@ -631,7 +665,7 @@ metadata: annotations: postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml' data: - ca.crt: |- + ca.crt: | ------BEGIN CERTIFICATE------ MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zAKBggqhkjOPQQDAzASMRAw DgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYwMDAw @@ -667,7 +701,7 @@ metadata: annotations: postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml' data: - config: |- + config: | # --------------------------------------------------------------------------- [section] key = value @@ -694,7 +728,7 @@ metadata: annotations: postrenderer.helm.sh/postrender-filename: 'templates/dashboard.yaml' data: - dashboard.json: |- + dashboard.json: | {"options":{"---------":{"color":"#292929","text":"N/A"}}} `, }, @@ -933,6 +967,605 @@ metadata: name: cm-12 annotations: postrenderer.helm.sh/postrender-filename: 'templates/many.yaml' +`, + }, + + // Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep) + // inputs with 0, 1, and 2 trailing newlines after the block content. + // Note: the emitter may normalize the output chomping indicator when the + // trailing newline count makes another indicator equivalent for the result. + + // | (clip) input — clips trailing newlines to exactly one, though with + // 0 trailing newlines the emitted output may normalize to |-. + { + name: "block scalar clip (|) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +`, + }, + { + name: "block scalar clip (|) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: | + hello +`, + }, + { + name: "block scalar clip (|) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: | + hello +`, + }, + + // |- (strip) — strips all trailing newlines + { + name: "block scalar strip (|-) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +`, + }, + { + name: "block scalar strip (|-) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +`, + }, + { + name: "block scalar strip (|-) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +`, + }, + + // |+ (keep) — preserves all trailing newlines + { + name: "block scalar keep (|+) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +`, + }, + { + name: "block scalar keep (|+) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: | + hello +`, + }, + { + name: "block scalar keep (|+) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |+ + hello + +`, + }, + + // Multi-doc tests: block scalar doc is NOT the last document. + // SplitManifests' regex consumes \s*\n before ---, so trailing + // newlines from non-last docs are always stripped. + + // | (clip) in multi-doc (first doc) + { + name: "multi-doc block scalar clip (|) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar clip (|) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar clip (|) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + + // |- (strip) in multi-doc (first doc) + { + name: "multi-doc block scalar strip (|-) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar strip (|-) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar strip (|-) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + + // |+ (keep) in multi-doc (first doc) + { + name: "multi-doc block scalar keep (|+) with 0 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar keep (|+) with 1 trailing newline", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple +`, + }, + { + name: "multi-doc block scalar keep (|+) with 2 trailing newlines", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +data: + val: simple`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + key: |- + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + val: simple `, }, } @@ -1201,12 +1834,15 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { expectedBuf := `--- # Source: yellow/templates/foodpie foodpie: world + --- # Source: yellow/templates/with-partials yellow: Earth + --- # Source: yellow/templates/yellow yellow: world + ` expectedHook := `kind: ConfigMap metadata: @@ -1214,7 +1850,8 @@ metadata: annotations: "helm.sh/hook": post-install,pre-delete,post-upgrade data: - name: value` + name: value +` assert.Equal(t, expectedBuf, buf.String()) assert.Len(t, hooks, 1) @@ -1319,14 +1956,17 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { # Source: hello/templates/goodbye goodbye: world color: blue + --- # Source: hello/templates/hello hello: world color: blue + --- # Source: hello/templates/with-partials hello: Earth color: blue + ` assert.Contains(t, output, "color: blue") assert.Equal(t, 3, strings.Count(output, "color: blue")) diff --git a/pkg/action/testdata/rbac.txt b/pkg/action/testdata/rbac.txt index 0cb15b868..91938d5cc 100644 --- a/pkg/action/testdata/rbac.txt +++ b/pkg/action/testdata/rbac.txt @@ -23,3 +23,4 @@ subjects: - kind: ServiceAccount name: schedule-agents namespace: spaced + diff --git a/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt b/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt index eb770967f..c2219d8c4 100644 --- a/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt +++ b/pkg/cmd/testdata/output/install-dry-run-with-secret-hidden.txt @@ -19,3 +19,4 @@ metadata: data: foo: bar + diff --git a/pkg/cmd/testdata/output/install-dry-run-with-secret.txt b/pkg/cmd/testdata/output/install-dry-run-with-secret.txt index d22c1437f..62bd78018 100644 --- a/pkg/cmd/testdata/output/install-dry-run-with-secret.txt +++ b/pkg/cmd/testdata/output/install-dry-run-with-secret.txt @@ -15,6 +15,7 @@ metadata: name: test-secret stringData: foo: bar + --- # Source: chart-with-secret/templates/configmap.yaml apiVersion: v1 @@ -24,3 +25,4 @@ metadata: data: foo: bar + diff --git a/pkg/cmd/testdata/output/issue-9027.txt b/pkg/cmd/testdata/output/issue-9027.txt index 1227336e4..f43032499 100644 --- a/pkg/cmd/testdata/output/issue-9027.txt +++ b/pkg/cmd/testdata/output/issue-9027.txt @@ -15,6 +15,7 @@ hash: key4: 4 key5: 5 key6: 6 + --- # Source: issue-9027/templates/values.yaml global: diff --git a/pkg/cmd/testdata/output/object-order.txt b/pkg/cmd/testdata/output/object-order.txt index 307f928f2..1ff39f33c 100644 --- a/pkg/cmd/testdata/output/object-order.txt +++ b/pkg/cmd/testdata/output/object-order.txt @@ -155,6 +155,7 @@ spec: policyTypes: - Egress - Ingress + --- # Source: object-order/templates/01-a.yml # 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order) diff --git a/pkg/cmd/testdata/output/template-name-template.txt b/pkg/cmd/testdata/output/template-name-template.txt index 9406048dd..b1077012e 100644 --- a/pkg/cmd/testdata/output/template-name-template.txt +++ b/pkg/cmd/testdata/output/template-name-template.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -93,6 +98,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -112,3 +118,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-set.txt b/pkg/cmd/testdata/output/template-set.txt index 4040991cf..1ecb8707b 100644 --- a/pkg/cmd/testdata/output/template-set.txt +++ b/pkg/cmd/testdata/output/template-set.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -93,6 +98,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -112,3 +118,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-skip-tests.txt b/pkg/cmd/testdata/output/template-skip-tests.txt index 5c907b563..4c5af8df3 100644 --- a/pkg/cmd/testdata/output/template-skip-tests.txt +++ b/pkg/cmd/testdata/output/template-skip-tests.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt index 56844e292..227d05903 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set-file.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -22,6 +23,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -36,6 +38,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -53,6 +56,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -70,6 +74,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -101,6 +106,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -120,3 +126,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-subchart-cm-set.txt b/pkg/cmd/testdata/output/template-subchart-cm-set.txt index e52f7c234..dd8be4db9 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm-set.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm-set.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -22,6 +23,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -36,6 +38,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -53,6 +56,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -70,6 +74,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -101,6 +106,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -120,3 +126,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-subchart-cm.txt b/pkg/cmd/testdata/output/template-subchart-cm.txt index 9cc9e2296..c4600a798 100644 --- a/pkg/cmd/testdata/output/template-subchart-cm.txt +++ b/pkg/cmd/testdata/output/template-subchart-cm.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/configmap.yaml apiVersion: v1 @@ -22,6 +23,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -36,6 +38,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -53,6 +56,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -70,6 +74,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -101,6 +106,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -120,3 +126,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-values-files.txt b/pkg/cmd/testdata/output/template-values-files.txt index 4040991cf..1ecb8707b 100644 --- a/pkg/cmd/testdata/output/template-values-files.txt +++ b/pkg/cmd/testdata/output/template-values-files.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -93,6 +98,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -112,3 +118,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-with-api-version.txt b/pkg/cmd/testdata/output/template-with-api-version.txt index 8b6074cdb..ae726e624 100644 --- a/pkg/cmd/testdata/output/template-with-api-version.txt +++ b/pkg/cmd/testdata/output/template-with-api-version.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -95,6 +100,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -114,3 +120,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-with-crds.txt b/pkg/cmd/testdata/output/template-with-crds.txt index 256fc7c3b..1d63265ec 100644 --- a/pkg/cmd/testdata/output/template-with-crds.txt +++ b/pkg/cmd/testdata/output/template-with-crds.txt @@ -21,6 +21,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -31,6 +32,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -45,6 +47,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -62,6 +65,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -79,6 +83,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -110,6 +115,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -129,3 +135,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template-with-kube-version.txt b/pkg/cmd/testdata/output/template-with-kube-version.txt index 9d326f328..2c42e2e84 100644 --- a/pkg/cmd/testdata/output/template-with-kube-version.txt +++ b/pkg/cmd/testdata/output/template-with-kube-version.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -93,6 +98,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -112,3 +118,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/cmd/testdata/output/template.txt b/pkg/cmd/testdata/output/template.txt index 58c480b47..ddbfebe9d 100644 --- a/pkg/cmd/testdata/output/template.txt +++ b/pkg/cmd/testdata/output/template.txt @@ -4,6 +4,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: subchart-sa + --- # Source: subchart/templates/subdir/role.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -14,6 +15,7 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get","list","watch"] + --- # Source: subchart/templates/subdir/rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 @@ -28,6 +30,7 @@ subjects: - kind: ServiceAccount name: subchart-sa namespace: default + --- # Source: subchart/charts/subcharta/templates/service.yaml apiVersion: v1 @@ -45,6 +48,7 @@ spec: name: apache selector: app.kubernetes.io/name: subcharta + --- # Source: subchart/charts/subchartb/templates/service.yaml apiVersion: v1 @@ -62,6 +66,7 @@ spec: name: nginx selector: app.kubernetes.io/name: subchartb + --- # Source: subchart/templates/service.yaml apiVersion: v1 @@ -93,6 +98,7 @@ metadata: "helm.sh/hook": test data: message: Hello World + --- # Source: subchart/templates/tests/test-nothing.yaml apiVersion: v1 @@ -112,3 +118,4 @@ spec: - echo - "$message" restartPolicy: Never + diff --git a/pkg/release/v1/util/manifest.go b/pkg/release/v1/util/manifest.go index 3160599bc..fa26f6256 100644 --- a/pkg/release/v1/util/manifest.go +++ b/pkg/release/v1/util/manifest.go @@ -21,6 +21,7 @@ import ( "regexp" "strconv" "strings" + "unicode" ) // SimpleHead defines what the structure of the head of a manifest file @@ -35,7 +36,7 @@ type SimpleHead struct { var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") -// SplitManifests takes a string of manifest and returns a map contains individual manifests +// SplitManifests takes a manifest string and returns a map containing individual manifests. // // **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_ // Go templates to be defined inside charts across the years. The generated text from Go @@ -53,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string { tpl := "manifest-%d" res := map[string]string{} // Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. - bigFileTmp := strings.TrimSpace(bigFile) + bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace) docs := sep.Split(bigFileTmp, -1) var count int for _, d := range docs { - if d == "" { + if strings.TrimSpace(d) == "" { continue } - d = strings.TrimSpace(d) + d = strings.TrimLeftFunc(d, unicode.IsSpace) res[fmt.Sprintf(tpl, count)] = d count = count + 1 } diff --git a/pkg/release/v1/util/manifest_test.go b/pkg/release/v1/util/manifest_test.go index 754ac1367..516ac42d7 100644 --- a/pkg/release/v1/util/manifest_test.go +++ b/pkg/release/v1/util/manifest_test.go @@ -21,7 +21,15 @@ import ( "testing" ) -const mockManifestFile = ` +func TestSplitManifests(t *testing.T) { + tests := []struct { + name string + input string + expected map[string]string + }{ + { + name: "single doc with leading separator and whitespace", + input: ` --- apiVersion: v1 @@ -35,9 +43,9 @@ spec: - name: nemo-test image: fake-image cmd: fake-command -` - -const expectedManifest = `apiVersion: v1 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 kind: Pod metadata: name: finding-nemo, @@ -47,15 +55,463 @@ spec: containers: - name: nemo-test image: fake-image - cmd: fake-command` + cmd: fake-command +`, + }, + }, + { + name: "empty input", + input: "", + expected: map[string]string{}, + }, + { + name: "whitespace only", + input: " \n\n \n", + expected: map[string]string{}, + }, + { + name: "whitespace-only doc after separator is skipped", + input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n", + expected: map[string]string{ + "manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1", + }, + }, + { + name: "single doc no separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +`, + }, + }, + { + name: "two docs with proper separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + }, + }, -func TestSplitManifest(t *testing.T) { - manifests := SplitManifests(mockManifestFile) - if len(manifests) != 1 { - t.Errorf("Expected 1 manifest, got %v", len(manifests)) + // Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep) + // inputs with 0, 1, and 2 trailing newlines after the block content. + // Note: the emitter may normalize the output chomping indicator when the + // trailing newline count makes another indicator equivalent for the result. + + // | (clip) input — clips trailing newlines to exactly one, though with + // 0 trailing newlines the emitted output may normalize to |-. + { + name: "block scalar clip (|) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + }, + }, + { + name: "block scalar clip (|) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +`, + }, + }, + { + name: "block scalar clip (|) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello + +`, + }, + }, + + // |- (strip) + { + name: "block scalar strip (|-) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello`, + }, + }, + { + name: "block scalar strip (|-) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello +`, + }, + }, + { + name: "block scalar strip (|-) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |- + hello + +`, + }, + }, + + // |+ (keep) + { + name: "block scalar keep (|+) with 0 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + }, + }, + { + name: "block scalar keep (|+) with 1 trailing newline", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello +`, + }, + }, + { + name: "block scalar keep (|+) with 2 trailing newlines", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + +`, + }, + }, + + // Multi-doc with block scalars: the regex consumes \s*\n before ---, + // so trailing newlines from non-last docs are stripped. + { + name: "multi-doc block scalar clip (|) before separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: | + hello`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + }, + }, + { + name: "multi-doc block scalar keep (|+) with 2 trailing newlines before separator", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: |+ + hello`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test2 +`, + }, + }, + + // **Note for Chart API v3**: The following tests exercise the lenient + // regex that splits `---apiVersion` back into separate documents. + // In Chart API v3, these inputs should return an _ERROR_ instead. + // See the comment on the SplitManifests function for more details. + { + name: "leading glued separator (---apiVersion)", + input: ` +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +`, + }, + }, + { + name: "mid-content glued separator (---apiVersion)", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +`, + }, + }, + { + name: "multiple glued separators", + input: ` +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2`, + "manifest-2": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + }, + }, + { + name: "mixed glued and proper separators", + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2 +---apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + expected: map[string]string{ + "manifest-0": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm1`, + "manifest-1": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm2`, + "manifest-2": `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm3 +`, + }, + }, } - expected := map[string]string{"manifest-0": expectedManifest} - if !reflect.DeepEqual(manifests, expected) { - t.Errorf("Expected %v, got %v", expected, manifests) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SplitManifests(tt.input) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) + } + }) } } From 934ace35dfaef9eeb9997bf1ee385db0986daecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:33:33 +0000 Subject: [PATCH 030/132] chore(deps): bump github/codeql-action from 4.35.1 to 4.35.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.1 to 4.35.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7916808e7..972602fea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 75406c17a..41e2f1254 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: results.sarif From 775e794319639f5c1e6b40448ce15ad3cc10d4e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:35:02 +0000 Subject: [PATCH 031/132] chore(deps): bump the k8s-io group with 7 updates Bumps the k8s-io group with 7 updates: | Package | From | To | | --- | --- | --- | | [k8s.io/api](https://github.com/kubernetes/api) | `0.35.3` | `0.35.4` | | [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.35.3` | `0.35.4` | | [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `0.35.3` | `0.35.4` | | [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `0.35.3` | `0.35.4` | | [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.35.3` | `0.35.4` | | [k8s.io/client-go](https://github.com/kubernetes/client-go) | `0.35.3` | `0.35.4` | | [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `0.35.3` | `0.35.4` | Updates `k8s.io/api` from 0.35.3 to 0.35.4 - [Commits](https://github.com/kubernetes/api/compare/v0.35.3...v0.35.4) Updates `k8s.io/apiextensions-apiserver` from 0.35.3 to 0.35.4 - [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases) - [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.35.3...v0.35.4) Updates `k8s.io/apimachinery` from 0.35.3 to 0.35.4 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.35.3...v0.35.4) Updates `k8s.io/apiserver` from 0.35.3 to 0.35.4 - [Commits](https://github.com/kubernetes/apiserver/compare/v0.35.3...v0.35.4) Updates `k8s.io/cli-runtime` from 0.35.3 to 0.35.4 - [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.35.3...v0.35.4) Updates `k8s.io/client-go` from 0.35.3 to 0.35.4 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.35.3...v0.35.4) Updates `k8s.io/kubectl` from 0.35.3 to 0.35.4 - [Commits](https://github.com/kubernetes/kubectl/compare/v0.35.3...v0.35.4) --- updated-dependencies: - dependency-name: k8s.io/api dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiextensions-apiserver dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiserver dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/cli-runtime dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/kubectl dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 1b1f9b4e3..7e734a01e 100644 --- a/go.mod +++ b/go.mod @@ -39,14 +39,14 @@ require ( golang.org/x/term v0.42.0 golang.org/x/text v0.36.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.3 - k8s.io/apiextensions-apiserver v0.35.3 - k8s.io/apimachinery v0.35.3 - k8s.io/apiserver v0.35.3 - k8s.io/cli-runtime v0.35.3 - k8s.io/client-go v0.35.3 + k8s.io/api v0.35.4 + k8s.io/apiextensions-apiserver v0.35.4 + k8s.io/apimachinery v0.35.4 + k8s.io/apiserver v0.35.4 + k8s.io/cli-runtime v0.35.4 + k8s.io/client-go v0.35.4 k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.35.3 + k8s.io/kubectl v0.35.4 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/kustomize/kyaml v0.21.1 @@ -172,7 +172,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.35.3 // indirect + k8s.io/component-base v0.35.4 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index ab994625e..9614bfb90 100644 --- a/go.sum +++ b/go.sum @@ -488,26 +488,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= -k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= -k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= -k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= -k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= -k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.3 h1:D2eIcfJ05hEAEewoSDg+05e0aSRwx8Y4Agvd/wiomUI= -k8s.io/apiserver v0.35.3/go.mod h1:JI0n9bHYzSgIxgIrfe21dbduJ9NHzKJ6RchcsmIKWKY= -k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI= -k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4= -k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= -k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= -k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0= -k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk= +k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= +k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= +k8s.io/apiextensions-apiserver v0.35.4 h1:HeP+Upp7ItdvnyGmub0yoix+2z5+ev4M5cE5TCgtOUU= +k8s.io/apiextensions-apiserver v0.35.4/go.mod h1:ogQlk+stIE8mnoRthSYCwlOS12fVqgWFiErMwPaXA7c= +k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= +k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= +k8s.io/apiserver v0.35.4 h1:vtuFqNFmF9bPRdHDL2lpK6qCTPWDreZJL4LRPwVM6ho= +k8s.io/apiserver v0.35.4/go.mod h1:JnBcb+J8kFXKpZkgcbcUnPBBHi4qgBii1I7dLxFY/oo= +k8s.io/cli-runtime v0.35.4 h1:8QRCXSDvopflFNM65Vkkdv42BljPdRSiqf6HFyI1iik= +k8s.io/cli-runtime v0.35.4/go.mod h1:MKLFuZxiJpm87UxjVeQRNy3sCaczHrSOPKN9pinlrM0= +k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= +k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= +k8s.io/component-base v0.35.4 h1:6n1tNJ87johN0Hif0Fs8K2GMthsaUwMqCebUDLYyv7U= +k8s.io/component-base v0.35.4/go.mod h1:qaDJgz5c1KYKla9occFmlJEfPpkuA55s90G509R+PeY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w= -k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s= +k8s.io/kubectl v0.35.4 h1:IHitney6OUeH29rBQnt6Cas6az8HpFeSAohormITNMc= +k8s.io/kubectl v0.35.4/go.mod h1:CGWAaof9ae4vGDAyhnSf1bSQN/U7jiWQHLVbMbLMjRI= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= From db40adb1d13573280b65bc2002df7d75c009235a Mon Sep 17 00:00:00 2001 From: Mohit Date: Sun, 19 Apr 2026 22:45:05 +0530 Subject: [PATCH 032/132] docs: fix grammar and spacing in CONTRIBUTING.md Signed-off-by: Mohit --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b704aa9a..7aa19972f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ chance to try to fix the issue before it is exploited in the wild. Helm v4 development takes place on the `main` branch while Helm v3 is on the `dev-v3` branch. -Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancement will still be applied until November 11th 2026. See the blog for more details. +Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancements will still be applied until November 11th 2026. See the blog for more details. Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v3 (and the `dev-v3` branch) is no longer accepting new features. @@ -162,9 +162,9 @@ There are 5 types of issues (each with their own corresponding [label](#labels)) for future reference. Generally these are questions that are too complex or large to store in the Slack channel or have particular interest to the community as a whole. Depending on the discussion, these can turn into `feature` or `bug` issues. -- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require +- `proposal`: Used for items (like this one) that propose new ideas or functionality that require a larger community discussion. This allows for feedback from others in the community before a - feature is actually developed. This is not needed for small additions. Final word on whether + feature is actually developed. This is not needed for small additions. Final word on whether a feature needs a proposal is up to the core maintainers. All issues that are proposals should both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become a `feature` and does not require a milestone. From 8f56f24d638612a46f3e23265d06338c1f93bccb Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Tue, 21 Apr 2026 19:11:38 +0100 Subject: [PATCH 033/132] fix(templating): hooks conflicting with templates in post-renderers (#32049) * fix(templating): hooks conflicting with templates in post-renderers Signed-off-by: Matheus Pimenta * fix(templating): allow disabling hooks from postrenderers entirely Signed-off-by: Matheus Pimenta --------- Signed-off-by: Matheus Pimenta --- pkg/action/action.go | 179 ++++++++++++++++++--- pkg/action/action_test.go | 317 ++++++++++++++++++++++++++++++++++++-- pkg/action/install.go | 13 +- pkg/action/upgrade.go | 13 +- 4 files changed, 481 insertions(+), 41 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index c93950103..8c1888144 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -88,6 +88,33 @@ const ( DryRunServer DryRunStrategy = "server" ) +// PostRenderStrategy determines how hooks and regular templates are passed +// to the configured post-renderer. +type PostRenderStrategy string + +const ( + // PostRenderStrategyCombined sends hooks and regular templates together + // as a single stream to the post-renderer. This is the default in Helm 4. + PostRenderStrategyCombined PostRenderStrategy = "combined" + + // PostRenderStrategySeparate sends hooks and regular templates to the + // post-renderer in independent invocations. This avoids duplicate-resource + // errors from post-renderers that de-duplicate by resource identity + // (for example Kustomize) when the same resource appears in both a hook + // and a regular template. Passing hooks to post-renderers was introduced + // in Helm 4; Helm 3 never did so, which is why the issue only surfaces + // with the Helm 4 combined default. + PostRenderStrategySeparate PostRenderStrategy = "separate" + + // PostRenderStrategyNoHooks sends only regular templates to the + // post-renderer and leaves hooks untouched. This matches the Helm 3 + // behavior and is useful for post-renderers that declare transforms + // targeting template-only resources (for example Kustomize patches + // against a Deployment that exists in templates but not in hooks), + // which would otherwise fail against the hook stream. + PostRenderStrategyNoHooks PostRenderStrategy = "nohooks" +) + // Configuration injects the dependencies that all actions share. type Configuration struct { // RESTClientGetter is an interface that loads Kubernetes clients. @@ -198,7 +225,14 @@ func annotateAndMerge(files map[string]string) (string, error) { // splitAndDeannotate reconstructs individual files from a merged YAML stream, // removing filename annotations and grouping documents by their original filenames. -func splitAndDeannotate(postrendered string) (map[string]string, error) { +// Documents without a filename annotation are assigned a synthesized name of the +// form "generated-by-postrender--.yaml" (or +// "generated-by-postrender-.yaml" when fallbackPrefix is empty). The prefix +// disambiguates fallback filenames across multiple post-render invocations (for +// example when PostRenderStrategySeparate runs the post-renderer once per +// group), so that merging results from different invocations does not collide +// on the same synthetic key. +func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, error) { manifests, err := kio.ParseAll(postrendered) if err != nil { return nil, fmt.Errorf("error parsing YAML: %w", err) @@ -212,7 +246,11 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { } fname := meta.Annotations[filenameAnnotation] if fname == "" { - fname = fmt.Sprintf("generated-by-postrender-%d.yaml", i) + if fallbackPrefix == "" { + fname = fmt.Sprintf("generated-by-postrender-%d.yaml", i) + } else { + fname = fmt.Sprintf("generated-by-postrender-%s-%d.yaml", fallbackPrefix, i) + } } if err := manifest.PipeE(kyaml.ClearAnnotation(filenameAnnotation)); err != nil { return nil, fmt.Errorf("clearing filename annotation: %w", err) @@ -237,7 +275,7 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -301,29 +339,122 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, notes := notesBuffer.String() if pr != nil { - // We need to send files to the post-renderer before sorting and splitting - // hooks from manifests. The post-renderer interface expects a stream of - // manifests (similar to what tools like Kustomize and kubectl expect), whereas - // the sorter uses filenames. - // Here, we merge the documents into a stream, post-render them, and then split - // them back into a map of filename -> content. - - // Merge files as stream of documents for sending to post renderer - merged, err := annotateAndMerge(files) - if err != nil { - return hs, b, notes, fmt.Errorf("error merging manifests: %w", err) - } + switch postRenderStrategy { + case PostRenderStrategySeparate, PostRenderStrategyNoHooks: + // Split hooks from manifests before post-rendering. For "separate", + // hooks and templates are sent to the post-renderer as independent + // streams to avoid duplicate-resource errors when the same resource + // appears in both (e.g. a ServiceAccount used by a pre-install hook + // that is also declared in the chart's regular templates). For + // "nohooks", hooks skip the post-renderer entirely, matching the + // Helm 3 behavior. + sortedHooks, sortedManifests, err := releaseutil.SortManifests(files, nil, releaseutil.InstallOrder) + if err != nil { + for name, content := range files { + if strings.TrimSpace(content) == "" { + continue + } + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + } + return hs, b, "", err + } - // Run the post renderer - postRendered, err := pr.Run(bytes.NewBufferString(merged)) - if err != nil { - return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err) - } + // Build separate files maps for hooks and manifests. + hookFiles := make(map[string]string) + for _, h := range sortedHooks { + if existing, ok := hookFiles[h.Path]; ok { + hookFiles[h.Path] = existing + "\n---\n" + h.Manifest + } else { + hookFiles[h.Path] = h.Manifest + } + } + manifestFiles := make(map[string]string) + for _, m := range sortedManifests { + if existing, ok := manifestFiles[m.Name]; ok { + manifestFiles[m.Name] = existing + "\n---\n" + m.Content + } else { + manifestFiles[m.Name] = m.Content + } + } - // Use the file list and contents received from the post renderer - files, err = splitAndDeannotate(postRendered.String()) - if err != nil { - return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err) + // Decide which groups to post-render. "nohooks" passes hooks + // through untouched and only post-renders manifests. + groups := []struct { + name string + files map[string]string + postRender bool + }{ + {"hooks", hookFiles, postRenderStrategy == PostRenderStrategySeparate}, + {"manifests", manifestFiles, true}, + } + + files = make(map[string]string) + for _, group := range groups { + if len(group.files) == 0 { + continue + } + + if !group.postRender { + for k, v := range group.files { + if existing, ok := files[k]; ok { + files[k] = existing + "\n---\n" + v + } else { + files[k] = v + } + } + continue + } + + merged, err := annotateAndMerge(group.files) + if err != nil { + return hs, b, notes, fmt.Errorf("error merging %s: %w", group.name, err) + } + + postRendered, err := pr.Run(bytes.NewBufferString(merged)) + if err != nil { + return hs, b, notes, fmt.Errorf("error while running post render on %s: %w", group.name, err) + } + + rendered, err := splitAndDeannotate(postRendered.String(), group.name) + if err != nil { + return hs, b, notes, fmt.Errorf("error while parsing post rendered output for %s: %w", group.name, err) + } + + for k, v := range rendered { + if existing, ok := files[k]; ok { + files[k] = existing + "\n---\n" + v + } else { + files[k] = v + } + } + } + case PostRenderStrategyCombined, "": + // We need to send files to the post-renderer before sorting and splitting + // hooks from manifests. The post-renderer interface expects a stream of + // manifests (similar to what tools like Kustomize and kubectl expect), whereas + // the sorter uses filenames. + // Here, we merge the documents into a stream, post-render them, and then split + // them back into a map of filename -> content. + + // Merge files as stream of documents for sending to post renderer + merged, err := annotateAndMerge(files) + if err != nil { + return hs, b, notes, fmt.Errorf("error merging manifests: %w", err) + } + + // Run the post renderer + postRendered, err := pr.Run(bytes.NewBufferString(merged)) + if err != nil { + return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err) + } + + // Use the file list and contents received from the post renderer + files, err = splitAndDeannotate(postRendered.String(), "") + if err != nil { + return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err) + } + default: + return hs, b, notes, fmt.Errorf("unknown post-render strategy: '%s'", postRenderStrategy) } } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index d6575a791..54b07273b 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1722,7 +1722,7 @@ metadata: data: key: value`, expectedFiles: map[string]string{ - "generated-by-postrender-0.yaml": `apiVersion: v1 + "generated-by-postrender-test-0.yaml": `apiVersion: v1 kind: ConfigMap metadata: name: test-cm @@ -1735,7 +1735,7 @@ data: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - files, err := splitAndDeannotate(tt.input) + files, err := splitAndDeannotate(tt.input, "test") if tt.expectedError != "" { assert.Error(t, err) @@ -1789,7 +1789,7 @@ data: require.NoError(t, err) // Split and deannotate - reconstructed, err := splitAndDeannotate(merged) + reconstructed, err := splitAndDeannotate(merged, "test") require.NoError(t, err) // Compare the results @@ -1824,7 +1824,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1871,7 +1871,7 @@ func TestRenderResources_PostRenderer_Error(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1899,7 +1899,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1921,7 +1921,7 @@ func TestRenderResources_PostRenderer_SplitError(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1942,7 +1942,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1981,7 +1981,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - nil, false, false, false, + nil, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1990,6 +1990,305 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { assert.Equal(t, "", notes) } +func TestRenderResources_PostRenderer_DuplicateResourceInHookAndTemplate(t *testing.T) { + cfg := actionConfigFixture(t) + + // Simulate a chart where the same ServiceAccount appears both as a + // pre-install hook and as a regular template. This is a valid Helm pattern + // but previously caused post-renderers like Kustomize to fail with + // "may not add resource with an already registered id" because hooks and + // templates were merged into a single stream before post-rendering. + saHook := `apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded` + + saTemplate := `apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app` + + deployment := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + serviceAccountName: my-app` + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/sa-hook.yaml", ModTime: modTime, Data: []byte(saHook)}, + {Name: "templates/sa.yaml", ModTime: modTime, Data: []byte(saTemplate)}, + {Name: "templates/deployment.yaml", ModTime: modTime, Data: []byte(deployment)}, + }) + + // Use a post-renderer that rejects duplicate resource IDs, similar to + // how Kustomize behaves. We verify that no single post-render call + // receives the ServiceAccount twice. + mockPR := &mockPostRenderer{ + transform: func(content string) string { + count := strings.Count(content, "kind: ServiceAccount") + if count > 1 { + t.Errorf("post-renderer received %d ServiceAccount resources in a single stream, expected at most 1", count) + } + return content + }, + } + + hooks, buf, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Len(t, hooks, 1) + assert.Equal(t, "my-app", hooks[0].Name) + assert.Contains(t, buf.String(), "kind: Deployment") + assert.Contains(t, buf.String(), "kind: ServiceAccount") +} + +func TestRenderResources_PostRenderer_CombinedInvokesOnceWithEverything(t *testing.T) { + cfg := actionConfigFixture(t) + + hookManifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install` + templateManifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm` + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(hookManifest)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(templateManifest)}, + }) + + var calls int + var lastInput string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + lastInput = content + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "combined strategy should invoke the post-renderer exactly once") + assert.Contains(t, lastInput, "hook-cm") + assert.Contains(t, lastInput, "template-cm") +} + +func TestRenderResources_PostRenderer_ZeroValueStrategyActsAsCombined(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategy(""), + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "unset strategy must preserve backwards-compatible combined behavior") +} + +func TestRenderResources_PostRenderer_SeparateSplitsHooksAndTemplates(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var inputs []string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + inputs = append(inputs, content) + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Len(t, inputs, 2, "separate strategy should invoke the post-renderer twice when both hooks and templates exist") + for _, in := range inputs { + hasHook := strings.Contains(in, "hook-cm") + hasTemplate := strings.Contains(in, "template-cm") + assert.False(t, hasHook && hasTemplate, "a single post-render invocation must not contain both hook and template resources") + assert.True(t, hasHook || hasTemplate, "each post-render invocation must contain either a hook or a template") + } +} + +func TestRenderResources_PostRenderer_SeparateWithOnlyTemplates(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "separate strategy should skip the empty hook group and invoke the post-renderer only once") +} + +func TestRenderResources_PostRenderer_NoHooksSkipsHooks(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var inputs []string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + inputs = append(inputs, content) + return content + }, + } + + hooks, manifestDoc, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyNoHooks, + ) + + assert.NoError(t, err) + assert.Len(t, inputs, 1, "nohooks strategy should invoke the post-renderer exactly once (for templates only)") + assert.NotContains(t, inputs[0], "hook-cm", "hooks must not be sent to the post-renderer") + assert.Contains(t, inputs[0], "template-cm", "templates must be sent to the post-renderer") + + // Hooks still round-trip through the release so they can execute. + require.Len(t, hooks, 1) + assert.Contains(t, hooks[0].Manifest, "hook-cm") + assert.Contains(t, manifestDoc.String(), "template-cm") +} + +func TestRenderResources_PostRenderer_NoHooksWithOnlyHooks(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyNoHooks, + ) + + assert.NoError(t, err) + assert.Equal(t, 0, calls, "nohooks strategy should not invoke the post-renderer when the chart only has hooks") +} + +func TestRenderResources_PostRenderer_UnknownStrategyErrors(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + mockPR := &mockPostRenderer{} + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategy("bogus"), + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown post-render strategy") + assert.Contains(t, err.Error(), "bogus") +} + func TestDetermineReleaseSSAApplyMethod(t *testing.T) { assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) diff --git a/pkg/action/install.go b/pkg/action/install.go index 50df13c05..580b8a0cb 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -130,6 +130,10 @@ type Install struct { // TakeOwnership will ignore the check for helm annotations and take ownership of the resources. TakeOwnership bool PostRenderer postrenderer.PostRenderer + // PostRenderStrategy controls how hooks and regular templates are passed + // to the configured post-renderer. See PostRenderStrategy for the + // available modes. Defaults to PostRenderStrategyCombined. + PostRenderStrategy PostRenderStrategy // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex goroutineCount atomic.Int32 @@ -158,9 +162,10 @@ type ChartPathOptions struct { // NewInstall creates a new Install object with the given configuration. func NewInstall(cfg *Configuration) *Install { in := &Install{ - cfg: cfg, - ServerSideApply: true, // Must always match the CLI default. - DryRunStrategy: DryRunNone, + cfg: cfg, + ServerSideApply: true, // Must always match the CLI default. + DryRunStrategy: DryRunNone, + PostRenderStrategy: PostRenderStrategyCombined, } in.registryClient = cfg.RegistryClient @@ -370,7 +375,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 103ab4fdb..00939ffa6 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -121,6 +121,10 @@ type Upgrade struct { // If this is non-nil, then after templates are rendered, they will be sent to the // post renderer before sending to the Kubernetes API server. PostRenderer postrenderer.PostRenderer + // PostRenderStrategy controls how hooks and regular templates are passed + // to the configured post-renderer. See PostRenderStrategy for the + // available modes. Defaults to PostRenderStrategyCombined. + PostRenderStrategy PostRenderStrategy // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. DisableOpenAPIValidation bool // Get missing dependencies @@ -141,9 +145,10 @@ type resultMessage struct { // NewUpgrade creates a new Upgrade object with the given configuration. func NewUpgrade(cfg *Configuration) *Upgrade { up := &Upgrade{ - cfg: cfg, - ServerSideApply: "auto", // Must always match the CLI default. - DryRunStrategy: DryRunNone, + cfg: cfg, + ServerSideApply: "auto", // Must always match the CLI default. + DryRunStrategy: DryRunNone, + PostRenderStrategy: PostRenderStrategyCombined, } up.registryClient = cfg.RegistryClient @@ -296,7 +301,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) if err != nil { return nil, nil, false, err } From 1e145ee2b243ef97bce0622cf55da8aa17ebb65f Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 23 Jan 2026 15:25:09 +0100 Subject: [PATCH 034/132] fix: prevent warning when using version range constraints When using version ranges like ^1 or ~1.10, Helm incorrectly showed warnings about falling back to closest version. Only show the warning when an exact version is requested but not found. Fixes: https://github.com/helm/helm/issues/31757 Signed-off-by: Benoit Tigeot --- pkg/repo/v1/index.go | 8 +++++++- pkg/repo/v1/index_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 3dbdf7dfc..461416a59 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -175,6 +175,12 @@ func (i IndexFile) SortEntries() { } } +// isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10") +// rather than an exact version (e.g., "1.10.0"). +func isVersionRange(version string) bool { + return strings.ContainsAny(version, "^~<>=!*xX") || strings.Contains(version, " || ") || strings.Contains(version, " - ") +} + // Get returns the ChartVersion for the given name. // // If version is empty, this will return the chart with the latest stable version, @@ -215,7 +221,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } if constraint.Check(test) { - if len(version) != 0 { + if len(version) != 0 && !isVersionRange(version) { slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) } return ver, nil diff --git a/pkg/repo/v1/index_test.go b/pkg/repo/v1/index_test.go index 550c8e82c..b01aa5a7d 100644 --- a/pkg/repo/v1/index_test.go +++ b/pkg/repo/v1/index_test.go @@ -718,3 +718,37 @@ func TestLoadIndex_DuplicateChartDeps(t *testing.T) { }) } } + +func TestIsVersionRange(t *testing.T) { + tests := []struct { + version string + expected bool + }{ + {"1.0.0", false}, + {"1.0.0+metadata", false}, + {"^1", true}, + {"^1.2.3", true}, + {"~1.10", true}, + {"~1.10.0", true}, + {">= 1.0.0", true}, + {"> 1.0.0", true}, + {"< 2.0.0", true}, + {"<= 2.0.0", true}, + {"!= 1.0.0", true}, + {"1.*", true}, + {"1.x", true}, + {"1.X", true}, + {"1.0.0 - 2.0.0", true}, + {"^1.0.0 || ^2.0.0", true}, + {">=1.0.0 <2.0.0", true}, + } + + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + got := isVersionRange(tt.version) + if got != tt.expected { + t.Errorf("isVersionRange(%q) = %v, want %v", tt.version, got, tt.expected) + } + }) + } +} From bf78b876c74ee4359a6cebb54e0b97183cdfe129 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 23 Jan 2026 17:48:35 +0100 Subject: [PATCH 035/132] feat: report in debug the version we select with version range arg Signed-off-by: Benoit Tigeot --- pkg/repo/v1/index.go | 2 ++ pkg/repo/v1/index_test.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 461416a59..ccec82b3e 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -223,6 +223,8 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { if constraint.Check(test) { if len(version) != 0 && !isVersionRange(version) { slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) + } else if len(version) != 0 && isVersionRange(version) { + slog.Debug("selected version matching constraint", "chart", name, "constraint", version, "selected", ver.Version) } return ver, nil } diff --git a/pkg/repo/v1/index_test.go b/pkg/repo/v1/index_test.go index b01aa5a7d..7a3120c89 100644 --- a/pkg/repo/v1/index_test.go +++ b/pkg/repo/v1/index_test.go @@ -726,6 +726,8 @@ func TestIsVersionRange(t *testing.T) { }{ {"1.0.0", false}, {"1.0.0+metadata", false}, + {"v1.19.2", false}, + {"v1", false}, {"^1", true}, {"^1.2.3", true}, {"~1.10", true}, @@ -738,6 +740,8 @@ func TestIsVersionRange(t *testing.T) { {"1.*", true}, {"1.x", true}, {"1.X", true}, + {"v1.x", true}, + {"v1.X", true}, {"1.0.0 - 2.0.0", true}, {"^1.0.0 || ^2.0.0", true}, {">=1.0.0 <2.0.0", true}, From b79d7f18813796f101f66eff6c513ded25edf0ad Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 4 Feb 2026 22:02:19 +0100 Subject: [PATCH 036/132] fix(version): version range || can has no space From Matt's comment > The check for " || " should remove the spaces and have "||". Spaces around the || aren't required. Signed-off-by: Benoit Tigeot --- pkg/repo/v1/index.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index ccec82b3e..1ed7c32b6 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -178,7 +178,7 @@ func (i IndexFile) SortEntries() { // isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10") // rather than an exact version (e.g., "1.10.0"). func isVersionRange(version string) bool { - return strings.ContainsAny(version, "^~<>=!*xX") || strings.Contains(version, " || ") || strings.Contains(version, " - ") + return strings.ContainsAny(version, "^~<>=!*xX") || strings.Contains(version, "||") || strings.Contains(version, " - ") } // Get returns the ChartVersion for the given name. From 740174a2b12074f7ca506ff330a918a4ff335c39 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 24 Apr 2026 09:23:11 +0200 Subject: [PATCH 037/132] fix(version): avoid false range detection on prerelease x/X `isVersionRange` checked for `x`/`X` across the entire version string, misclassifying exact versions like `1.0.0-fix`, `2.0.0-next`, or `1.0.0+exp` as ranges. Signed-off-by: Benoit Tigeot --- pkg/repo/v1/index.go | 9 ++++++++- pkg/repo/v1/index_test.go | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 1ed7c32b6..f9829ec7f 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -178,7 +178,14 @@ func (i IndexFile) SortEntries() { // isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10") // rather than an exact version (e.g., "1.10.0"). func isVersionRange(version string) bool { - return strings.ContainsAny(version, "^~<>=!*xX") || strings.Contains(version, "||") || strings.Contains(version, " - ") + if strings.ContainsAny(version, "^~<>=!*") || strings.Contains(version, "||") || strings.Contains(version, " - ") { + return true + } + core := version + if idx := strings.IndexAny(version, "-+"); idx != -1 { + core = version[:idx] + } + return strings.ContainsAny(core, "xX") } // Get returns the ChartVersion for the given name. diff --git a/pkg/repo/v1/index_test.go b/pkg/repo/v1/index_test.go index 7a3120c89..a86efe1e3 100644 --- a/pkg/repo/v1/index_test.go +++ b/pkg/repo/v1/index_test.go @@ -745,6 +745,10 @@ func TestIsVersionRange(t *testing.T) { {"1.0.0 - 2.0.0", true}, {"^1.0.0 || ^2.0.0", true}, {">=1.0.0 <2.0.0", true}, + // Exact versions with 'x'/'X' in prerelease or build metadata + {"1.0.0-fix", false}, + {"2.0.0-next", false}, + {"1.0.0+exp", false}, } for _, tt := range tests { From a4a9cc7a314d98456a2f23798a78e9ad05d96d0c Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Sat, 25 Apr 2026 15:13:25 +0100 Subject: [PATCH 038/132] Upgrade Go to 1.26, Kubernetes to 1.36, kstatus to 1.1 Signed-off-by: Matheus Pimenta --- .github/env | 2 +- go.mod | 35 +++++---- go.sum | 71 +++++++++---------- internal/chart/v3/lint/rules/template.go | 4 +- pkg/chart/v2/lint/rules/template.go | 4 +- pkg/cmd/lint_test.go | 2 +- .../lint-chart-with-deprecated-api-strict.txt | 2 +- .../output/lint-chart-with-deprecated-api.txt | 2 +- .../templates/horizontalpodautoscaler.yaml | 9 --- .../templates/poddisruptionbudget.yaml | 9 +++ pkg/kube/client_test.go | 1 + 11 files changed, 69 insertions(+), 72 deletions(-) delete mode 100644 pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml create mode 100644 pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml diff --git a/.github/env b/.github/env index fc6f21880..43ebdd36e 100644 --- a/.github/env +++ b/.github/env @@ -1,2 +1,2 @@ -GOLANG_VERSION=1.25 +GOLANG_VERSION=1.26 GOLANGCI_LINT_VERSION=v2.11.3 diff --git a/go.mod b/go.mod index 7e734a01e..bcddd6986 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module helm.sh/helm/v4 -go 1.25.0 +go 1.26.0 require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 @@ -17,7 +17,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.0.0 + github.com/fluxcd/cli-utils v1.1.0 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -39,14 +39,14 @@ require ( golang.org/x/term v0.42.0 golang.org/x/text v0.36.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.4 - k8s.io/apiextensions-apiserver v0.35.4 - k8s.io/apimachinery v0.35.4 - k8s.io/apiserver v0.35.4 - k8s.io/cli-runtime v0.35.4 - k8s.io/client-go v0.35.4 - k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.35.4 + k8s.io/api v0.36.0 + k8s.io/apiextensions-apiserver v0.36.0 + k8s.io/apimachinery v0.36.0 + k8s.io/apiserver v0.36.0 + k8s.io/cli-runtime v0.36.0 + k8s.io/client-go v0.36.0 + k8s.io/klog/v2 v2.140.0 + k8s.io/kubectl v0.36.0 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/kustomize/kyaml v0.21.1 @@ -65,7 +65,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.6.3 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -74,7 +74,7 @@ require ( github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -91,7 +91,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect @@ -168,15 +167,15 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.35.4 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/component-base v0.36.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/go.sum b/go.sum index 9614bfb90..9b7f40f94 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNS github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -81,8 +81,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.0.0 h1:+luz8igR6dM5f7uHwkkMTECsl+jp0kR69POuV5aOoDs= -github.com/fluxcd/cli-utils v1.0.0/go.mod h1:ANTIXWLLsNmn5bMNxbyoY22rtwRSR/fbu+IFy756fs0= +github.com/fluxcd/cli-utils v1.1.0 h1:P2oULlj4aNSqjBGcWOCQS+TOS5ZSyoJMy1zYCpqsYus= +github.com/fluxcd/cli-utils v1.1.0/go.mod h1:+ipwad8nfETe+VB3SMgrDv6m0mqA/KQSj2wyn8Y7vmo= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -128,7 +128,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -155,8 +154,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= @@ -373,8 +370,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -472,8 +469,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -488,28 +485,28 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= -k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= -k8s.io/apiextensions-apiserver v0.35.4 h1:HeP+Upp7ItdvnyGmub0yoix+2z5+ev4M5cE5TCgtOUU= -k8s.io/apiextensions-apiserver v0.35.4/go.mod h1:ogQlk+stIE8mnoRthSYCwlOS12fVqgWFiErMwPaXA7c= -k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= -k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= -k8s.io/apiserver v0.35.4 h1:vtuFqNFmF9bPRdHDL2lpK6qCTPWDreZJL4LRPwVM6ho= -k8s.io/apiserver v0.35.4/go.mod h1:JnBcb+J8kFXKpZkgcbcUnPBBHi4qgBii1I7dLxFY/oo= -k8s.io/cli-runtime v0.35.4 h1:8QRCXSDvopflFNM65Vkkdv42BljPdRSiqf6HFyI1iik= -k8s.io/cli-runtime v0.35.4/go.mod h1:MKLFuZxiJpm87UxjVeQRNy3sCaczHrSOPKN9pinlrM0= -k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= -k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= -k8s.io/component-base v0.35.4 h1:6n1tNJ87johN0Hif0Fs8K2GMthsaUwMqCebUDLYyv7U= -k8s.io/component-base v0.35.4/go.mod h1:qaDJgz5c1KYKla9occFmlJEfPpkuA55s90G509R+PeY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.35.4 h1:IHitney6OUeH29rBQnt6Cas6az8HpFeSAohormITNMc= -k8s.io/kubectl v0.35.4/go.mod h1:CGWAaof9ae4vGDAyhnSf1bSQN/U7jiWQHLVbMbLMjRI= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE= +k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho= +k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg= +k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo= +k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms= +k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= @@ -522,7 +519,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7 sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index 35e4940ab..a8ae910eb 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -28,8 +28,8 @@ import ( "slices" "strings" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/api/validation" - apipath "k8s.io/apimachinery/pkg/api/validation/path" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" @@ -292,7 +292,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { case "role", "clusterrole", "rolebinding", "clusterrolebinding": // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 return func(name string, _ bool) []string { - return apipath.IsValidPathSegmentName(name) + return content.IsPathSegmentName(name) } default: return validation.NameIsDNSSubdomain diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 43665aa3a..94210dec8 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -28,8 +28,8 @@ import ( "slices" "strings" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/api/validation" - apipath "k8s.io/apimachinery/pkg/api/validation/path" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" @@ -323,7 +323,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { case "role", "clusterrole", "rolebinding", "clusterrolebinding": // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 return func(name string, _ bool) []string { - return apipath.IsValidPathSegmentName(name) + return content.IsPathSegmentName(name) } default: return validation.NameIsDNSSubdomain diff --git a/pkg/cmd/lint_test.go b/pkg/cmd/lint_test.go index a13ec423b..82fe249f6 100644 --- a/pkg/cmd/lint_test.go +++ b/pkg/cmd/lint_test.go @@ -84,7 +84,7 @@ func TestLintCmdWithKubeVersionFlag(t *testing.T) { wantError: false, }, { name: "lint chart with deprecated api version with older kube version", - cmd: "lint --kube-version 1.21.0 --strict " + testChart, + cmd: "lint --kube-version 1.20.0 --strict " + testChart, golden: "output/lint-chart-with-deprecated-api-old-k8s.txt", wantError: false, }} diff --git a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt index a1ec4394e..c25efc1df 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt @@ -1,5 +1,5 @@ ==> Linting testdata/testcharts/chart-with-deprecated-api [INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler +[WARNING] templates/poddisruptionbudget.yaml: policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget Error: 1 chart(s) linted, 1 chart(s) failed diff --git a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt index dac54620c..08dbde95d 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt @@ -1,5 +1,5 @@ ==> Linting testdata/testcharts/chart-with-deprecated-api [INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler +[WARNING] templates/poddisruptionbudget.yaml: policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget 1 chart(s) linted, 0 chart(s) failed diff --git a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml deleted file mode 100644 index b77a4beeb..000000000 --- a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: deprecated -spec: - scaleTargetRef: - kind: Pod - name: pod - maxReplicas: 3 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml new file mode 100644 index 000000000..214d3cb68 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: deprecated +spec: + maxUnavailable: 1 + selector: + matchLabels: + app: deprecated diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index e98d87520..ed871c05a 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -972,6 +972,7 @@ func TestGetPodList(t *testing.T) { podList, err := c.GetPodList(namespace, metav1.ListOptions{}) clientAssertions := assert.New(t) clientAssertions.NoError(err) + podList.ResourceVersion = "" clientAssertions.Equal(&responsePodList, podList) } From 4e24ee41a436889a2542a008eaa0ecab8332a1eb Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 26 Apr 2026 09:16:27 -0600 Subject: [PATCH 039/132] fix(kube): prevent spurious early exit in WaitForDelete during informer sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During informer initialization there is a brief window where watched resources appear as Unknown before their real statuses are delivered. The statusObserver skips Unknown resources when waiting for deletion (they may have been deleted before the watch started), but if *all* resources are in that transient Unknown state the skipped-resource list is empty. AggregateStatus on an empty slice returns the desired status, causing cancel() to be called immediately — before any real status event has arrived. Guard against this by tracking the count of Unknown-skipped resources. When every resource was Unknown-skipped and none have a definitive status yet, defer the early-cancel decision until at least one resource reports a real status. This preserves the correct behaviour for resources that were genuinely deleted before the watch started (they eventually receive a NotFound or stay Unknown, and the aggregate succeeds), while fixing the race for resources that are transiently Unknown at startup. Also tighten the ctx.Err() check in waitForDelete: only append a deadline error when there are resource-specific errors to accompany it. A timeout while all resources are Unknown or NotFound is not itself an error — the resources are in an acceptable state for a delete wait. Fixes: TestStatusWaitForDelete/error_when_not_all_objects_are_deleted Signed-off-by: Terry Howe --- pkg/kube/statuswait.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index 59c1218ff..a93d28fc1 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -160,7 +160,10 @@ func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceL errs = append(errs, fmt.Errorf("resource %s/%s/%s still exists. status: %s, message: %s", rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message)) } - if err := ctx.Err(); err != nil { + // Only include a deadline error when there are also resource-specific errors. + // If all resources are Unknown or NotFound (e.g. deleted before the watch started), + // a timeout is not itself an error for WaitForDelete. + if err := ctx.Err(); err != nil && len(errs) > 0 { errs = append(errs, err) } if len(errs) > 0 { @@ -234,6 +237,7 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl return func(statusCollector *collector.ResourceStatusCollector, _ event.Event) { var rss []*event.ResourceStatus var nonDesiredResources []*event.ResourceStatus + var unknownSkipped int for _, rs := range statusCollector.ResourceStatuses { if rs == nil { continue @@ -241,6 +245,7 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl // If a resource is already deleted before waiting has started, it will show as unknown. // This check ensures we don't wait forever for a resource that is already deleted. if rs.Status == status.UnknownStatus && desired == status.NotFoundStatus { + unknownSkipped++ continue } // Failed is a terminal state. This check ensures we don't wait forever for a resource @@ -254,6 +259,14 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl } } + // During informer initialization there is a brief window where existing resources + // appear as Unknown before their real status is delivered. If every resource was + // skipped as Unknown, we cannot yet distinguish "all deleted" from "not yet synced", + // so hold off on the early-cancel to avoid a spurious success or premature exit. + if unknownSkipped > 0 && len(rss) == 0 { + return + } + if aggregator.AggregateStatus(rss, desired) == desired { logger.Debug("all resources achieved desired status", "desiredStatus", desired, "resourceCount", len(rss)) cancel() From 5e09ee78ee5861b857a3526a2fd1ddf2f73950e2 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 26 Apr 2026 09:49:20 -0600 Subject: [PATCH 040/132] fix(kube): always propagate context.Canceled in WaitForDelete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous change suppressed ctx.Err() whenever there were no resource-specific errors, which incorrectly swallowed context.Canceled and other non-deadline errors signalling an external interruption. Refine the condition: only suppress context.DeadlineExceeded when there are no resource-specific errors (resources are Unknown/NotFound, meaning the delete wait succeeded or resources were already gone). Any other context error — including context.Canceled — is always propagated. Signed-off-by: Terry Howe --- pkg/kube/statuswait.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index a93d28fc1..10327358e 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -160,11 +160,16 @@ func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceL errs = append(errs, fmt.Errorf("resource %s/%s/%s still exists. status: %s, message: %s", rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message)) } - // Only include a deadline error when there are also resource-specific errors. - // If all resources are Unknown or NotFound (e.g. deleted before the watch started), - // a timeout is not itself an error for WaitForDelete. - if err := ctx.Err(); err != nil && len(errs) > 0 { - errs = append(errs, err) + if err := ctx.Err(); err != nil { + // context.Canceled and other non-deadline errors always propagate: they signal an + // external interruption regardless of resource state. + // context.DeadlineExceeded is only added when there are resource-specific errors; + // if all resources are Unknown or NotFound the timeout is not itself a failure for + // a delete wait (e.g. resources deleted before the watch started stay Unknown in + // the fake client but are effectively gone). + if !errors.Is(err, context.DeadlineExceeded) || len(errs) > 0 { + errs = append(errs, err) + } } if len(errs) > 0 { return errors.Join(errs...) From f8ba28bb17f9254bc2c1b6409b24c16a59934a9c Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Mon, 27 Apr 2026 19:48:55 +0200 Subject: [PATCH 041/132] testifylint: enable error-is-as and error-nil rules Signed-off-by: Matthieu MOREL --- .golangci.yml | 2 - internal/chart/v3/chart_test.go | 2 +- internal/plugin/runtime_extismv1_test.go | 2 +- pkg/action/install_test.go | 8 +-- pkg/action/registry_login_test.go | 10 ++-- pkg/action/upgrade_test.go | 2 +- pkg/chart/v2/chart_test.go | 2 +- pkg/cmd/helpers_test.go | 4 +- pkg/engine/engine_test.go | 5 +- pkg/registry/client_http_test.go | 11 ++-- pkg/registry/client_insecure_tls_test.go | 8 +-- pkg/registry/client_tls_test.go | 16 +++--- pkg/registry/registry_test.go | 72 ++++++++++++------------ pkg/repo/v1/repotest/tlsconfig.go | 3 +- 14 files changed, 72 insertions(+), 75 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 1ed3353b4..853324c6e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -101,8 +101,6 @@ linters: - empty - encoded-compare - equal-values - - error-is-as - - error-nil - expected-actual - float-compare - go-require diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index 5f6ca548d..f89030095 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -104,7 +104,7 @@ func TestMetadata(t *testing.T) { is.Equal("foo.yaml", chrt.Name()) is.Equal("1.0.0", chrt.AppVersion()) - is.Equal(nil, chrt.Validate()) + is.NoError(chrt.Validate()) } func TestIsRoot(t *testing.T) { diff --git a/internal/plugin/runtime_extismv1_test.go b/internal/plugin/runtime_extismv1_test.go index 8d9c55195..bcd791905 100644 --- a/internal/plugin/runtime_extismv1_test.go +++ b/internal/plugin/runtime_extismv1_test.go @@ -79,7 +79,7 @@ func TestRuntimeExtismV1InvokePlugin(t *testing.T) { Name: "Phippy", }, }) - require.Nil(t, err) + require.NoError(t, err) msg := output.Message.(schema.OutputMessageTestV1) assert.Equal(t, "Hello, Phippy! (6)", msg.Greeting) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 05ca9a75e..38d692a0b 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -867,7 +867,7 @@ func TestInstallReleaseOutputDir(t *testing.T) { test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt") _, err = os.Stat(filepath.Join(dir, "hello/templates/empty")) - is.True(errors.Is(err, fs.ErrNotExist)) + is.ErrorIs(err, fs.ErrNotExist) } func TestInstallOutputDirWithReleaseName(t *testing.T) { @@ -903,7 +903,7 @@ func TestInstallOutputDirWithReleaseName(t *testing.T) { test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt") _, err = os.Stat(filepath.Join(newDir, "hello/templates/empty")) - is.True(errors.Is(err, fs.ErrNotExist)) + is.ErrorIs(err, fs.ErrNotExist) } func TestNameAndChart(t *testing.T) { @@ -1168,7 +1168,7 @@ func TestInstallCRDs_AlreadyExist(t *testing.T) { mockChart := buildChart(withFile(mockFile)) crdsToInstall := mockChart.CRDObjects() - assert.Nil(t, instAction.installCRDs(crdsToInstall)) + assert.NoError(t, instAction.installCRDs(crdsToInstall)) } func TestInstallCRDs_KubeClient_BuildError(t *testing.T) { @@ -1227,7 +1227,7 @@ func TestCheckDependencies(t *testing.T) { dependency := chart.Dependency{Name: "hello"} mockChart := buildChart(withDependency()) - assert.Nil(t, CheckDependencies(mockChart, []ci.Dependency{&dependency})) + assert.NoError(t, CheckDependencies(mockChart, []ci.Dependency{&dependency})) } func TestCheckDependencies_MissingDependency(t *testing.T) { diff --git a/pkg/action/registry_login_test.go b/pkg/action/registry_login_test.go index 590673b3a..474118c9f 100644 --- a/pkg/action/registry_login_test.go +++ b/pkg/action/registry_login_test.go @@ -37,7 +37,7 @@ func TestWithCertFile(t *testing.T) { certFile := "testdata/cert.pem" opt := WithCertFile(certFile) - assert.Nil(t, opt(client)) + assert.NoError(t, opt(client)) assert.Equal(t, certFile, client.certFile) } @@ -47,7 +47,7 @@ func TestWithInsecure(t *testing.T) { opt := WithInsecure(true) - assert.Nil(t, opt(client)) + assert.NoError(t, opt(client)) assert.True(t, client.insecure) } @@ -58,7 +58,7 @@ func TestWithKeyFile(t *testing.T) { keyFile := "testdata/key.pem" opt := WithKeyFile(keyFile) - assert.Nil(t, opt(client)) + assert.NoError(t, opt(client)) assert.Equal(t, keyFile, client.keyFile) } @@ -69,7 +69,7 @@ func TestWithCAFile(t *testing.T) { caFile := "testdata/ca.pem" opt := WithCAFile(caFile) - assert.Nil(t, opt(client)) + assert.NoError(t, opt(client)) assert.Equal(t, caFile, client.caFile) } @@ -79,6 +79,6 @@ func TestWithPlainHTTPLogin(t *testing.T) { opt := WithPlainHTTPLogin(true) - assert.Nil(t, opt(client)) + assert.NoError(t, opt(client)) assert.True(t, client.plainHTTP) } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 393692976..4e48b28a6 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -716,7 +716,7 @@ func TestGetUpgradeServerSideValue(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { serverSideApply, err := getUpgradeServerSideValue(tt.actionServerSideOption, tt.releaseApplyMethod) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, tt.expectedServerSideApply, serverSideApply) }) } diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index d44e7251b..52e9c6b43 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -104,7 +104,7 @@ func TestMetadata(t *testing.T) { is.Equal("foo.yaml", chrt.Name()) is.Equal("1.0.0", chrt.AppVersion()) - is.Equal(nil, chrt.Validate()) + is.NoError(chrt.Validate()) } func TestIsRoot(t *testing.T) { diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 08065499e..9b4483793 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -290,14 +290,14 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) { if tc.ExpectedError { assert.Error(t, err) } else { - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, tc.ExpectedStrategy, dryRunStrategy) } if tc.ExpectedLog != nil { logResult := map[string]string{} err = json.Unmarshal(logBuf.Bytes(), &logResult) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, tc.ExpectedLog.Level, logResult["level"]) assert.Equal(t, tc.ExpectedLog.Msg, logResult["msg"]) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 869b5d202..f618dbb44 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -1357,7 +1358,7 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 } _, err := Render(c, vals) - assert.NotNil(t, err) + require.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) } @@ -1391,7 +1392,7 @@ template: no template "nested_helper.name" associated with template "gotpl"` } _, err := Render(c, vals) - assert.NotNil(t, err) + require.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) } diff --git a/pkg/registry/client_http_test.go b/pkg/registry/client_http_test.go index 1c6751559..3eb74c541 100644 --- a/pkg/registry/client_http_test.go +++ b/pkg/registry/client_http_test.go @@ -17,7 +17,6 @@ limitations under the License. package registry import ( - "errors" "os" "testing" @@ -43,12 +42,12 @@ func (suite *HTTPRegistryClientTestSuite) Test_0_Login() { err := suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth("badverybad", "ohsobad"), LoginOptPlainText(true)) - suite.NotNil(err, "error logging into registry with bad credentials") + suite.Require().Error(err, "error logging into registry with bad credentials") err = suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth(testUsername, testPassword), LoginOptPlainText(true)) - suite.Nil(err, "no error logging into registry with good credentials") + suite.Require().NoError(err, "no error logging into registry with good credentials") } func (suite *HTTPRegistryClientTestSuite) Test_1_Push() { @@ -68,15 +67,15 @@ func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() { // returns content that does not match the expected digest _, err := suite.RegistryClient.Pull(ref) - suite.NotNil(err) - suite.True(errors.Is(err, content.ErrMismatchedDigest)) + suite.Require().Error(err) + suite.ErrorIs(err, content.ErrMismatchedDigest) } func (suite *HTTPRegistryClientTestSuite) Test_5_ImageIndex() { ref := suite.FakeRegistryHost + "/testrepo/image-index:0.1.0" _, err := suite.RegistryClient.Pull(ref) - suite.Nil(err) + suite.Require().NoError(err) } func TestHTTPRegistryClientTestSuite(t *testing.T) { diff --git a/pkg/registry/client_insecure_tls_test.go b/pkg/registry/client_insecure_tls_test.go index 2774f5e6f..8c4e928e4 100644 --- a/pkg/registry/client_insecure_tls_test.go +++ b/pkg/registry/client_insecure_tls_test.go @@ -41,12 +41,12 @@ func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() { err := suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth("badverybad", "ohsobad"), LoginOptInsecure(true)) - suite.NotNil(err, "error logging into registry with bad credentials") + suite.Require().Error(err, "error logging into registry with bad credentials") err = suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth(testUsername, testPassword), LoginOptInsecure(true)) - suite.Nil(err, "no error logging into registry with good credentials") + suite.Require().NoError(err, "no error logging into registry with good credentials") } func (suite *InsecureTLSRegistryClientTestSuite) Test_1_Push() { @@ -65,11 +65,11 @@ func (suite *InsecureTLSRegistryClientTestSuite) Test_4_Logout() { err := suite.RegistryClient.Logout("this-host-aint-real:5000") if err != nil { // credential backend for mac generates an error - suite.NotNil(err, "failed to delete the credential for this-host-aint-real:5000") + suite.Require().Error(err, "failed to delete the credential for this-host-aint-real:5000") } err = suite.RegistryClient.Logout(suite.DockerRegistryHost) - suite.Nil(err, "no error logging out of registry") + suite.Require().NoError(err, "no error logging out of registry") } func TestInsecureTLSRegistryClientTestSuite(t *testing.T) { diff --git a/pkg/registry/client_tls_test.go b/pkg/registry/client_tls_test.go index ddeeb3b66..2bea0377f 100644 --- a/pkg/registry/client_tls_test.go +++ b/pkg/registry/client_tls_test.go @@ -43,26 +43,26 @@ func (suite *TLSRegistryClientTestSuite) Test_0_Login() { err := suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth("badverybad", "ohsobad"), LoginOptTLSClientConfig(tlsCert, tlsKey, tlsCA)) - suite.NotNil(err, "error logging into registry with bad credentials") + suite.Require().Error(err, "error logging into registry with bad credentials") err = suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth(testUsername, testPassword), LoginOptTLSClientConfig(tlsCert, tlsKey, tlsCA)) - suite.Nil(err, "no error logging into registry with good credentials") + suite.Require().NoError(err, "no error logging into registry with good credentials") } func (suite *TLSRegistryClientTestSuite) Test_1_Login() { err := suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth("badverybad", "ohsobad"), LoginOptTLSClientConfigFromConfig(&tls.Config{})) - suite.NotNil(err, "error logging into registry with bad credentials") + suite.Require().Error(err, "error logging into registry with bad credentials") // Create a *tls.Config from tlsCert, tlsKey, and tlsCA. cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) - suite.Nil(err, "error loading x509 key pair") + suite.Require().NoError(err, "error loading x509 key pair") rootCAs := x509.NewCertPool() caCert, err := os.ReadFile(tlsCA) - suite.Nil(err, "error reading CA certificate") + suite.Require().NoError(err, "error reading CA certificate") rootCAs.AppendCertsFromPEM(caCert) conf := &tls.Config{ Certificates: []tls.Certificate{cert}, @@ -72,7 +72,7 @@ func (suite *TLSRegistryClientTestSuite) Test_1_Login() { err = suite.RegistryClient.Login(suite.DockerRegistryHost, LoginOptBasicAuth(testUsername, testPassword), LoginOptTLSClientConfigFromConfig(conf)) - suite.Nil(err, "no error logging into registry with good credentials") + suite.Require().NoError(err, "no error logging into registry with good credentials") } func (suite *TLSRegistryClientTestSuite) Test_1_Push() { @@ -91,11 +91,11 @@ func (suite *TLSRegistryClientTestSuite) Test_4_Logout() { err := suite.RegistryClient.Logout("this-host-aint-real:5000") if err != nil { // credential backend for mac generates an error - suite.NotNil(err, "failed to delete the credential for this-host-aint-real:5000") + suite.Require().Error(err, "failed to delete the credential for this-host-aint-real:5000") } err = suite.RegistryClient.Logout(suite.DockerRegistryHost) - suite.Nil(err, "no error logging out of registry") + suite.Require().NoError(err, "no error logging out of registry") } func TestTLSRegistryClientTestSuite(t *testing.T) { diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index b0c08fa3c..9b9f51ca0 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -107,26 +107,26 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { TLSClientConfig: tlsConf, }, } - suite.Nil(err, "no error loading tls config") + suite.Require().NoError(err, "no error loading tls config") opts = append(opts, ClientOptHTTPClient(httpClient)) } else { opts = append(opts, ClientOptPlainHTTP()) } suite.RegistryClient, err = NewClient(opts...) - suite.Nil(err, "no error creating registry client") + suite.Require().NoError(err, "no error creating registry client") // create htpasswd file (w BCrypt, which is required) pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) - suite.Nil(err, "no error generating bcrypt password for test htpasswd file") + suite.Require().NoError(err, "no error generating bcrypt password for test htpasswd file") htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename) err = os.WriteFile(htpasswdPath, fmt.Appendf(nil, "%s:%s\n", testUsername, string(pwBytes)), 0644) - suite.Nil(err, "no error creating test htpasswd file") + suite.Require().NoError(err, "no error creating test htpasswd file") // Registry config config := &configuration.Configuration{} ln, err := net.Listen("tcp", "127.0.0.1:0") - suite.Nil(err, "no error finding free port for test registry") + suite.Require().NoError(err, "no error finding free port for test registry") defer func() { _ = ln.Close() }() // Change the registry host to another host which is not localhost. @@ -159,7 +159,7 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { } } suite.dockerRegistry, err = registry.NewRegistry(context.Background(), config) - suite.Nil(err, "no error creating test registry") + suite.Require().NoError(err, "no error creating test registry") suite.FakeRegistryHost = initFakeRegistryTestServer() suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() @@ -386,61 +386,61 @@ func testPush(suite *TestRegistry) { // Bad bytes ref := suite.DockerRegistryHost + "/testrepo/testchart:1.2.3" _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime)) - suite.NotNil(err, "error pushing non-chart bytes") + suite.Require().Error(err, "error pushing non-chart bytes") // Load a test chart chartData, err := os.ReadFile("../repo/v1/repotest/testdata/examplechart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") // non-strict ref (chart name) ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) - suite.NotNil(err, "error pushing non-strict ref (bad basename)") + suite.Require().Error(err, "error pushing non-strict ref (bad basename)") // non-strict ref (chart name), with strict mode disabled _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) - suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") + suite.Require().NoError(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") // non-strict ref (chart version) ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) - suite.NotNil(err, "error pushing non-strict ref (bad tag)") + suite.Require().Error(err, "error pushing non-strict ref (bad tag)") // non-strict ref (chart version), with strict mode disabled _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) - suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") + suite.Require().NoError(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") // basic push, good ref chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) - suite.Nil(err, "no error pushing good ref") + suite.Require().NoError(err, "no error pushing good ref") _, err = suite.RegistryClient.Pull(ref) - suite.Nil(err, "no error pulling a simple chart") + suite.Require().NoError(err, "no error pulling a simple chart") // Load another test chart chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") // Load prov file provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") - suite.Nil(err, "no error loading test prov") + suite.Require().NoError(err, "no error loading test prov") // push with prov ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime)) - suite.Nil(err, "no error pushing good ref with prov") + suite.Require().NoError(err, "no error pushing good ref with prov") _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.Nil(err, "no error pulling a simple chart") + suite.Require().NoError(err, "no error pulling a simple chart") // Validate the output // Note: these digests/sizes etc may change if the test chart/prov files are modified, @@ -470,50 +470,50 @@ func testPull(suite *TestRegistry) { // bad/missing ref ref := suite.DockerRegistryHost + "/testrepo/no-existy:1.2.3" _, err := suite.RegistryClient.Pull(ref) - suite.NotNil(err, "error on bad/missing ref") + suite.Require().Error(err, "error on bad/missing ref") // Load test chart (to build ref pushed in previous test) chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) // Simple pull, chart only _, err = suite.RegistryClient.Pull(ref) - suite.Nil(err, "no error pulling a simple chart") + suite.Require().NoError(err, "no error pulling a simple chart") // Simple pull with prov (no prov uploaded) _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.NotNil(err, "error pulling a chart with prov when no prov exists") + suite.Require().Error(err, "error pulling a chart with prov when no prov exists") // Simple pull with prov, ignoring missing prov _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true), PullOptIgnoreMissingProv(true)) - suite.Nil(err, + suite.Require().NoError(err, "no error pulling a chart with prov when no prov exists, ignoring missing") // Load test chart (to build ref pushed in previous test) chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) // Load prov file provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") - suite.Nil(err, "no error loading test prov") + suite.Require().NoError(err, "no error loading test prov") // no chart and no prov causes error _, err = suite.RegistryClient.Pull(ref, PullOptWithChart(false), PullOptWithProv(false)) - suite.NotNil(err, "error on both no chart and no prov") + suite.Require().Error(err, "error on both no chart and no prov") // full pull with chart and prov result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.Require().Nil(err, "no error pulling a chart with prov") + suite.Require().NoError(err, "no error pulling a chart with prov") // Validate the output // Note: these digests/sizes etc may change if the test chart/prov files are modified, @@ -548,13 +548,13 @@ func testPull(suite *TestRegistry) { func testTags(suite *TestRegistry) { // Load test chart (to build ref pushed in previous test) chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") + suite.Require().NoError(err, "no error loading test chart") meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") + suite.Require().NoError(err, "no error extracting chart meta") ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name) // Query for tags and validate length tags, err := suite.RegistryClient.Tags(ref) - suite.Nil(err, "no error retrieving tags") + suite.Require().NoError(err, "no error retrieving tags") suite.Equal(1, len(tags)) } diff --git a/pkg/repo/v1/repotest/tlsconfig.go b/pkg/repo/v1/repotest/tlsconfig.go index 3ea7338ff..d579f8054 100644 --- a/pkg/repo/v1/repotest/tlsconfig.go +++ b/pkg/repo/v1/repotest/tlsconfig.go @@ -35,8 +35,7 @@ func MakeTestTLSConfig(t *testing.T, path string) *tls.Config { tlsutil.WithCertKeyPairFiles(pub, priv), tlsutil.WithCAFile(ca), ) - //require.Nil(t, err, err.Error()) - require.Nil(t, err) + require.NoError(t, err) tlsConf.ServerName = "helm.sh" From 277d9702555532d13426119d31c70fffb389d589 Mon Sep 17 00:00:00 2001 From: Gagan H R Date: Tue, 28 Apr 2026 22:32:21 +0530 Subject: [PATCH 042/132] fix: adds topLevel permissions to improve openSSF scores Signed-off-by: Gagan H R --- .github/workflows/codeql-analysis.yml | 7 ++++--- .github/workflows/stale.yaml | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 972602fea..51ba3ed3f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,14 +24,15 @@ on: schedule: - cron: '29 6 * * 6' -permissions: - contents: read - security-events: write +permissions: {} jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + contents: read + security-events: write strategy: fail-fast: false diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 7d41280ad..9b0c29952 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -3,9 +3,14 @@ on: schedule: - cron: "0 0 * * *" +permissions: {} + jobs: stale: runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: From 081c6dff537087f52ec6e470d8986439e24e8e33 Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Thu, 30 Apr 2026 14:23:01 +0100 Subject: [PATCH 043/132] Upgrade kstatus to 1.2 and controller-runtime to 0.24 Signed-off-by: Matheus Pimenta --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bcddd6986..182898384 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.1.0 + github.com/fluxcd/cli-utils v1.2.0 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -48,7 +48,7 @@ require ( k8s.io/klog/v2 v2.140.0 k8s.io/kubectl v0.36.0 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/controller-runtime v0.24.0 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 9b7f40f94..9d9e3fee4 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.1.0 h1:P2oULlj4aNSqjBGcWOCQS+TOS5ZSyoJMy1zYCpqsYus= -github.com/fluxcd/cli-utils v1.1.0/go.mod h1:+ipwad8nfETe+VB3SMgrDv6m0mqA/KQSj2wyn8Y7vmo= +github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI= +github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -509,8 +509,8 @@ k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0x k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= +sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= From 73f71bceae2674d41fb2f082ee4fa5a842b613cc Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 30 Apr 2026 14:35:16 +0100 Subject: [PATCH 044/132] fix(registry): remove pre-Go-1.20 transport cloner fallback Helm now requires Go 1.26 (#32078); the cloner[T] type-assertion fallback in transport.go was a defensive shim for Go versions before http.Transport.Clone() existed. The fallback path is unreachable on supported Go versions. Refs: #31386 Signed-off-by: Ogulcan Aydogan --- pkg/registry/transport.go | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/pkg/registry/transport.go b/pkg/registry/transport.go index f039a8159..e4177efb3 100644 --- a/pkg/registry/transport.go +++ b/pkg/registry/transport.go @@ -52,22 +52,10 @@ type LoggingTransport struct { // NewTransport creates and returns a new instance of LoggingTransport func NewTransport(debug bool) *retry.Transport { - type cloner[T any] interface { - Clone() T - } - - // try to copy (clone) the http.DefaultTransport so any mutations we - // perform on it (e.g. TLS config) are not reflected globally - // follow https://github.com/golang/go/issues/39299 for a more elegant - // solution in the future + // clone http.DefaultTransport so mutations (e.g. TLS config) are not + // reflected globally transport := http.DefaultTransport - if t, ok := transport.(cloner[*http.Transport]); ok { - transport = t.Clone() - } else if t, ok := transport.(cloner[http.RoundTripper]); ok { - // this branch will not be used with go 1.20, it was added - // optimistically to try to clone if the http.DefaultTransport - // implementation changes, still the Clone method in that case - // might not return http.RoundTripper... + if t, ok := transport.(*http.Transport); ok { transport = t.Clone() } if debug { From e61bbfbfff41958b0ba1984e4d6799fe131f325e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:34:33 +0000 Subject: [PATCH 045/132] chore(deps): bump github.com/Masterminds/semver/v3 from 3.4.0 to 3.5.0 Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/Masterminds/semver/releases) - [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md) - [Commits](https://github.com/Masterminds/semver/compare/v3.4.0...v3.5.0) --- updated-dependencies: - dependency-name: github.com/Masterminds/semver/v3 dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 182898384..323d6eedb 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 github.com/BurntSushi/toml v1.6.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/semver/v3 v3.5.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/vcs v1.13.3 diff --git a/go.sum b/go.sum index 9d9e3fee4..d47661f15 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= From 58e8ffdc3302260b1b55718c9b72c6f169a76ee0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 00:58:20 +0000 Subject: [PATCH 046/132] chore(deps): bump github/codeql-action from 4.35.2 to 4.35.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.35.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51ba3ed3f..6714fc8de 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 41e2f1254..5d78ddd61 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: sarif_file: results.sarif From 12f2c41c0d7a74739c58a5995cbbb3125d9247e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 01:01:06 +0000 Subject: [PATCH 047/132] chore(deps): bump github.com/distribution/distribution/v3 Bumps [github.com/distribution/distribution/v3](https://github.com/distribution/distribution) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/distribution/distribution/releases) - [Commits](https://github.com/distribution/distribution/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: github.com/distribution/distribution/v3 dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 323d6eedb..40c02c7e1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/cyphar/filepath-securejoin v0.6.1 - github.com/distribution/distribution/v3 v3.1.0 + github.com/distribution/distribution/v3 v3.1.1 github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 diff --git a/go.sum b/go.sum index d47661f15..f1a2ca9f8 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.1.0 h1:u1v788HreKTLGdNY6s7px8Exgrs9mZ9UrCDjSrpCM8g= -github.com/distribution/distribution/v3 v3.1.0/go.mod h1:73BuF5/ziMHNVt7nnL1roYpH4Eg/FgUlKZm3WryIx/o= +github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0= +github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= From 075c096afec70155bc43ac3587a119df1ae5fcc6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 29 Sep 2025 10:00:19 -0600 Subject: [PATCH 048/132] chore: replace mitchellh/gox with goreleaser Signed-off-by: Terry Howe --- .goreleaser.yml | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 11 +++---- 2 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 .goreleaser.yml diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 000000000..264a4f206 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,78 @@ +project_name: helm + +before: + hooks: + - go mod tidy + +dist: _dist +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + - arm + - "386" + - ppc64le + - s390x + - riscv64 + goarm: + - "7" + ignore: + - goos: darwin + goarch: "386" + - goos: darwin + goarch: arm + - goos: darwin + goarch: ppc64le + - goos: darwin + goarch: s390x + - goos: darwin + goarch: riscv64 + - goos: windows + goarch: arm + - goos: windows + goarch: ppc64le + - goos: windows + goarch: s390x + - goos: windows + goarch: riscv64 + main: ./cmd/helm + binary: helm + ldflags: + - "{{ .Env.LDFLAGS }}" + flags: + - -trimpath + dir: . + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}- + {{- title .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: 'checksums.txt' + +snapshot: + name_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' \ No newline at end of file diff --git a/Makefile b/Makefile index a18b83f0d..3d4d8261e 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ GOBIN = $(shell go env GOBIN) ifeq ($(GOBIN),) GOBIN = $(shell go env GOPATH)/bin endif -GOX = $(GOBIN)/gox +GOTOOLCHAIN = $(shell awk '/^go / {print "go" $$2}' go.mod) +GORELEASER = $(GOBIN)/goreleaser GOIMPORTS = $(GOBIN)/goimports ARCH = $(shell go env GOARCH) @@ -162,8 +163,8 @@ gen-test-golden: test-unit # dependencies to the go.mod file. To avoid that we change to a directory # without a go.mod file when downloading the following dependencies -$(GOX): - (cd /; go install github.com/mitchellh/gox@v1.0.2-0.20220701044238-9f712387e2d2) +$(GORELEASER): + (cd /; GOTOOLCHAIN=$(GOTOOLCHAIN) go install github.com/goreleaser/goreleaser@latest) $(GOIMPORTS): (cd /; go install golang.org/x/tools/cmd/goimports@latest) @@ -173,8 +174,8 @@ $(GOIMPORTS): .PHONY: build-cross build-cross: LDFLAGS += -extldflags "-static" -build-cross: $(GOX) - GOFLAGS="-trimpath" CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm +build-cross: $(GORELEASER) + LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean .PHONY: dist dist: From e7bea8513c30475664919f031774e18fecdf1f66 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 13:02:15 -0600 Subject: [PATCH 049/132] remove GOTOOLCHAIN Signed-off-by: Terry Howe --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 3d4d8261e..85fae12b2 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,6 @@ GOBIN = $(shell go env GOBIN) ifeq ($(GOBIN),) GOBIN = $(shell go env GOPATH)/bin endif -GOTOOLCHAIN = $(shell awk '/^go / {print "go" $$2}' go.mod) GORELEASER = $(GOBIN)/goreleaser GOIMPORTS = $(GOBIN)/goimports ARCH = $(shell go env GOARCH) @@ -164,7 +163,7 @@ gen-test-golden: test-unit # without a go.mod file when downloading the following dependencies $(GORELEASER): - (cd /; GOTOOLCHAIN=$(GOTOOLCHAIN) go install github.com/goreleaser/goreleaser@latest) + (cd /; go install github.com/goreleaser/goreleaser@latest) $(GOIMPORTS): (cd /; go install golang.org/x/tools/cmd/goimports@latest) From e368f170af8a200e672adac5f765b8101db0c8fa Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 13:30:27 -0600 Subject: [PATCH 050/132] update configuration to v2 Signed-off-by: Terry Howe --- .goreleaser.yml | 13 +++++++------ Makefile | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 264a4f206..5ac434dda 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + project_name: helm before: @@ -50,7 +52,7 @@ builds: dir: . archives: - - format: tar.gz + - id: default # this name template makes the OS and Arch compatible with the results of uname. name_template: >- {{ .ProjectName }}- @@ -59,16 +61,15 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives - format_overrides: - - goos: windows - format: zip + files: + - LICENSE + - README.md checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: sort: asc diff --git a/Makefile b/Makefile index 85fae12b2..022320d1f 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,8 @@ gen-test-golden: test-unit # without a go.mod file when downloading the following dependencies $(GORELEASER): - (cd /; go install github.com/goreleaser/goreleaser@latest) + echo go install github.com/goreleaser/goreleaser/v2@latest + (cd /; go install github.com/goreleaser/goreleaser/v2@latest) $(GOIMPORTS): (cd /; go install golang.org/x/tools/cmd/goimports@latest) From a9659b07e3eec20ab5b964fddae05f51f478f704 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 13:49:24 -0600 Subject: [PATCH 051/132] fix artifact directory Signed-off-by: Terry Howe --- .goreleaser.yml => .goreleaser.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename .goreleaser.yml => .goreleaser.yaml (91%) diff --git a/.goreleaser.yml b/.goreleaser.yaml similarity index 91% rename from .goreleaser.yml rename to .goreleaser.yaml index 5ac434dda..a81a97845 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yaml @@ -35,6 +35,8 @@ builds: goarch: s390x - goos: darwin goarch: riscv64 + - goos: windows + goarch: "386" - goos: windows goarch: arm - goos: windows @@ -44,7 +46,8 @@ builds: - goos: windows goarch: riscv64 main: ./cmd/helm - binary: helm + no_unique_dist_dir: true + binary: "{{ .Os }}-{{ .Arch }}/helm" ldflags: - "{{ .Env.LDFLAGS }}" flags: @@ -55,7 +58,6 @@ archives: - id: default # this name template makes the OS and Arch compatible with the results of uname. name_template: >- - {{ .ProjectName }}- {{- title .Os }}- {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 From 45336ccd5b2621357e3f785c1fe93627c5990a6e Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 15:02:22 -0600 Subject: [PATCH 052/132] add support for loong64 Signed-off-by: Terry Howe --- .goreleaser.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a81a97845..f4d19a967 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -22,6 +22,7 @@ builds: - ppc64le - s390x - riscv64 + - loong64 goarm: - "7" ignore: @@ -35,6 +36,8 @@ builds: goarch: s390x - goos: darwin goarch: riscv64 + - goos: darwin + goarch: loong64 - goos: windows goarch: "386" - goos: windows @@ -45,6 +48,8 @@ builds: goarch: s390x - goos: windows goarch: riscv64 + - goos: windows + goarch: loong64 main: ./cmd/helm no_unique_dist_dir: true binary: "{{ .Os }}-{{ .Arch }}/helm" From 37284a9211972f7f41a2acc3c3313517596dd4b0 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 15:30:10 -0600 Subject: [PATCH 053/132] fix goreleaser archive Signed-off-by: Terry Howe --- .goreleaser.yaml | 8 ++++++-- Makefile | 8 +------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f4d19a967..e7d93d01c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -63,11 +63,15 @@ archives: - id: default # this name template makes the OS and Arch compatible with the results of uname. name_template: >- - {{- title .Os }}- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} + formats: + - tar.gz + - zip files: - LICENSE - README.md diff --git a/Makefile b/Makefile index 022320d1f..bca7b28d8 100644 --- a/Makefile +++ b/Makefile @@ -179,13 +179,7 @@ build-cross: $(GORELEASER) .PHONY: dist dist: - ( \ - cd _dist && \ - $(DIST_DIRS) cp ../LICENSE {} \; && \ - $(DIST_DIRS) cp ../README.md {} \; && \ - $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ - $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ - ) + LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean .PHONY: fetch-dist fetch-dist: From 5a75279c1a017a60b97bd44986288af7399c6ff8 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 29 Oct 2025 16:03:51 -0600 Subject: [PATCH 054/132] Fix archive name Signed-off-by: Terry Howe --- .github/workflows/release.yml | 1 - .goreleaser.yaml | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 593003192..82a5a208a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,7 +101,6 @@ jobs: - name: Build Helm Binaries run: | - make build-cross make dist checksum VERSION="canary" - name: Upload Binaries diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e7d93d01c..aedd77a1a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -66,9 +66,7 @@ archives: {{- .ProjectName }}- {{- .Version }}- {{- .Os }}- - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} + {{- .Arch }} formats: - tar.gz - zip From eaa09100b9b18175d878b1e114cbe9df2a3f70c2 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 24 Nov 2025 05:20:57 -0700 Subject: [PATCH 055/132] fix: canary build file names Signed-off-by: Terry Howe --- .goreleaser.yaml | 2 +- Makefile | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index aedd77a1a..22bb4e379 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -78,7 +78,7 @@ checksum: name_template: 'checksums.txt' snapshot: - version_template: "{{ incpatch .Version }}-next" + version_template: "{{ if .Env.GORELEASER_CURRENT_TAG }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" changelog: sort: asc diff --git a/Makefile b/Makefile index bca7b28d8..1e339fec4 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,11 @@ build-cross: $(GORELEASER) .PHONY: dist dist: +ifeq ($(VERSION),canary) + GORELEASER_CURRENT_TAG=canary LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean +else LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean +endif .PHONY: fetch-dist fetch-dist: From e49a1dc16eee526928d8928b8d96c01ee513ebd9 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 10:34:04 -0600 Subject: [PATCH 056/132] fix: use index for optional env var in version_template Signed-off-by: Terry Howe --- .goreleaser.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 22bb4e379..c105e1813 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -78,7 +78,7 @@ checksum: name_template: 'checksums.txt' snapshot: - version_template: "{{ if .Env.GORELEASER_CURRENT_TAG }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" + version_template: "{{ if index .Env \"GORELEASER_CURRENT_TAG\" }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" changelog: sort: asc From 93103ce66cb6374d9d7b552802f53b21ea2c2dd1 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 11:25:43 -0600 Subject: [PATCH 057/132] fix: disable goreleaser checksums.txt and restrict zip to windows only Signed-off-by: Terry Howe --- .goreleaser.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c105e1813..f99faced0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -69,13 +69,17 @@ archives: {{- .Arch }} formats: - tar.gz - - zip + format_overrides: + - goos: windows + formats: + - tar.gz + - zip files: - LICENSE - README.md checksum: - name_template: 'checksums.txt' + disable: true snapshot: version_template: "{{ if index .Env \"GORELEASER_CURRENT_TAG\" }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" From 04885dd905b6f8a823733dbc9b9f5cb2843a975f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 11:35:35 -0600 Subject: [PATCH 058/132] fix: pass VERSION as GORELEASER_CURRENT_TAG to preserve v-prefix in archive names Signed-off-by: Terry Howe --- Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 1e339fec4..250deac75 100644 --- a/Makefile +++ b/Makefile @@ -179,11 +179,7 @@ build-cross: $(GORELEASER) .PHONY: dist dist: -ifeq ($(VERSION),canary) - GORELEASER_CURRENT_TAG=canary LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean -else - LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean -endif + GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean .PHONY: fetch-dist fetch-dist: From c075022ce16489f5f7afd45a37b679cf58fa36ea Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 11:50:15 -0600 Subject: [PATCH 059/132] fix: address goreleaser build issues flagged in review Signed-off-by: Terry Howe --- .github/workflows/release.yml | 1 - .goreleaser.yaml | 4 ---- Makefile | 3 +-- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82a5a208a..01df0bda5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,6 @@ jobs: run: | set -eu -o pipefail - make build-cross VERSION="${{ github.ref_name }}" make dist checksum VERSION="${{ github.ref_name }}" - name: Set latest version diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f99faced0..4046ee796 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,10 +2,6 @@ version: 2 project_name: helm -before: - hooks: - - go mod tidy - dist: _dist builds: - env: diff --git a/Makefile b/Makefile index 250deac75..c7fc2f1f0 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,6 @@ gen-test-golden: test-unit # without a go.mod file when downloading the following dependencies $(GORELEASER): - echo go install github.com/goreleaser/goreleaser/v2@latest (cd /; go install github.com/goreleaser/goreleaser/v2@latest) $(GOIMPORTS): @@ -178,7 +177,7 @@ build-cross: $(GORELEASER) LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean .PHONY: dist -dist: +dist: $(GORELEASER) GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean .PHONY: fetch-dist From 82899404a68f3826389bb38cf67bf75085db6b2c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 12:12:38 -0600 Subject: [PATCH 060/132] ci: add fetch-depth 0 to canary checkout for goreleaser Signed-off-by: Terry Howe --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01df0bda5..e50da2387 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + fetch-depth: 0 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" From d199a1a42c04bccb287f2c7d9c3f73b669412e5a Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 12:25:35 -0600 Subject: [PATCH 061/132] chore: remove build-cross dependency from test-acceptance Signed-off-by: Terry Howe --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c7fc2f1f0..57365a025 100644 --- a/Makefile +++ b/Makefile @@ -130,8 +130,7 @@ test-source-headers: @scripts/validate-license.sh .PHONY: test-acceptance -test-acceptance: TARGETS = linux/amd64 -test-acceptance: build build-cross +test-acceptance: build @if [ -d "${ACCEPTANCE_DIR}" ]; then \ cd ${ACCEPTANCE_DIR} && \ ROBOT_RUN_TESTS=$(ACCEPTANCE_RUN_TESTS) ROBOT_HELM_PATH='$(BINDIR)' make acceptance; \ From 64aa46f2f1cf239cf6535c5e847e14dcb933a847 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 19:19:20 -0600 Subject: [PATCH 062/132] build: use goreleaser build with manual archive creation goreleaser v2 has a bug with no_unique_dist_dir where it registers archive tasks for all sub-arch variants even when constraints limit builds to one per arch, causing archive collision errors. Switch dist target to use goreleaser build (binaries only) and create tar.gz/zip archives manually, copying LICENSE and README.md into each platform directory to match the existing archive structure. Add sub-arch constraints (goamd64, goarm64, go386, goriscv64) to ensure only one variant is built per architecture. Signed-off-by: Terry Howe --- .goreleaser.yaml | 32 +++++++++----------------------- Makefile | 13 ++++++++++++- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4046ee796..ab2089882 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,8 +19,16 @@ builds: - s390x - riscv64 - loong64 + goamd64: + - v1 goarm: - "7" + goarm64: + - v8.0 + go386: + - sse2 + goriscv64: + - rva20u64 ignore: - goos: darwin goarch: "386" @@ -55,28 +63,6 @@ builds: - -trimpath dir: . -archives: - - id: default - # this name template makes the OS and Arch compatible with the results of uname. - name_template: >- - {{- .ProjectName }}- - {{- .Version }}- - {{- .Os }}- - {{- .Arch }} - formats: - - tar.gz - format_overrides: - - goos: windows - formats: - - tar.gz - - zip - files: - - LICENSE - - README.md - -checksum: - disable: true - snapshot: version_template: "{{ if index .Env \"GORELEASER_CURRENT_TAG\" }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" @@ -85,4 +71,4 @@ changelog: filters: exclude: - '^docs:' - - '^test:' \ No newline at end of file + - '^test:' diff --git a/Makefile b/Makefile index 57365a025..966380805 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,18 @@ build-cross: $(GORELEASER) .PHONY: dist dist: $(GORELEASER) - GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) release --snapshot --clean + GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean + @for platform_dir in _dist/*/; do \ + platform=$$(basename "$$platform_dir"); \ + { [ -f "_dist/$$platform/helm" ] || [ -f "_dist/$$platform/helm.exe" ]; } || continue; \ + cp LICENSE README.md "_dist/$$platform/"; \ + tar czf "_dist/helm-$(VERSION)-$$platform.tar.gz" -C _dist "$$platform/"; \ + done + @for platform_dir in _dist/windows-*/; do \ + [ -d "$$platform_dir" ] || continue; \ + platform=$$(basename "$$platform_dir"); \ + (cd _dist && zip -r "helm-$(VERSION)-$$platform.zip" "$$platform/"); \ + done .PHONY: fetch-dist fetch-dist: From f60ab7c31c81a73b8e0aade5aff41bfc01c08820 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 2 May 2026 19:40:24 -0600 Subject: [PATCH 063/132] fix: add -extldflags -static to dist target to match build-cross Signed-off-by: Terry Howe --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 966380805..f68df780a 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,7 @@ build-cross: $(GORELEASER) LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean .PHONY: dist +dist: LDFLAGS += -extldflags "-static" dist: $(GORELEASER) GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean @for platform_dir in _dist/*/; do \ From 854f7f6b7217dcfe135df9e4652517d3ec9c3913 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Mon, 4 May 2026 13:57:52 -0700 Subject: [PATCH 064/132] fix: fetch logs from all containers in test pods When a test pod contains multiple containers (e.g. Istio/Consul/Vault sidecars), 'helm test --logs' failed with 'a container name must be specified'. This happened because GetPodLogs called the Kubernetes log API without specifying a container name. The fix fetches the pod spec first, then iterates over all containers (init containers + regular containers) and requests logs for each one explicitly. Errors from individual containers are collected and returned together via errors.Join rather than aborting on the first failure. Also fixes a typo: hooksByWight -> hooksByWeight. Closes #6902 Signed-off-by: Sebastien Tardif --- pkg/action/release_testing.go | 53 +++++++++++---- pkg/action/release_testing_test.go | 103 ++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 15 deletions(-) diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index 043a41236..9de4e58f2 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -18,6 +18,7 @@ package action import ( "context" + "errors" "fmt" "io" "slices" @@ -25,6 +26,8 @@ import ( "time" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" @@ -124,9 +127,9 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { return fmt.Errorf("unable to get kubernetes client to fetch pod logs: %w", err) } - hooksByWight := append([]*release.Hook{}, rel.Hooks...) - sort.Stable(hookByWeight(hooksByWight)) - for _, h := range hooksByWight { + hooksByWeight := append([]*release.Hook{}, rel.Hooks...) + sort.Stable(hookByWeight(hooksByWeight)) + for _, h := range hooksByWeight { for _, e := range h.Events { if e == release.HookTest { if slices.Contains(r.Filters[ExcludeNameFilter], h.Name) { @@ -135,20 +138,42 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { if len(r.Filters[IncludeNameFilter]) > 0 && !slices.Contains(r.Filters[IncludeNameFilter], h.Name) { continue } - req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{}) - logReader, err := req.Stream(context.Background()) - if err != nil { - return fmt.Errorf("unable to get pod logs for %s: %w", h.Name, err) - } - - fmt.Fprintf(out, "POD LOGS: %s\n", h.Name) - _, err = io.Copy(out, logReader) - fmt.Fprintln(out) - if err != nil { - return fmt.Errorf("unable to write pod logs for %s: %w", h.Name, err) + if err := r.getContainerLogs(out, client, h.Name); err != nil { + return err } } } } return nil } + +// getContainerLogs fetches logs from all containers (init and regular) in the +// named pod and writes them to out. It continues on per-container errors and +// returns all of them joined at the end. +func (r *ReleaseTesting) getContainerLogs(out io.Writer, client kubernetes.Interface, podName string) error { + pod, err := client.CoreV1().Pods(r.Namespace).Get(context.Background(), podName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("unable to get pod %s: %w", podName, err) + } + + allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...) + + var errs []error + for _, c := range allContainers { + opts := &v1.PodLogOptions{Container: c.Name} + req := client.CoreV1().Pods(r.Namespace).GetLogs(podName, opts) + logReader, err := req.Stream(context.Background()) + if err != nil { + errs = append(errs, fmt.Errorf("unable to get logs for pod %s, container %s: %w", podName, c.Name, err)) + continue + } + + fmt.Fprintf(out, "POD LOGS: %s (%s)\n", podName, c.Name) + _, err = io.Copy(out, logReader) + fmt.Fprintln(out) + if err != nil { + errs = append(errs, fmt.Errorf("unable to write logs for pod %s, container %s: %w", podName, c.Name, err)) + } + } + return errors.Join(errs...) +} diff --git a/pkg/action/release_testing_test.go b/pkg/action/release_testing_test.go index ab35e104a..ea647af80 100644 --- a/pkg/action/release_testing_test.go +++ b/pkg/action/release_testing_test.go @@ -22,10 +22,14 @@ import ( "errors" "io" "os" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeclientset "k8s.io/client-go/kubernetes/fake" "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/kube" @@ -89,7 +93,7 @@ func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) { }, } - require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod logs") + require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod") } func TestReleaseTesting_WaitOptionsPassedDownstream(t *testing.T) { @@ -117,3 +121,100 @@ func TestReleaseTesting_WaitOptionsPassedDownstream(t *testing.T) { // Verify that WaitOptions were passed to GetWaiter is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter") } + +func TestGetContainerLogs_MultipleContainers(t *testing.T) { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "main"}, + {Name: "sidecar"}, + }, + }, + } + + client := fakeclientset.NewClientset(pod) + rt := &ReleaseTesting{Namespace: "default"} + + var buf bytes.Buffer + err := rt.getContainerLogs(&buf, client, "test-pod") + // The fake client doesn't serve real log streams, so we expect + // per-container errors rather than success, but critically it should + // NOT fail with "a container name must be specified". + if err != nil { + assert.NotContains(t, err.Error(), "a container name must be specified") + assert.Contains(t, err.Error(), "container main") + assert.Contains(t, err.Error(), "container sidecar") + } +} + +func TestGetContainerLogs_WithInitContainers(t *testing.T) { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + {Name: "init-setup"}, + }, + Containers: []v1.Container{ + {Name: "main"}, + }, + }, + } + + client := fakeclientset.NewClientset(pod) + rt := &ReleaseTesting{Namespace: "default"} + + var buf bytes.Buffer + err := rt.getContainerLogs(&buf, client, "test-pod") + if err != nil { + // Both init and regular containers should be attempted + assert.Contains(t, err.Error(), "container init-setup") + assert.Contains(t, err.Error(), "container main") + } +} + +func TestGetContainerLogs_PodNotFound(t *testing.T) { + client := fakeclientset.NewClientset() + rt := &ReleaseTesting{Namespace: "default"} + + var buf bytes.Buffer + err := rt.getContainerLogs(&buf, client, "nonexistent-pod") + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to get pod nonexistent-pod") +} + +func TestGetPodLogs_MultiContainerOutput(t *testing.T) { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-test", + Namespace: "default", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "container-a"}, + {Name: "container-b"}, + }, + }, + } + + client := fakeclientset.NewClientset(pod) + rt := &ReleaseTesting{ + Namespace: "default", + Filters: map[string][]string{}, + } + + // Call getContainerLogs directly to test output formatting + var buf bytes.Buffer + _ = rt.getContainerLogs(&buf, client, "multi-test") + output := buf.String() + // Even if logs fail, check that header formatting uses container names + if len(output) > 0 { + assert.True(t, strings.Contains(output, "(container-a)") || strings.Contains(output, "(container-b)")) + } +} From 922558fc1a3b3f2c2b7e8ad0f32bd5bcd46ca70e Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Mon, 4 May 2026 14:04:25 -0700 Subject: [PATCH 065/132] fix: address review feedback - Close log stream after reading (prevents connection/fd leak) - Strengthen tests to assert on output headers rather than error paths - Remove unused import Signed-off-by: Sebastien Tardif --- pkg/action/release_testing.go | 1 + pkg/action/release_testing_test.go | 40 +++++++++++------------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index 9de4e58f2..8cb6ce664 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -170,6 +170,7 @@ func (r *ReleaseTesting) getContainerLogs(out io.Writer, client kubernetes.Inter fmt.Fprintf(out, "POD LOGS: %s (%s)\n", podName, c.Name) _, err = io.Copy(out, logReader) + logReader.Close() fmt.Fprintln(out) if err != nil { errs = append(errs, fmt.Errorf("unable to write logs for pod %s, container %s: %w", podName, c.Name, err)) diff --git a/pkg/action/release_testing_test.go b/pkg/action/release_testing_test.go index ea647af80..dcc708548 100644 --- a/pkg/action/release_testing_test.go +++ b/pkg/action/release_testing_test.go @@ -22,7 +22,6 @@ import ( "errors" "io" "os" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -141,14 +140,10 @@ func TestGetContainerLogs_MultipleContainers(t *testing.T) { var buf bytes.Buffer err := rt.getContainerLogs(&buf, client, "test-pod") - // The fake client doesn't serve real log streams, so we expect - // per-container errors rather than success, but critically it should - // NOT fail with "a container name must be specified". - if err != nil { - assert.NotContains(t, err.Error(), "a container name must be specified") - assert.Contains(t, err.Error(), "container main") - assert.Contains(t, err.Error(), "container sidecar") - } + require.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "POD LOGS: test-pod (main)") + assert.Contains(t, output, "POD LOGS: test-pod (sidecar)") } func TestGetContainerLogs_WithInitContainers(t *testing.T) { @@ -172,11 +167,11 @@ func TestGetContainerLogs_WithInitContainers(t *testing.T) { var buf bytes.Buffer err := rt.getContainerLogs(&buf, client, "test-pod") - if err != nil { - // Both init and regular containers should be attempted - assert.Contains(t, err.Error(), "container init-setup") - assert.Contains(t, err.Error(), "container main") - } + require.NoError(t, err) + output := buf.String() + // Init containers should appear before regular containers + assert.Contains(t, output, "POD LOGS: test-pod (init-setup)") + assert.Contains(t, output, "POD LOGS: test-pod (main)") } func TestGetContainerLogs_PodNotFound(t *testing.T) { @@ -189,7 +184,7 @@ func TestGetContainerLogs_PodNotFound(t *testing.T) { assert.Contains(t, err.Error(), "unable to get pod nonexistent-pod") } -func TestGetPodLogs_MultiContainerOutput(t *testing.T) { +func TestGetContainerLogs_OutputHeaderFormat(t *testing.T) { pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "multi-test", @@ -204,17 +199,12 @@ func TestGetPodLogs_MultiContainerOutput(t *testing.T) { } client := fakeclientset.NewClientset(pod) - rt := &ReleaseTesting{ - Namespace: "default", - Filters: map[string][]string{}, - } + rt := &ReleaseTesting{Namespace: "default"} - // Call getContainerLogs directly to test output formatting var buf bytes.Buffer - _ = rt.getContainerLogs(&buf, client, "multi-test") + err := rt.getContainerLogs(&buf, client, "multi-test") + require.NoError(t, err) output := buf.String() - // Even if logs fail, check that header formatting uses container names - if len(output) > 0 { - assert.True(t, strings.Contains(output, "(container-a)") || strings.Contains(output, "(container-b)")) - } + assert.Contains(t, output, "POD LOGS: multi-test (container-a)") + assert.Contains(t, output, "POD LOGS: multi-test (container-b)") } From e23bf3af53c52185123278e83b7023c102707778 Mon Sep 17 00:00:00 2001 From: Scott Rigby Date: Mon, 4 May 2026 19:46:58 -0400 Subject: [PATCH 066/132] build: Clean up Goreleaser change (#32098) Makefile: - restore dist target - remove unused LDFLAGS added to dist target - remove unused TARGETS var gh release action: - restore build-cross to release and canary-release jobs Signed-off-by: Scott Rigby Co-authored-by: Terry Howe --- .github/workflows/release.yml | 2 ++ Makefile | 23 ++++++++--------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e50da2387..ab8a4a509 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: run: | set -eu -o pipefail + make build-cross VERSION="${{ github.ref_name }}" make dist checksum VERSION="${{ github.ref_name }}" - name: Set latest version @@ -102,6 +103,7 @@ jobs: - name: Build Helm Binaries run: | + make build-cross make dist checksum VERSION="canary" - name: Upload Binaries diff --git a/Makefile b/Makefile index f68df780a..81b149a68 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ BINDIR := $(CURDIR)/bin INSTALL_PATH ?= /usr/local/bin DIST_DIRS := find * -type d -exec -TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64 TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum BINNAME ?= helm @@ -176,20 +175,14 @@ build-cross: $(GORELEASER) LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean .PHONY: dist -dist: LDFLAGS += -extldflags "-static" -dist: $(GORELEASER) - GORELEASER_CURRENT_TAG='$(VERSION)' LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean - @for platform_dir in _dist/*/; do \ - platform=$$(basename "$$platform_dir"); \ - { [ -f "_dist/$$platform/helm" ] || [ -f "_dist/$$platform/helm.exe" ]; } || continue; \ - cp LICENSE README.md "_dist/$$platform/"; \ - tar czf "_dist/helm-$(VERSION)-$$platform.tar.gz" -C _dist "$$platform/"; \ - done - @for platform_dir in _dist/windows-*/; do \ - [ -d "$$platform_dir" ] || continue; \ - platform=$$(basename "$$platform_dir"); \ - (cd _dist && zip -r "helm-$(VERSION)-$$platform.zip" "$$platform/"); \ - done +dist: + ( \ + cd _dist && \ + $(DIST_DIRS) cp ../LICENSE {} \; && \ + $(DIST_DIRS) cp ../README.md {} \; && \ + $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ + $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ + ) .PHONY: fetch-dist fetch-dist: From 9728c5af31c7039e9806037c4e59e46eeb43d79c Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 5 May 2026 18:09:23 +0100 Subject: [PATCH 067/132] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Evans Mungai --- pkg/action/validate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 102259bf1..88771053b 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -95,7 +95,7 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) } - +// Resources that are not found are skipped because they are already deleted and do not need deletion. infoCopy := *info requireUpdate.Append(&infoCopy) return nil From 007713087bd4f381bb8b74f66f35af9e3dafa376 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 5 May 2026 19:02:23 +0100 Subject: [PATCH 068/132] fix: address Copilot review on ownership-verified uninstall - Distinguish unowned resources from those whose ownership could not be verified (e.g. RBAC/network errors). Unverifiable resources are now returned in a separate list with the underlying error so log output and the "kept" summary describe what actually happened. - Only prefix the kept summary with the resource-policy header when there are policy-kept resources, so users with only ownership-skipped resources don't see a misleading header. - Update verifyOwnershipBeforeDelete doc comment to reflect that not-found resources are excluded from all returned lists. - Assert success of Releases.Create in uninstall tests so fixture setup failures are surfaced immediately. Signed-off-by: Evans Mungai --- pkg/action/uninstall.go | 52 ++++++++++++++++++++++++++++++------ pkg/action/uninstall_test.go | 8 +++--- pkg/action/validate.go | 28 ++++++++++++++----- pkg/action/validate_test.go | 12 ++++----- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 73927ed9d..b14305112 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -102,7 +102,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) if err == nil && len(resources) > 0 { - ownedResources, unownedResources, err := verifyOwnershipBeforeDelete(resources, r.Name, r.Namespace) + ownedResources, unownedResources, unverifiableResources, err := verifyOwnershipBeforeDelete(resources, r.Name, r.Namespace) if err == nil { if len(unownedResources) > 0 { u.cfg.Logger().Warn("dry-run: resources would be skipped because they are not owned by this release", @@ -116,6 +116,19 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) } } + if len(unverifiableResources) > 0 { + u.cfg.Logger().Warn("dry-run: resources would be skipped because their ownership could not be verified", + "release", r.Name, + "count", len(unverifiableResources)) + for _, ur := range unverifiableResources { + u.cfg.Logger().Warn("dry-run: would skip resource (ownership could not be verified)", + "kind", ur.Info.Mapping.GroupVersionKind.Kind, + "name", ur.Info.Name, + "namespace", ur.Info.Namespace, + "error", ur.Err) + } + } + if len(ownedResources) > 0 { u.cfg.Logger().Debug("dry-run: resources would be deleted", "release", r.Name, @@ -207,9 +220,6 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) return nil, fmt.Errorf("failed to delete release: %s", name) } - if kept != "" { - kept = "These resources were kept due to the resource policy:\n" + kept - } res.Info = kept if err := waiter.WaitForDelete(deletedResources, u.Timeout); err != nil { @@ -323,8 +333,11 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri filesToKeep, filesToDelete := filterManifestsToKeep(files) var kept strings.Builder - for _, f := range filesToKeep { - fmt.Fprintf(&kept, "[%s] %s\n", f.Head.Kind, f.Head.Metadata.Name) + if len(filesToKeep) > 0 { + kept.WriteString("These resources were kept due to the resource policy:\n") + for _, f := range filesToKeep { + fmt.Fprintf(&kept, "[%s] %s\n", f.Head.Kind, f.Head.Metadata.Name) + } } var builder strings.Builder @@ -339,8 +352,9 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri // Verify ownership before deleting resources var ownedResources, unownedResources kube.ResourceList + var unverifiableResources []unverifiableResource if len(resources) > 0 { - ownedResources, unownedResources, err = verifyOwnershipBeforeDelete(resources, rel.Name, rel.Namespace) + ownedResources, unownedResources, unverifiableResources, err = verifyOwnershipBeforeDelete(resources, rel.Name, rel.Namespace) if err != nil { return nil, "", []error{fmt.Errorf("unable to verify resource ownership: %w", err)} } @@ -354,12 +368,34 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri "namespace", info.Namespace, "release", rel.Name) } - kept.WriteString(fmt.Sprintf("\n%d resource(s) were not deleted because they are not owned by this release:\n", len(unownedResources))) + if kept.Len() > 0 { + kept.WriteString("\n") + } + fmt.Fprintf(&kept, "%d resource(s) were not deleted because they are not owned by this release:\n", len(unownedResources)) for _, info := range unownedResources { fmt.Fprintf(&kept, "[%s] %s\n", info.Mapping.GroupVersionKind.Kind, info.Name) } } + // Log warnings for resources whose ownership could not be verified + if len(unverifiableResources) > 0 { + for _, ur := range unverifiableResources { + u.cfg.Logger().Warn("skipping delete of resource because ownership could not be verified", + "kind", ur.Info.Mapping.GroupVersionKind.Kind, + "name", ur.Info.Name, + "namespace", ur.Info.Namespace, + "release", rel.Name, + "error", ur.Err) + } + if kept.Len() > 0 { + kept.WriteString("\n") + } + fmt.Fprintf(&kept, "%d resource(s) were not deleted because their ownership could not be verified:\n", len(unverifiableResources)) + for _, ur := range unverifiableResources { + fmt.Fprintf(&kept, "[%s] %s: %s\n", ur.Info.Mapping.GroupVersionKind.Kind, ur.Info.Name, ur.Err) + } + } + // Delete only owned resources if len(ownedResources) > 0 { for _, info := range ownedResources { diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 2bf3d3878..3b6d09c33 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -148,7 +148,7 @@ func TestUninstallRelease_Cascade(t *testing.T) { "password": "password" } }` - unAction.cfg.Releases.Create(rel) + require.NoError(t, unAction.cfg.Releases.Create(rel)) // Create dummy resources with Mapping but no Client - this skips ownership verification // (nil Client is treated as owned) and goes directly to delete @@ -208,7 +208,7 @@ metadata: meta.helm.sh/release-namespace: default data: key: value` - config.Releases.Create(rel) + require.NoError(t, config.Releases.Create(rel)) // Create dummy resources with proper ownership metadata labels := map[string]string{ @@ -281,7 +281,7 @@ metadata: meta.helm.sh/release-namespace: default data: key: value` - config.Releases.Create(rel) + require.NoError(t, config.Releases.Create(rel)) // Create dummy resources - one unowned to test logging dummyResources := kube.ResourceList{ @@ -330,7 +330,7 @@ metadata: meta.helm.sh/release-namespace: default data: key: value` - config.Releases.Create(rel) + require.NoError(t, config.Releases.Create(rel)) // Create dummy resources - one unowned to test dry-run logging dummyResources := kube.ResourceList{ diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 88771053b..bc6cb4e9c 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -104,12 +104,26 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN return requireUpdate, err } +// unverifiableResource pairs a resource with the error encountered while attempting +// to verify its ownership (for example, RBAC or network failures). +type unverifiableResource struct { + Info *resource.Info + Err error +} + // verifyOwnershipBeforeDelete checks that resources in the list are owned by the specified release. -// It returns two lists: owned resources (safe to delete) and unowned resources (should skip). -// Resources that are not found are considered owned (already deleted, safe to attempt delete). -func verifyOwnershipBeforeDelete(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, kube.ResourceList, error) { +// It returns three lists: +// - owned: resources confirmed to be owned by the release (safe to delete). +// - unowned: resources that exist but are not owned by the release (should be skipped). +// - unverifiable: resources whose ownership could not be determined due to a fetch +// error (e.g. RBAC or network issues), paired with the underlying error. +// +// Resources that are not found on the server are excluded from all returned lists, +// since they have already been deleted and require no further action. +func verifyOwnershipBeforeDelete(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, kube.ResourceList, []unverifiableResource, error) { var owned kube.ResourceList var unowned kube.ResourceList + var unverifiable []unverifiableResource err := resources.Visit(func(info *resource.Info, err error) error { if err != nil { @@ -127,12 +141,12 @@ func verifyOwnershipBeforeDelete(resources kube.ResourceList, releaseName, relea existing, err := helper.Get(info.Namespace, info.Name) if err != nil { if apierrors.IsNotFound(err) { - // Resource already deleted, skip deletion + // Resource already deleted; nothing to do. return nil } - // Cannot fetch resource (network/permission issue), cannot verify ownership + // Cannot fetch resource (network/permission issue); ownership unverifiable. infoCopy := *info - unowned.Append(&infoCopy) + unverifiable = append(unverifiable, unverifiableResource{Info: &infoCopy, Err: err}) return nil } @@ -150,7 +164,7 @@ func verifyOwnershipBeforeDelete(resources kube.ResourceList, releaseName, relea return nil }) - return owned, unowned, err + return owned, unowned, unverifiable, err } func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error { diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go index b425299f5..1cb1002d6 100644 --- a/pkg/action/validate_test.go +++ b/pkg/action/validate_test.go @@ -234,7 +234,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { owned2 := newDeploymentWithOwner("owned2", "ns-a", labels, annotations) resources := kube.ResourceList{owned1, owned2} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 2) assert.Len(t, unownedList, 0) @@ -246,7 +246,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { unowned := newDeploymentWithOwner("unowned", "ns-a", labels, wrongAnnotations) resources := kube.ResourceList{owned, unowned} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 1) assert.Len(t, unownedList, 1) @@ -259,7 +259,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { missing := newMissingDeployment("missing", "ns-a") resources := kube.ResourceList{missing} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 0) assert.Len(t, unownedList, 0) @@ -270,7 +270,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { noMeta := newDeploymentWithOwner("no-meta", "ns-a", nil, nil) resources := kube.ResourceList{noMeta} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 0) assert.Len(t, unownedList, 1) @@ -281,7 +281,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { otherRelease := newDeploymentWithOwner("other", "ns-a", labels, wrongAnnotations) resources := kube.ResourceList{otherRelease} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 0) assert.Len(t, unownedList, 1) @@ -294,7 +294,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { missing := newMissingDeployment("missing", "ns-a") resources := kube.ResourceList{owned, unowned, missing} - ownedList, unownedList, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) + ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 1) assert.Len(t, unownedList, 1) From 03a65a71f7397620315747ecac4ba60b73f63623 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 5 May 2026 23:13:14 +0100 Subject: [PATCH 069/132] Fix lint errors Signed-off-by: Evans Mungai --- internal/chart/v3/util/dependencies.go | 5 +++-- pkg/action/hooks.go | 4 ++-- pkg/action/uninstall_test.go | 3 +-- pkg/action/validate.go | 2 +- pkg/chart/v2/util/dependencies.go | 5 +++-- pkg/cmd/history.go | 5 +++-- pkg/repo/v1/index.go | 17 +++++++++-------- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index 9c4d8e80f..b31f7eb96 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" chart "helm.sh/helm/v4/internal/chart/v3" @@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any { return nil } cur := data - for i := len(path) - 1; i >= 0; i-- { - cur = map[string]any{path[i]: cur} + for _, v := range slices.Backward(path) { + cur = map[string]any{v: cur} } return cur } diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index a4a8da7a6..e7be37bd8 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -150,8 +150,8 @@ func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook return func() error { // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted // or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook - for i := len(executingHooks) - 1; i >= 0; i-- { - h := executingHooks[i] + for _, v := range slices.Backward(executingHooks) { + h := v if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil { // We log here as we still want to attempt hook resource deletion even if output logging fails. log.Printf("error outputting logs for hook failure: %v", err) diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 3b6d09c33..90f69d639 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -19,7 +19,6 @@ package action import ( "bytes" "errors" - "fmt" "io" "log/slog" "testing" @@ -157,7 +156,7 @@ func TestUninstallRelease_Cascade(t *testing.T) { } failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.DeleteError = fmt.Errorf("Uninstall with cascade failed") + failer.DeleteError = errors.New("Uninstall with cascade failed") failer.DummyResources = dummyResources unAction.cfg.KubeClient = failer _, err := unAction.Run(rel.Name) diff --git a/pkg/action/validate.go b/pkg/action/validate.go index bc6cb4e9c..cce8e32ee 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -95,7 +95,7 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) } -// Resources that are not found are skipped because they are already deleted and do not need deletion. + // Resources that are not found are skipped because they are already deleted and do not need deletion. infoCopy := *info requireUpdate.Append(&infoCopy) return nil diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index abd673f9d..f28a4f4b1 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" "helm.sh/helm/v4/internal/copystructure" @@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any { return nil } cur := data - for i := len(path) - 1; i >= 0; i-- { - cur = map[string]any{path[i]: cur} + for _, v := range slices.Backward(path) { + cur = map[string]any{v: cur} } return cur } diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index 3349b7bc1..7c0f3ecde 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "slices" "strconv" "time" @@ -207,8 +208,8 @@ func getHistory(client *action.History, name string) (releaseHistory, error) { } func getReleaseHistory(rls []*release.Release) (history releaseHistory) { - for i := len(rls) - 1; i >= 0; i-- { - r := rls[i] + for _, v := range slices.Backward(rls) { + r := v c := formatChartName(r.Chart) s := r.Info.Status.String() v := r.Version diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 3dbdf7dfc..825dc2753 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -25,6 +25,7 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" "time" @@ -356,21 +357,21 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { } for name, cvs := range i.Entries { - for idx := len(cvs) - 1; idx >= 0; idx-- { - if cvs[idx] == nil { + for idx, v := range slices.Backward(cvs) { + if v == nil { slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)) cvs = append(cvs[:idx], cvs[idx+1:]...) continue } // When metadata section missing, initialize with no data - if cvs[idx].Metadata == nil { - cvs[idx].Metadata = &chart.Metadata{} + if v.Metadata == nil { + v.Metadata = &chart.Metadata{} } - if cvs[idx].APIVersion == "" { - cvs[idx].APIVersion = chart.APIVersionV1 + if v.APIVersion == "" { + v.APIVersion = chart.APIVersionV1 } - if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil { - slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)) + if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil { + slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, v.Version, source, err)) cvs = append(cvs[:idx], cvs[idx+1:]...) } } From 19b3656306829cae166f484feb4a1319f47844c0 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 6 May 2026 10:35:57 -0700 Subject: [PATCH 070/132] Bump to version v4.2 (#32102) Signed-off-by: George Jenkins --- internal/version/version.go | 2 +- pkg/chart/common/capabilities_test.go | 4 ++-- pkg/cmd/testdata/output/version-short.txt | 2 +- pkg/cmd/testdata/output/version-template.txt | 2 +- pkg/cmd/testdata/output/version.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/version/version.go b/internal/version/version.go index 3daf80893..007f79f16 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -34,7 +34,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "v4.1" + version = "v4.2" // metadata is extra build time data metadata = "" diff --git a/pkg/chart/common/capabilities_test.go b/pkg/chart/common/capabilities_test.go index c9f026cbb..c8d47581c 100644 --- a/pkg/chart/common/capabilities_test.go +++ b/pkg/chart/common/capabilities_test.go @@ -60,8 +60,8 @@ func TestDefaultCapabilities(t *testing.T) { } hv := caps.HelmVersion - if hv.Version != "v4.1" { - t.Errorf("Expected default HelmVersion to be v4.1, got %q", hv.Version) + if hv.Version != "v4.2" { + t.Errorf("Expected default HelmVersion to be v4.2, got %q", hv.Version) } } diff --git a/pkg/cmd/testdata/output/version-short.txt b/pkg/cmd/testdata/output/version-short.txt index 8cf4318fb..2fa2c5705 100644 --- a/pkg/cmd/testdata/output/version-short.txt +++ b/pkg/cmd/testdata/output/version-short.txt @@ -1 +1 @@ -v4.1 +v4.2 diff --git a/pkg/cmd/testdata/output/version-template.txt b/pkg/cmd/testdata/output/version-template.txt index 8fd8b4962..8f2491e54 100644 --- a/pkg/cmd/testdata/output/version-template.txt +++ b/pkg/cmd/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v4.1 \ No newline at end of file +Version: v4.2 \ No newline at end of file diff --git a/pkg/cmd/testdata/output/version.txt b/pkg/cmd/testdata/output/version.txt index 1f4cf4d4a..331c6d5e9 100644 --- a/pkg/cmd/testdata/output/version.txt +++ b/pkg/cmd/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v4.1", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} +version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} From d9b2716be5fbb3309c3176b0c887d91ffb16c048 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 21:33:30 +0000 Subject: [PATCH 071/132] chore(deps): bump github/codeql-action from 4.35.3 to 4.35.4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.3 to 4.35.4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6714fc8de..256d26cc3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 + uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5d78ddd61..16a9a8c13 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: sarif_file: results.sarif From 2cc69251d81ad94e9e0edbea6c31dedf4e2c25d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 21:34:37 +0000 Subject: [PATCH 072/132] chore(deps): bump golang.org/x/crypto from 0.50.0 to 0.51.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.50.0 to 0.51.0. - [Commits](https://github.com/golang/crypto/compare/v0.50.0...v0.51.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 40c02c7e1..4fd2ecde8 100644 --- a/go.mod +++ b/go.mod @@ -35,9 +35,9 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.11.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.50.0 - golang.org/x/term v0.42.0 - golang.org/x/text v0.36.0 + golang.org/x/crypto v0.51.0 + golang.org/x/term v0.43.0 + golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.36.0 k8s.io/apiextensions-apiserver v0.36.0 @@ -157,13 +157,13 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect + golang.org/x/sys v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.43.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index f1a2ca9f8..50373d1f4 100644 --- a/go.sum +++ b/go.sum @@ -382,14 +382,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -400,8 +400,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -430,8 +430,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -439,8 +439,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -448,8 +448,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -458,8 +458,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= From 6620fec5d19f6c86425501ee64f8cb8a8fb3fa3e Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Sat, 9 May 2026 12:17:40 +0530 Subject: [PATCH 073/132] fix(downloader): order DiskCache.Get checks for overlayfs empty dirs Signed-off-by: Sumit Solanki --- pkg/downloader/cache.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/downloader/cache.go b/pkg/downloader/cache.go index 1e23fbfcd..c628ba95d 100644 --- a/pkg/downloader/cache.go +++ b/pkg/downloader/cache.go @@ -59,15 +59,17 @@ func (c *DiskCache) Get(key [sha256.Size]byte, cacheType string) (string, error) if err != nil { return "", err } - // Empty files treated as not exist because there is no content. - if fi.Size() == 0 { - return p, os.ErrNotExist - } // directories should never happen unless something outside helm is operating // on this content. if fi.IsDir() { return p, errors.New("is a directory") } + // Empty files treated as not exist because there is no content. + // IsDir must be checked first: some filesystems (e.g. overlay) report + // directory size as 0. + if fi.Size() == 0 { + return p, os.ErrNotExist + } return p, nil } From 53d5f13f46f7dd555be8bd90616abdc77d470000 Mon Sep 17 00:00:00 2001 From: Sumit Solanki Date: Sat, 9 May 2026 12:28:41 +0530 Subject: [PATCH 074/132] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sumit Solanki --- pkg/downloader/cache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/downloader/cache.go b/pkg/downloader/cache.go index c628ba95d..92d477e49 100644 --- a/pkg/downloader/cache.go +++ b/pkg/downloader/cache.go @@ -64,8 +64,8 @@ func (c *DiskCache) Get(key [sha256.Size]byte, cacheType string) (string, error) if fi.IsDir() { return p, errors.New("is a directory") } - // Empty files treated as not exist because there is no content. - // IsDir must be checked first: some filesystems (e.g. overlay) report + // Empty files are treated as non-existent because there is no content. + // IsDir must be checked first: some filesystems (e.g. overlayfs) report // directory size as 0. if fi.Size() == 0 { return p, os.ErrNotExist From 432fc8a21724cc75e0cd8ec8fd754d2bde42c15f Mon Sep 17 00:00:00 2001 From: box4wangjing Date: Tue, 12 May 2026 01:22:24 +0800 Subject: [PATCH 075/132] refactor: use slices.Backward to simplify the code Signed-off-by: box4wangjing --- internal/chart/v3/util/dependencies.go | 5 +++-- pkg/action/hooks.go | 4 ++-- pkg/chart/v2/util/dependencies.go | 5 +++-- pkg/cmd/history.go | 5 +++-- pkg/repo/v1/index.go | 17 +++++++++-------- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index 9c4d8e80f..b31f7eb96 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" chart "helm.sh/helm/v4/internal/chart/v3" @@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any { return nil } cur := data - for i := len(path) - 1; i >= 0; i-- { - cur = map[string]any{path[i]: cur} + for _, v := range slices.Backward(path) { + cur = map[string]any{v: cur} } return cur } diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index a4a8da7a6..e7be37bd8 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -150,8 +150,8 @@ func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook return func() error { // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted // or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook - for i := len(executingHooks) - 1; i >= 0; i-- { - h := executingHooks[i] + for _, v := range slices.Backward(executingHooks) { + h := v if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil { // We log here as we still want to attempt hook resource deletion even if output logging fails. log.Printf("error outputting logs for hook failure: %v", err) diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index abd673f9d..f28a4f4b1 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" "helm.sh/helm/v4/internal/copystructure" @@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any { return nil } cur := data - for i := len(path) - 1; i >= 0; i-- { - cur = map[string]any{path[i]: cur} + for _, v := range slices.Backward(path) { + cur = map[string]any{v: cur} } return cur } diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index 3349b7bc1..7c0f3ecde 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "slices" "strconv" "time" @@ -207,8 +208,8 @@ func getHistory(client *action.History, name string) (releaseHistory, error) { } func getReleaseHistory(rls []*release.Release) (history releaseHistory) { - for i := len(rls) - 1; i >= 0; i-- { - r := rls[i] + for _, v := range slices.Backward(rls) { + r := v c := formatChartName(r.Chart) s := r.Info.Status.String() v := r.Version diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 3dbdf7dfc..825dc2753 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -25,6 +25,7 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" "time" @@ -356,21 +357,21 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { } for name, cvs := range i.Entries { - for idx := len(cvs) - 1; idx >= 0; idx-- { - if cvs[idx] == nil { + for idx, v := range slices.Backward(cvs) { + if v == nil { slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)) cvs = append(cvs[:idx], cvs[idx+1:]...) continue } // When metadata section missing, initialize with no data - if cvs[idx].Metadata == nil { - cvs[idx].Metadata = &chart.Metadata{} + if v.Metadata == nil { + v.Metadata = &chart.Metadata{} } - if cvs[idx].APIVersion == "" { - cvs[idx].APIVersion = chart.APIVersionV1 + if v.APIVersion == "" { + v.APIVersion = chart.APIVersionV1 } - if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil { - slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)) + if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil { + slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, v.Version, source, err)) cvs = append(cvs[:idx], cvs[idx+1:]...) } } From 2032ec5cbaf168de0fadc59959319e4b580ff72e Mon Sep 17 00:00:00 2001 From: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com> Date: Tue, 12 May 2026 14:12:03 +0000 Subject: [PATCH 076/132] docs: fix 'than'->'that' typo in deprecatedAPIError godoc Signed-off-by: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com> --- internal/chart/v3/lint/rules/deprecations.go | 2 +- pkg/chart/v2/lint/rules/deprecations.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/chart/v3/lint/rules/deprecations.go b/internal/chart/v3/lint/rules/deprecations.go index a607a5fb4..9c73268af 100644 --- a/internal/chart/v3/lint/rules/deprecations.go +++ b/internal/chart/v3/lint/rules/deprecations.go @@ -28,7 +28,7 @@ import ( kscheme "k8s.io/client-go/kubernetes/scheme" ) -// deprecatedAPIError indicates than an API is deprecated in Kubernetes +// deprecatedAPIError indicates that an API is deprecated in Kubernetes type deprecatedAPIError struct { Deprecated string Message string diff --git a/pkg/chart/v2/lint/rules/deprecations.go b/pkg/chart/v2/lint/rules/deprecations.go index 7d5245869..76626659b 100644 --- a/pkg/chart/v2/lint/rules/deprecations.go +++ b/pkg/chart/v2/lint/rules/deprecations.go @@ -28,7 +28,7 @@ import ( kscheme "k8s.io/client-go/kubernetes/scheme" ) -// deprecatedAPIError indicates than an API is deprecated in Kubernetes +// deprecatedAPIError indicates that an API is deprecated in Kubernetes type deprecatedAPIError struct { Deprecated string Message string From ea2343ebeaf7b4b6b115fd1c7b34a159d6a573ff Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Fri, 15 May 2026 07:16:39 -0700 Subject: [PATCH 077/132] fix(repo): use structured slog args in index.go slog.Error on line 157 passes printf-style positional args (%q, %s) instead of key-value pairs. The slog API treats these as unkeyed attributes, producing garbled log output. Two nearby slog.Warn calls wrap fmt.Sprintf unnecessarily. Convert all three calls to use proper structured key-value arguments. Signed-off-by: Sebastien Tardif Assisted-by: Grok/grok-4 --- pkg/repo/v1/index.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 825dc2753..ba747d702 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -154,7 +154,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) // Deprecated: Use index.MustAdd instead. func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { if err := i.MustAdd(md, filename, baseURL, digest); err != nil { - slog.Error("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err) + slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err) } } @@ -359,7 +359,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { for name, cvs := range i.Entries { for idx, v := range slices.Backward(cvs) { if v == nil { - slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)) + slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source) cvs = append(cvs[:idx], cvs[idx+1:]...) continue } @@ -371,7 +371,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { v.APIVersion = chart.APIVersionV1 } if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil { - slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, v.Version, source, err)) + slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err) cvs = append(cvs[:idx], cvs[idx+1:]...) } } From f772ffedc6154f82bc0ac680f9ffa46054112ac0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 21:33:32 +0000 Subject: [PATCH 078/132] chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 256d26cc3..2af5c67cc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 16a9a8c13..77f220426 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: results.sarif From b5a9299eecefe45da13a01188cabac6af4c336d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 21:34:27 +0000 Subject: [PATCH 079/132] chore(deps): bump github.com/fluxcd/cli-utils from 1.2.0 to 1.2.1 Bumps [github.com/fluxcd/cli-utils](https://github.com/fluxcd/cli-utils) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/fluxcd/cli-utils/releases) - [Commits](https://github.com/fluxcd/cli-utils/compare/v1.2.0...v1.2.1) --- updated-dependencies: - dependency-name: github.com/fluxcd/cli-utils dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4fd2ecde8..0ae34e4f6 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.2.0 + github.com/fluxcd/cli-utils v1.2.1 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -48,7 +48,7 @@ require ( k8s.io/klog/v2 v2.140.0 k8s.io/kubectl v0.36.0 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.24.0 + sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 50373d1f4..dc6c788e1 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI= -github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M= +github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY= +github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -509,8 +509,8 @@ k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0x k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= -sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= From 378ceacd9ce239e5b3de1e7ce7c55cf18e16f69b Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Sat, 16 May 2026 15:28:16 +0100 Subject: [PATCH 080/132] fix(upstream): upgrade to cli-utils 1.2.1, controller-runtime 0.24.1 and k8s 1.36.1 Signed-off-by: Matheus Pimenta --- go.mod | 20 ++++++++++---------- go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 4fd2ecde8..8220a5f1c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.2.0 + github.com/fluxcd/cli-utils v1.2.1 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -39,16 +39,16 @@ require ( golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.36.0 - k8s.io/apiextensions-apiserver v0.36.0 - k8s.io/apimachinery v0.36.0 - k8s.io/apiserver v0.36.0 - k8s.io/cli-runtime v0.36.0 - k8s.io/client-go v0.36.0 + k8s.io/api v0.36.1 + k8s.io/apiextensions-apiserver v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/apiserver v0.36.1 + k8s.io/cli-runtime v0.36.1 + k8s.io/client-go v0.36.1 k8s.io/klog/v2 v2.140.0 - k8s.io/kubectl v0.36.0 + k8s.io/kubectl v0.36.1 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.24.0 + sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) @@ -171,7 +171,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.36.0 // indirect + k8s.io/component-base v0.36.1 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index 50373d1f4..544e4205e 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI= -github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M= +github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY= +github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -485,32 +485,32 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= -k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= -k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= -k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= -k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= -k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= -k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE= -k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho= -k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg= -k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA= -k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= -k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= -k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo= -k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= +k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY= +k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw= +k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= +k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= +k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms= -k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo= +k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= +k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= -sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= From 1a1ec6e3cc052f1358ff668eeb439acd3f8e60c1 Mon Sep 17 00:00:00 2001 From: Gregory Man Date: Sun, 17 May 2026 15:11:42 +0300 Subject: [PATCH 081/132] fix: skip non-Pod test hooks in GetPodLogs Before this change, hooks with `helm.sh/hook: test` but Kind != Pod (ConfigMap, Secret, ...) led to `helm test --logs` fail with `unable to get pod `. Signed-off-by: Gregory Man --- pkg/action/release_testing.go | 4 +++ pkg/action/release_testing_test.go | 50 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index 8cb6ce664..5604e8cc4 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -138,6 +138,10 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { if len(r.Filters[IncludeNameFilter]) > 0 && !slices.Contains(r.Filters[IncludeNameFilter], h.Name) { continue } + + if h.Kind != "Pod" { + continue + } if err := r.getContainerLogs(out, client, h.Name); err != nil { return err } diff --git a/pkg/action/release_testing_test.go b/pkg/action/release_testing_test.go index dcc708548..2ec62caf0 100644 --- a/pkg/action/release_testing_test.go +++ b/pkg/action/release_testing_test.go @@ -65,13 +65,20 @@ func TestReleaseTestingGetPodLogs_FilterEvents(t *testing.T) { hooks := []*release.Hook{ { + Kind: "Pod", Name: "event-1", Events: []release.HookEvent{release.HookTest}, }, { + Kind: "Pod", Name: "event-2", Events: []release.HookEvent{release.HookTest}, }, + { + Kind: "ConfigMap", + Name: "event-3", + Events: []release.HookEvent{release.HookTest}, + }, } out := &bytes.Buffer{} @@ -80,6 +87,25 @@ func TestReleaseTestingGetPodLogs_FilterEvents(t *testing.T) { assert.Empty(t, out.String()) } +func TestReleaseTestingGetPodLogs_ExcludeFilter_SkipsPodHook(t *testing.T) { + config := actionConfigFixture(t) + require.NoError(t, config.Init(cli.New().RESTClientGetter(), "", os.Getenv("HELM_DRIVER"))) + client := NewReleaseTesting(config) + client.Filters[ExcludeNameFilter] = []string{"excluded-pod"} + + hooks := []*release.Hook{ + { + Kind: "Pod", + Name: "excluded-pod", + Events: []release.HookEvent{release.HookTest}, + }, + } + + out := &bytes.Buffer{} + require.NoError(t, client.GetPodLogs(out, &release.Release{Hooks: hooks})) + assert.Empty(t, out.String()) +} + func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) { config := actionConfigFixture(t) require.NoError(t, config.Init(cli.New().RESTClientGetter(), "", os.Getenv("HELM_DRIVER"))) @@ -87,6 +113,7 @@ func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) { hooks := []*release.Hook{ { + Kind: "Pod", Name: "event-1", Events: []release.HookEvent{release.HookTest}, }, @@ -95,6 +122,29 @@ func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) { require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod") } +func TestReleaseTestingGetPodLogs_SkipNonPodHooks(t *testing.T) { + config := actionConfigFixture(t) + require.NoError(t, config.Init(cli.New().RESTClientGetter(), "", os.Getenv("HELM_DRIVER"))) + client := NewReleaseTesting(config) + + hooks := []*release.Hook{ + { + Name: "cm-hook", + Kind: "ConfigMap", + Events: []release.HookEvent{release.HookTest}, + }, + { + Name: "secret-hook", + Kind: "Secret", + Events: []release.HookEvent{release.HookTest}, + }, + } + + out := &bytes.Buffer{} + require.NoError(t, client.GetPodLogs(out, &release.Release{Hooks: hooks})) + assert.Empty(t, out.String()) +} + func TestReleaseTesting_WaitOptionsPassedDownstream(t *testing.T) { is := assert.New(t) config := actionConfigFixture(t) From d097d882d473b42c7460d0be674138a3b52c08da Mon Sep 17 00:00:00 2001 From: MrJack <36191829+biagiopietro@users.noreply.github.com> Date: Tue, 19 May 2026 19:17:35 +0200 Subject: [PATCH 082/132] Renamed flag from show-rollback to show-rollback-revision Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com> --- pkg/cmd/history.go | 6 +++--- pkg/cmd/history_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index bb713ae33..e390cafc4 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -50,9 +50,9 @@ The historical release set is printed as a formatted table, e.g: 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully -Use '--show-rollback' to include a column showing the revision that was rolled back to: +Use '--show-rollback-revision' to include a column showing the revision that was rolled back to: - $ helm history angry-bird --show-rollback + $ helm history angry-bird --show-rollback-revision REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully @@ -92,7 +92,7 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f := cmd.Flags() f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") - f.BoolVar(&showRollback, "show-rollback", false, "show the rollback revision column in table output") + f.BoolVar(&showRollback, "show-rollback-revision", false, "show the rollback revision column in table output") bindOutputFlag(cmd, &outfmt) return cmd diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index b25eaba7f..3ec59a99f 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -130,8 +130,8 @@ func TestHistoryWithRollback(t *testing.T) { rels: rels, golden: "output/history-with-rollback-no-flag.txt", }, { - name: "history with rollback revision and --show-rollback flag", - cmd: "history angry-bird --show-rollback", + name: "history with rollback revision and --show-rollback-revision flag", + cmd: "history angry-bird --show-rollback-revision", rels: rels, golden: "output/history-with-rollback.txt", }, { From 979e68fbbd5a932c67b756884880d7641f268632 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 20 May 2026 13:24:39 +0300 Subject: [PATCH 083/132] internal/plugin: remove zero-width spaces from plugin name comment The comment describing allowed plugin name characters contained three U+200B (zero-width space) characters around the '_' and '-' literals. The rendered comment is identical without them; the ZWSP were likely copy-pasted from a rich-text source. Downstream impact: every project that vendors helm.sh/helm/v4 and runs Renovate gets a repo-wide warning on its Dependency Dashboard about hidden Unicode characters. Renovate scans the whole tree (including vendor/) for ZWSP/bidi-override codepoints and cannot be told to skip a path for this specific check. Removing the characters here clears the warning everywhere downstream. Signed-off-by: Aleksei Sviridkin --- internal/plugin/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 132b1739e..789e583ae 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -77,5 +77,5 @@ type Output struct { // validPluginName is a regular expression that validates plugin names. // -// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-. +// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, _ and -. var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$") From e095e690a8b3c31b6dbcb9af02f870cc41fbf18b Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 21 May 2026 07:28:33 -0700 Subject: [PATCH 084/132] fix(provenance): check error return in Digest Return the actual error from io.Copy in Digest() instead of nil. The previous code swallowed the error and returned an empty string as a valid SHA-256 digest, which could silently break chart provenance verification. Also fix encodeRelease() in pkg/storage/driver/util.go: - Close the gzip writer on the w.Write() error path to avoid leaking resources. - Check the error return from gzip.Writer.Close(), which flushes remaining compressed data and can fail. Assisted-by: Grok/xAI Signed-off-by: Sebastien Tardif --- pkg/provenance/sign.go | 2 +- pkg/storage/driver/util.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 45d4fe1a5..9b3e774cb 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -388,7 +388,7 @@ func DigestFile(filename string) (string, error) { func Digest(in io.Reader) (string, error) { hash := crypto.SHA256.New() if _, err := io.Copy(hash, in); err != nil { - return "", nil + return "", err } return hex.EncodeToString(hash.Sum(nil)), nil } diff --git a/pkg/storage/driver/util.go b/pkg/storage/driver/util.go index ca8e23cc2..756731ef6 100644 --- a/pkg/storage/driver/util.go +++ b/pkg/storage/driver/util.go @@ -46,9 +46,12 @@ func encodeRelease(rls *rspb.Release) (string, error) { return "", err } if _, err = w.Write(b); err != nil { + w.Close() + return "", err + } + if err = w.Close(); err != nil { return "", err } - w.Close() return b64.EncodeToString(buf.Bytes()), nil } From ace245b8273e23c9f12993befa535a19ab1bc172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:33:33 +0000 Subject: [PATCH 085/132] chore(deps): bump actions/stale from 10.2.0 to 10.3.0 Bumps [actions/stale](https://github.com/actions/stale) from 10.2.0 to 10.3.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 9b0c29952..bbe339e79 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -12,7 +12,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' From 442e1460b9ec4450a7c12d6b6a59848e7b16d9d1 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 22 May 2026 09:47:08 +0200 Subject: [PATCH 086/132] docs: update version status for v4 stable release Helm v4 shipped on 2025-11-17. Mark it as the current stable release and v3 as support-mode with its end-of-support dates. Signed-off-by: Benoit Tigeot --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37bc8abaa..d581ceac3 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Think of it like apt/yum/homebrew for Kubernetes. ## Helm Development and Stable Versions -Helm v4 is currently under development on the `main` branch. This is unstable and the APIs within the Go SDK and at the command line are changing. -Helm v3 (current stable) is maintained on the `dev-v3` branch. APIs there follow semantic versioning. +Helm v4 is the current stable release, developed on the `main` branch. +Helm v3 is in support mode on the `dev-v3` branch: bug fixes until July 8th 2026, security fixes until November 11th 2026. ## Install @@ -64,7 +64,7 @@ Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/) The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project. -The development of Helm v4 is currently happening on the `main` branch while the development of Helm v3, the stable branch, is happening on the `dev-v3` branch. Changes should be made to the `main` branch prior to being added to the `dev-v3` branch so that all changes are carried along to Helm v4. +Helm v4 development happens on the `main` branch. Helm v3 is in support mode on the `dev-v3` branch and receives only bug and security fixes. ## Community, discussion, contribution, and support From 60665e9035ccab375780ef77934bde2ffa674448 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:33:30 +0000 Subject: [PATCH 087/132] chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2af5c67cc..1c3ea0bf4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 77f220426..c4a0cb196 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: sarif_file: results.sarif From dbb3e353372f6e4abca9155c44d8b9a5f1e97626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:33:35 +0000 Subject: [PATCH 088/132] chore(deps): bump golangci/golangci-lint-action from 9.2.0 to 9.2.1 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.0 to 9.2.1. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...82606bf257cbaff209d206a39f5134f0cfbfd2ee) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 9.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8b64a3be2..734bc682a 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,6 +22,6 @@ jobs: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #pin@9.2.0 + uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #pin@9.2.1 with: version: ${{ env.GOLANGCI_LINT_VERSION }} From 54ae27fd841f33fd979476b1c378fe58fe7ff52f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 22 May 2026 16:09:20 -0600 Subject: [PATCH 089/132] fix(deps): bump golang.org/x/net to v0.55.0 to address GO-2026-5026 Upgrades golang.org/x/net from v0.53.0 to v0.55.0 to fix CVE-2026-39821 (GO-2026-5026), where idna.ToASCII/ToUnicode incorrectly accept Punycode- encoded labels that decode to ASCII-only labels, enabling privilege escalation via hostname check bypass. Coordinated x/ upgrade pulled in by the module graph: - golang.org/x/sys v0.44.0 => v0.45.0 Signed-off-by: Terry Howe --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8220a5f1c..14662ddb4 100644 --- a/go.mod +++ b/go.mod @@ -158,10 +158,10 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index 544e4205e..422a39eb9 100644 --- a/go.sum +++ b/go.sum @@ -400,8 +400,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -430,8 +430,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From e679ec9f041f9fa484f8539efd4b5e665526b883 Mon Sep 17 00:00:00 2001 From: Arnav Nagzirkar Date: Mon, 25 May 2026 22:27:12 -0700 Subject: [PATCH 090/132] ci: enable bidichk linter to prevent invisible Unicode characters Adds the bidichk linter to .golangci.yml to detect dangerous invisible Unicode characters (ZWSP, bidi controls) in source files during CI. Fixes #32137 Signed-off-by: Arnav Nagzirkar --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 1ed3353b4..ca3566f15 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,7 @@ linters: # Keep sorted alphabetically enable: + - bidichk - depguard - dupl - exhaustive From 7510b832149ee739b3ace110f8238e1ffd922cca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 02:05:35 +0000 Subject: [PATCH 091/132] chore(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.51.0 to 0.52.0. - [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.52.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 14662ddb4..2cd535e49 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.11.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.52.0 golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 422a39eb9..30244c99c 100644 --- a/go.sum +++ b/go.sum @@ -382,8 +382,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= From 6dc1c1ccf8066b06f5e30c5bf1d2cdd2924c8c43 Mon Sep 17 00:00:00 2001 From: Austin Abro Date: Wed, 27 May 2026 14:59:21 -0400 Subject: [PATCH 092/132] lower resync period Signed-off-by: Austin Abro --- pkg/kube/statuswait.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index 59c1218ff..29de0af2b 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -68,6 +68,12 @@ func alwaysReady(_ *unstructured.Unstructured) (*status.Result, error) { }, nil } +func getStatusWatcher(dynamicClient dynamic.Interface, mapper meta.RESTMapper) *watcher.DefaultStatusWatcher { + sw := watcher.NewDefaultStatusWatcher(dynamicClient, mapper) + sw.ResyncPeriod = 3 * time.Minute + return sw +} + func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error { if timeout == 0 { timeout = DefaultStatusWatcherTimeout @@ -75,7 +81,7 @@ func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.D ctx, cancel := w.contextWithTimeout(w.watchUntilReadyCtx, timeout) defer cancel() w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) - sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + sw := getStatusWatcher(w.client, w.restMapper) jobSR := helmStatusReaders.NewCustomJobStatusReader(w.restMapper) podSR := helmStatusReaders.NewCustomPodStatusReader(w.restMapper) // We don't want to wait on any other resources as watchUntilReady is only for Helm hooks. @@ -97,7 +103,7 @@ func (w *statusWaiter) Wait(resourceList ResourceList, timeout time.Duration) er ctx, cancel := w.contextWithTimeout(w.waitCtx, timeout) defer cancel() w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) - sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + sw := getStatusWatcher(w.client, w.restMapper) sw.StatusReader = statusreaders.NewStatusReader(w.restMapper, w.readers...) return w.wait(ctx, resourceList, sw) } @@ -109,7 +115,7 @@ func (w *statusWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Dura ctx, cancel := w.contextWithTimeout(w.waitWithJobsCtx, timeout) defer cancel() w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) - sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + sw := getStatusWatcher(w.client, w.restMapper) newCustomJobStatusReader := helmStatusReaders.NewCustomJobStatusReader(w.restMapper) readers := append([]engine.StatusReader(nil), w.readers...) readers = append(readers, newCustomJobStatusReader) @@ -125,7 +131,7 @@ func (w *statusWaiter) WaitForDelete(resourceList ResourceList, timeout time.Dur ctx, cancel := w.contextWithTimeout(w.waitForDeleteCtx, timeout) defer cancel() w.Logger().Debug("waiting for resources to be deleted", "count", len(resourceList), "timeout", timeout) - sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + sw := getStatusWatcher(w.client, w.restMapper) return w.waitForDelete(ctx, resourceList, sw) } From 488c4a805debe0fc1b2777fa1823e079e79a5869 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Fri, 29 May 2026 08:01:54 +0200 Subject: [PATCH 093/132] Fix empty and len testifylint violations across test files Signed-off-by: Matthieu MOREL --- .golangci.yml | 2 -- internal/chart/v3/chart_test.go | 2 +- internal/chart/v3/lint/rules/crds_test.go | 2 +- internal/plugin/loader_test.go | 2 +- pkg/action/action_test.go | 10 +++++----- pkg/action/get_metadata_test.go | 6 +++--- pkg/action/install_test.go | 14 +++++++------- pkg/action/list_test.go | 4 ++-- pkg/action/upgrade_test.go | 2 +- pkg/chart/v2/chart_test.go | 2 +- pkg/chart/v2/lint/rules/crds_test.go | 2 +- pkg/engine/files_test.go | 2 +- pkg/kube/client_test.go | 4 ++-- pkg/registry/registry_test.go | 2 +- 14 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b7afe1f8e..01bd24569 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -99,13 +99,11 @@ linters: testifylint: disable: - - empty - encoded-compare - equal-values - expected-actual - float-compare - go-require - - len - nil-compare - require-error - suite-dont-use-pkg diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index f89030095..d5d066d9b 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -59,7 +59,7 @@ func TestCRDs(t *testing.T) { is := assert.New(t) crds := chrt.CRDs() - is.Equal(2, len(crds)) + is.Len(crds, 2) is.Equal("crds/foo.yaml", crds[0].Name) is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) } diff --git a/internal/chart/v3/lint/rules/crds_test.go b/internal/chart/v3/lint/rules/crds_test.go index e435b8ea3..b80339d7b 100644 --- a/internal/chart/v3/lint/rules/crds_test.go +++ b/internal/chart/v3/lint/rules/crds_test.go @@ -62,5 +62,5 @@ metadata: linter := support.Linter{ChartDir: chartDir} Crds(&linter) - assert.Len(t, linter.Messages, 0) + assert.Empty(t, linter.Messages) } diff --git a/internal/plugin/loader_test.go b/internal/plugin/loader_test.go index b98f87a07..d2d50f7df 100644 --- a/internal/plugin/loader_test.go +++ b/internal/plugin/loader_test.go @@ -208,7 +208,7 @@ 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) + assert.Empty(t, plugs) } func TestLoadAllPluginsDir(t *testing.T) { diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 54b07273b..6a561985f 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1742,7 +1742,7 @@ data: assert.Contains(t, err.Error(), tt.expectedError) } else { assert.NoError(t, err) - assert.Equal(t, len(tt.expectedFiles), len(files)) + assert.Len(t, files, len(tt.expectedFiles)) for expectedFile, expectedContent := range tt.expectedFiles { actualContent, exists := files[expectedFile] @@ -1793,7 +1793,7 @@ data: require.NoError(t, err) // Compare the results - assert.Equal(t, len(originalFiles), len(reconstructed)) + assert.Len(t, reconstructed, len(originalFiles)) for filename, originalContent := range originalFiles { reconstructedContent, exists := reconstructed[filename] assert.True(t, exists, "File %s should exist in reconstructed files", filename) @@ -1830,7 +1830,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, hooks) assert.NotNil(t, buf) - assert.Equal(t, "", notes) + assert.Empty(t, notes) expectedBuf := `--- # Source: yellow/templates/foodpie foodpie: world @@ -1948,7 +1948,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, hooks) assert.NotNil(t, buf) - assert.Equal(t, "", notes) // Notes should be empty for this test + assert.Empty(t, notes) // Notes should be empty for this test // Verify that the post-renderer modifications are present in the output output := buf.String() @@ -1987,7 +1987,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, hooks) assert.NotNil(t, buf) - assert.Equal(t, "", notes) + assert.Empty(t, notes) } func TestRenderResources_PostRenderer_DuplicateResourceInHookAndTemplate(t *testing.T) { diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index 4caa966ab..b488abf64 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -279,9 +279,9 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { // Verify dependencies without aliases assert.Equal(t, "nginx", dep1.Name()) - assert.Equal(t, "", dep1.Alias()) + assert.Empty(t, dep1.Alias()) assert.Equal(t, "postgresql", dep3.Name()) - assert.Equal(t, "", dep3.Alias()) + assert.Empty(t, dep3.Alias()) } func TestGetMetadata_Run_WithAnnotations(t *testing.T) { @@ -485,7 +485,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) { result, err := client.Run(releaseName) require.NoError(t, err) - assert.Equal(t, "", result.AppVersion) + assert.Empty(t, result.AppVersion) } func TestMetadata_FormattedDepNames(t *testing.T) { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 38d692a0b..fa63193bc 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -204,8 +204,8 @@ func TestInstallRelease(t *testing.T) { is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) + is.NotEmpty(res.Manifest) + is.NotEmpty(rel.Manifest) is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") is.Equal(rel.Info.Description, "Install complete") @@ -318,8 +318,8 @@ func TestInstallReleaseWithValues(t *testing.T) { is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) + is.NotEmpty(res.Manifest) + is.NotEmpty(rel.Manifest) is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") is.Equal("Install complete", rel.Info.Description) is.Equal(expectedUserValues, rel.Config) @@ -359,8 +359,8 @@ func TestInstallRelease_WithNotes(t *testing.T) { is.Equal(rel.Hooks[0].Manifest, manifestWithHook) is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) + is.NotEmpty(res.Manifest) + is.NotEmpty(rel.Manifest) is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") is.Equal(rel.Info.Description, "Install complete") @@ -582,7 +582,7 @@ func TestInstallRelease_FailedHooks(t *testing.T) { res, err := releaserToV1Release(resi) is.NoError(err) is.Contains(res.Info.Description, "failed post-install") - is.Equal("", outBuffer.String()) + is.Empty(outBuffer.String()) is.Equal(rcommon.StatusFailed, res.Info.Status) } diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index 643bcea42..19596dbbb 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -65,7 +65,7 @@ func TestList_Empty(t *testing.T) { lister := NewList(actionConfigFixture(t)) list, err := lister.Run() assert.NoError(t, err) - assert.Len(t, list, 0) + assert.Empty(t, list) } func newListFixture(t *testing.T) *List { @@ -166,7 +166,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) { makeMeSomeReleases(t, lister.cfg.Releases) list, err := lister.Run() is.NoError(err) - is.Len(list, 0) + is.Empty(list) lister.Limit = 10 lister.Offset = 1 diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 4e48b28a6..719a94720 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -328,7 +328,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { is.NoError(err) is.Equal(common.StatusDeployed, updatedRes.Info.Status) - is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies") + is.Empty(updatedRes.Chart.Dependencies(), "expected 0 dependencies") expectedValues := map[string]any{ "subchart": map[string]any{ diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index 52e9c6b43..813669366 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -59,7 +59,7 @@ func TestCRDs(t *testing.T) { is := assert.New(t) crds := chrt.CRDs() - is.Equal(2, len(crds)) + is.Len(crds, 2) is.Equal("crds/foo.yaml", crds[0].Name) is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) } diff --git a/pkg/chart/v2/lint/rules/crds_test.go b/pkg/chart/v2/lint/rules/crds_test.go index 228f40a66..759bb54fa 100644 --- a/pkg/chart/v2/lint/rules/crds_test.go +++ b/pkg/chart/v2/lint/rules/crds_test.go @@ -62,5 +62,5 @@ metadata: linter := support.Linter{ChartDir: chartDir} Crds(&linter) - assert.Len(t, linter.Messages, 0) + assert.Empty(t, linter.Messages) } diff --git a/pkg/engine/files_test.go b/pkg/engine/files_test.go index e53263c76..f07f6afeb 100644 --- a/pkg/engine/files_test.go +++ b/pkg/engine/files_test.go @@ -107,5 +107,5 @@ func TestBlankLines(t *testing.T) { as.Len(out, 4) as.Equal("bar", out[0]) - as.Equal("", out[3]) + as.Empty(out[3]) } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index ed871c05a..5f85f5d09 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -550,9 +550,9 @@ func TestUpdate(t *testing.T) { // Special handling for the rollback test case if name == "rollback after failed upgrade with removed resource" { - assert.Len(t, result.Created, 0, "expected 0 resource created, got %d", len(result.Created)) + assert.Empty(t, result.Created, "expected 0 resource created, got %d", len(result.Created)) assert.Len(t, result.Updated, 1, "expected 1 resource updated, got %d", len(result.Updated)) - assert.Len(t, result.Deleted, 0, "expected 0 resource deleted, got %d", len(result.Deleted)) + assert.Empty(t, result.Deleted, "expected 0 resource deleted, got %d", len(result.Deleted)) } else { assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 9b9f51ca0..8a4296591 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -556,5 +556,5 @@ func testTags(suite *TestRegistry) { // Query for tags and validate length tags, err := suite.RegistryClient.Tags(ref) suite.Require().NoError(err, "no error retrieving tags") - suite.Equal(1, len(tags)) + suite.Len(tags, 1) } From 3aa1b742b9228219378ce3d1b978d8d9e30547cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 23:30:01 +0000 Subject: [PATCH 094/132] chore(deps): bump github.com/tetratelabs/wazero from 1.11.0 to 1.12.0 Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.11.0 to 1.12.0. - [Release notes](https://github.com/tetratelabs/wazero/releases) - [Commits](https://github.com/tetratelabs/wazero/compare/v1.11.0...v1.12.0) --- updated-dependencies: - dependency-name: github.com/tetratelabs/wazero dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2cd535e49..77d0e92fc 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - github.com/tetratelabs/wazero v1.11.0 + github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.52.0 golang.org/x/term v0.43.0 diff --git a/go.sum b/go.sum index 30244c99c..a48486be0 100644 --- a/go.sum +++ b/go.sum @@ -309,8 +309,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= From f8abbfd7d4ab7d9cd883fab5d24c8175eaec56b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:57:37 +0000 Subject: [PATCH 095/132] chore(deps): bump github/codeql-action from 4.36.0 to 4.36.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.0 to 4.36.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/7211b7c8077ea37d8641b6271f6a365a22a5fbfa...87557b9c84dde89fdd9b10e88954ac2f4248e463) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1c3ea0bf4..351acf713 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 + uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0 + uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index c4a0cb196..0fe504b87 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: sarif_file: results.sarif From c603c50aa654b23c46c7e248cdf5cd016f0a3fa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:32:47 +0000 Subject: [PATCH 096/132] chore(deps): bump github/codeql-action from 4.36.1 to 4.36.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.1 to 4.36.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/87557b9c84dde89fdd9b10e88954ac2f4248e463...8aad20d150bbac5944a9f9d289da16a4b0d87c1e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 351acf713..008d6a594 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # pinv4.36.1 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 0fe504b87..55a9aec82 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif From 7f855dfe8a1176d41808451aa0e8ab7b11664f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:33:52 +0000 Subject: [PATCH 097/132] chore(deps): bump golang.org/x/text from 0.37.0 to 0.38.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.37.0 to 0.38.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 77d0e92fc..4f317e079 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.52.0 golang.org/x/term v0.43.0 - golang.org/x/text v0.37.0 + golang.org/x/text v0.38.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.36.1 k8s.io/apiextensions-apiserver v0.36.1 @@ -157,13 +157,13 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/mod v0.35.0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index a48486be0..dbc195cc9 100644 --- a/go.sum +++ b/go.sum @@ -388,8 +388,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -412,8 +412,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -448,8 +448,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -458,8 +458,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= From 1019146bb36fde225ee93fdb173d12c91d96b834 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:34:10 +0000 Subject: [PATCH 098/132] chore(deps): bump golang.org/x/term from 0.43.0 to 0.44.0 Bumps [golang.org/x/term](https://github.com/golang/term) from 0.43.0 to 0.44.0. - [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4f317e079..9629f9c00 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.52.0 - golang.org/x/term v0.43.0 + golang.org/x/term v0.44.0 golang.org/x/text v0.38.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.36.1 @@ -161,7 +161,7 @@ require ( golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sys v0.46.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index dbc195cc9..725fcc333 100644 --- a/go.sum +++ b/go.sum @@ -430,8 +430,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -439,8 +439,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From 33b40718860fcb563ef34d0a4af2a1e565fdd108 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:59:28 +0000 Subject: [PATCH 099/132] chore(deps): bump golang.org/x/crypto from 0.52.0 to 0.53.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.52.0 to 0.53.0. - [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.53.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9629f9c00..db1ebd0a4 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.52.0 + golang.org/x/crypto v0.53.0 golang.org/x/term v0.44.0 golang.org/x/text v0.38.0 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 725fcc333..54105cad2 100644 --- a/go.sum +++ b/go.sum @@ -382,8 +382,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= From 74c1702157722e44f72c4a731c652c9e6ed58f83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:33:08 +0000 Subject: [PATCH 100/132] chore(deps): bump oras.land/oras-go/v2 from 2.6.0 to 2.6.1 Bumps [oras.land/oras-go/v2](https://github.com/oras-project/oras-go) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/oras-project/oras-go/releases) - [Commits](https://github.com/oras-project/oras-go/compare/v2.6.0...v2.6.1) --- updated-dependencies: - dependency-name: oras.land/oras-go/v2 dependency-version: 2.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9629f9c00..02ce7d963 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( k8s.io/client-go v0.36.1 k8s.io/klog/v2 v2.140.0 k8s.io/kubectl v0.36.1 - oras.land/oras-go/v2 v2.6.0 + oras.land/oras-go/v2 v2.6.1 sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 diff --git a/go.sum b/go.sum index 725fcc333..153c6dcdb 100644 --- a/go.sum +++ b/go.sum @@ -507,8 +507,8 @@ k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= +oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= From 7058f841af78112f81098e3f434f90d30c43b4fd Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 11 Jun 2026 22:31:18 +0100 Subject: [PATCH 101/132] fix(engine): add debug logging when lookup returns empty (#32205) When lookup cannot find the requested resource (apierrors.IsNotFound), add slog.Debug() calls with structured fields (apiVersion, kind, namespace, name) so that users running helm template --debug can see why lookup returned an empty map instead of silently swallowing the not-found result. Fixes: https://github.com/helm/helm/issues/32101 Signed-off-by: Ogulcan Aydogan --- pkg/engine/lookup_func.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 52b6ffdaf..c5c441c14 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -73,6 +73,12 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. + slog.Debug("lookup: resource not found", + slog.String("apiVersion", apiversion), + slog.String("kind", kind), + slog.String("namespace", namespace), + slog.String("name", name), + ) return map[string]any{}, nil } return map[string]any{}, err @@ -85,6 +91,11 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. + slog.Debug("lookup: resource list not found", + slog.String("apiVersion", apiversion), + slog.String("kind", kind), + slog.String("namespace", namespace), + ) return map[string]any{}, nil } return map[string]any{}, err From c2f1b238a114d6d7a85b37f80fc6975bd48e4c09 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 11 Jun 2026 15:36:42 -0600 Subject: [PATCH 102/132] fix: route registry client output to stdout instead of stderr (#32056) Commands like 'helm registry login', 'helm push', and 'helm pull' were writing success messages ("Login Succeeded", "Pushed:", "Pulled:", "Digest:") to stderr instead of stdout. The root cause was that newDefaultRegistryClient and newRegistryClientWithTLS hard-coded os.Stderr as the registry client writer, ignoring the out io.Writer that main() passes as os.Stdout. Thread out io.Writer through newRegistryClient, newDefaultRegistryClient, and newRegistryClientWithTLS, and update all call sites in pkg/cmd. Fixes #13464 Signed-off-by: Terry Howe --- pkg/cmd/dependency_build.go | 2 +- pkg/cmd/dependency_update.go | 2 +- pkg/cmd/install.go | 2 +- pkg/cmd/package.go | 2 +- pkg/cmd/pull.go | 2 +- pkg/cmd/push.go | 2 +- pkg/cmd/root.go | 16 ++++++++-------- pkg/cmd/show.go | 14 +++++++------- pkg/cmd/template.go | 2 +- pkg/cmd/upgrade.go | 2 +- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/cmd/dependency_build.go b/pkg/cmd/dependency_build.go index b8ac16e60..f572fbae1 100644 --- a/pkg/cmd/dependency_build.go +++ b/pkg/cmd/dependency_build.go @@ -55,7 +55,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { if len(args) > 0 { chartpath = filepath.Clean(args[0]) } - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/dependency_update.go b/pkg/cmd/dependency_update.go index 7f805c37b..a7131e587 100644 --- a/pkg/cmd/dependency_update.go +++ b/pkg/cmd/dependency_update.go @@ -58,7 +58,7 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma if len(args) > 0 { chartpath = filepath.Clean(args[0]) } - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 67e2a9fab..832968ff0 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -143,7 +143,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return compInstall(args, toComplete, client) }, RunE: func(cmd *cobra.Command, args []string) error { - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/package.go b/pkg/cmd/package.go index 14f9c8425..46133d971 100644 --- a/pkg/cmd/package.go +++ b/pkg/cmd/package.go @@ -75,7 +75,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { return err } - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/pull.go b/pkg/cmd/pull.go index bb7a8d1c0..d371db4ad 100644 --- a/pkg/cmd/pull.go +++ b/pkg/cmd/pull.go @@ -65,7 +65,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Version = ">0.0.0-0" } - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/push.go b/pkg/cmd/push.go index 94c5732ff..f32ce92be 100644 --- a/pkg/cmd/push.go +++ b/pkg/cmd/push.go @@ -71,7 +71,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, RunE: func(_ *cobra.Command, args []string) error { registryClient, err := newRegistryClient( - o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password, + out, o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password, ) if err != nil { diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 1fa01ca0e..3203ced25 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -257,7 +257,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg log.Fatal(err) } - registryClient, err := newDefaultRegistryClient(false, "", "") + registryClient, err := newDefaultRegistryClient(out, false, "", "") if err != nil { return nil, err } @@ -401,27 +401,27 @@ func checkForExpiredRepos(repofile string) { } func newRegistryClient( - certFile, keyFile, caFile string, insecureSkipTLSVerify, plainHTTP bool, username, password string, + out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSVerify, plainHTTP bool, username, password string, ) (*registry.Client, error) { if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSVerify { - registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSVerify, username, password) + registryClient, err := newRegistryClientWithTLS(out, certFile, keyFile, caFile, insecureSkipTLSVerify, username, password) if err != nil { return nil, err } return registryClient, nil } - registryClient, err := newDefaultRegistryClient(plainHTTP, username, password) + registryClient, err := newDefaultRegistryClient(out, plainHTTP, username, password) if err != nil { return nil, err } return registryClient, nil } -func newDefaultRegistryClient(plainHTTP bool, username, password string) (*registry.Client, error) { +func newDefaultRegistryClient(out io.Writer, plainHTTP bool, username, password string) (*registry.Client, error) { opts := []registry.ClientOption{ registry.ClientOptDebug(settings.Debug), registry.ClientOptEnableCache(true), - registry.ClientOptWriter(os.Stderr), + registry.ClientOptWriter(out), registry.ClientOptCredentialsFile(settings.RegistryConfig), registry.ClientOptBasicAuth(username, password), } @@ -438,7 +438,7 @@ func newDefaultRegistryClient(plainHTTP bool, username, password string) (*regis } func newRegistryClientWithTLS( - certFile, keyFile, caFile string, insecureSkipTLSVerify bool, username, password string, + out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSVerify bool, username, password string, ) (*registry.Client, error) { tlsConf, err := tlsutil.NewTLSConfig( tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify), @@ -454,7 +454,7 @@ func newRegistryClientWithTLS( registryClient, err := registry.NewClient( registry.ClientOptDebug(settings.Debug), registry.ClientOptEnableCache(true), - registry.ClientOptWriter(os.Stderr), + registry.ClientOptWriter(out), registry.ClientOptCredentialsFile(settings.RegistryConfig), registry.ClientOptHTTPClient(&http.Client{ Transport: &http.Transport{ diff --git a/pkg/cmd/show.go b/pkg/cmd/show.go index d7249c3fe..a92ca75b4 100644 --- a/pkg/cmd/show.go +++ b/pkg/cmd/show.go @@ -84,7 +84,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(_ *cobra.Command, args []string) error { client.OutputFormat = action.ShowAll - err := addRegistryClient(client) + err := addRegistryClient(out, client) if err != nil { return err } @@ -105,7 +105,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(_ *cobra.Command, args []string) error { client.OutputFormat = action.ShowValues - err := addRegistryClient(client) + err := addRegistryClient(out, client) if err != nil { return err } @@ -126,7 +126,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(_ *cobra.Command, args []string) error { client.OutputFormat = action.ShowChart - err := addRegistryClient(client) + err := addRegistryClient(out, client) if err != nil { return err } @@ -147,7 +147,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(_ *cobra.Command, args []string) error { client.OutputFormat = action.ShowReadme - err := addRegistryClient(client) + err := addRegistryClient(out, client) if err != nil { return err } @@ -168,7 +168,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(_ *cobra.Command, args []string) error { client.OutputFormat = action.ShowCRDs - err := addRegistryClient(client) + err := addRegistryClient(out, client) if err != nil { return err } @@ -225,8 +225,8 @@ func runShow(args []string, client *action.Show) (string, error) { return client.Run(cp) } -func addRegistryClient(client *action.Show) error { - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, +func addRegistryClient(out io.Writer, client *action.Show) error { + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index e5789db8b..0e2824ce9 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -85,7 +85,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.KubeVersion = parsedKubeVersion } - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 43e19ab22..9ec4f4ef2 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -105,7 +105,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client.Namespace = settings.Namespace() - registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) From a5552edf9fb0e23b475310d943a2ecd1df5aeafd Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 11 Jun 2026 15:38:02 -0600 Subject: [PATCH 103/132] fix: protect FailingKubeClient.RecordedWaitOptions from data race (#31925) * fix: protect FailingKubeClient.RecordedWaitOptions from concurrent access Add a sync.Mutex to guard the append to RecordedWaitOptions in GetWaiterWithOptions, fixing a data race detected by -race when concurrent goroutines (e.g. upgrade + rollback) both call GetWaiterWithOptions on the same FailingKubeClient instance. Fixes race failures in TestUpgradeRelease_Interrupted_RollbackOnFailure and TestInstallRelease_RollbackOnFailure_Interrupted. Signed-off-by: Terry Howe * fix: extract appendRecordedWaitOptionsLocked helper with defer unlock Signed-off-by: Terry Howe --------- Signed-off-by: Terry Howe Signed-off-by: Terry Howe --- pkg/kube/fake/failing_kube_client.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/kube/fake/failing_kube_client.go b/pkg/kube/fake/failing_kube_client.go index 0f7787f79..75d0c8de1 100644 --- a/pkg/kube/fake/failing_kube_client.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -19,6 +19,7 @@ package fake import ( "io" + "sync" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,6 +50,7 @@ type FailingKubeClient struct { WaitDuration time.Duration // RecordedWaitOptions stores the WaitOptions passed to GetWaiter for testing RecordedWaitOptions []kube.WaitOption + mu sync.Mutex } var _ kube.Interface = &FailingKubeClient{} @@ -158,9 +160,14 @@ func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) return f.GetWaiterWithOptions(ws) } -func (f *FailingKubeClient) GetWaiterWithOptions(ws kube.WaitStrategy, opts ...kube.WaitOption) (kube.Waiter, error) { - // Record the WaitOptions for testing +func (f *FailingKubeClient) appendRecordedWaitOptionsLocked(opts ...kube.WaitOption) { + f.mu.Lock() + defer f.mu.Unlock() f.RecordedWaitOptions = append(f.RecordedWaitOptions, opts...) +} + +func (f *FailingKubeClient) GetWaiterWithOptions(ws kube.WaitStrategy, opts ...kube.WaitOption) (kube.Waiter, error) { + f.appendRecordedWaitOptionsLocked(opts...) waiter, _ := f.PrintingKubeClient.GetWaiterWithOptions(ws, opts...) printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) return &FailingKubeWaiter{ From 1f7869c31f8d96638d238957b20a27d665001d88 Mon Sep 17 00:00:00 2001 From: Mohammad Abdolirad Date: Fri, 5 Jun 2026 00:20:46 +0200 Subject: [PATCH 104/132] fix panic on repeated IsReachable calls - only cache kube client when creation succeeds - add regression test for two failed reachability checks Signed-off-by: Mohammad Abdolirad --- pkg/kube/client.go | 13 ++++++++----- pkg/kube/client_test.go | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 04d634740..a640bde8a 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -235,12 +235,15 @@ func New(getter genericclioptions.RESTClientGetter) *Client { // getKubeClient get or create a new KubernetesClientSet func (c *Client) getKubeClient() (kubernetes.Interface, error) { - var err error - if c.kubeClient == nil { - c.kubeClient, err = c.Factory.KubernetesClientSet() + if c.kubeClient != nil { + return c.kubeClient, nil } - - return c.kubeClient, err + kc, err := c.Factory.KubernetesClientSet() + if err != nil { + return nil, err + } + c.kubeClient = kc + return c.kubeClient, nil } // IsReachable tests connectivity to the cluster. diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index cd00baa94..64fd8282d 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -1416,6 +1416,26 @@ func TestIsReachable(t *testing.T) { } } +func TestIsReachableTwiceAfterClientCreationFailure(t *testing.T) { + refusedErr := errors.New("connection refused") + client := newTestClient(t) + client.Factory = &errorFactory{err: refusedErr} + + assertReachableErr := func(label string, err error) { + t.Helper() + if err == nil { + t.Fatalf("%s: expected error, got nil", label) + } + if !errors.Is(err, refusedErr) { + t.Fatalf("%s: expected error wrapping %v, got %v", label, refusedErr, err) + } + } + + assertReachableErr("first call", client.IsReachable()) + // Second call must return the same underlying error, not panic. + assertReachableErr("second call", client.IsReachable()) +} + func TestIsIncompatibleServerError(t *testing.T) { testCases := map[string]struct { Err error From 2014946e550ed7a667b9ae8cfe15a377a2fe94ca Mon Sep 17 00:00:00 2001 From: Jeaeun Kim Date: Tue, 26 Aug 2025 13:07:00 +0900 Subject: [PATCH 105/132] chore: Improve error reporting for `helm template --debug` with `--show-only` Signed-off-by: Jeaeun Kim --- pkg/cmd/template.go | 7 ++++++- pkg/cmd/template_test.go | 6 ++++++ ...e-with-invalid-template-expr-debug-show-only.txt | 3 +++ .../Chart.yaml | 8 ++++++++ .../README.md | 13 +++++++++++++ .../templates/alpine-pod.yaml | 9 +++++++++ .../values.yaml | 1 + 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/testdata/output/template-with-invalid-template-expr-debug-show-only.txt create mode 100644 pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/Chart.yaml create mode 100644 pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/README.md create mode 100644 pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml create mode 100644 pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/values.yaml diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 0e2824ce9..13eb1da04 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -186,7 +186,12 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { missing = false } if missing { - return fmt.Errorf("could not find template %s in chart", f) + if err != nil && settings.Debug { + // assume the manifest itself is too malformed to be rendered + return err + } else { + return fmt.Errorf("could not find template %s in chart", f) + } } } for _, m := range manifestsToRender { diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 7391781f6..b43151413 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -133,6 +133,12 @@ func TestTemplateCmd(t *testing.T) { wantError: true, golden: "output/template-with-invalid-yaml-debug.txt", }, + { + name: "chart with template with invalid template expression (--debug, --show-only)", + cmd: fmt.Sprintf("template '%s' --debug --show-only %s", "testdata/testcharts/chart-with-template-with-invalid-template-expr", "templates/alpine-pod.yaml"), + wantError: true, + golden: "output/template-with-invalid-template-expr-debug-show-only.txt", + }, { name: "template skip-tests", cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), diff --git a/pkg/cmd/testdata/output/template-with-invalid-template-expr-debug-show-only.txt b/pkg/cmd/testdata/output/template-with-invalid-template-expr-debug-show-only.txt new file mode 100644 index 000000000..89d518bbb --- /dev/null +++ b/pkg/cmd/testdata/output/template-with-invalid-template-expr-debug-show-only.txt @@ -0,0 +1,3 @@ +Error: chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml:7:38 + executing "chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml" at : + invalid value; expected string diff --git a/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/Chart.yaml new file mode 100644 index 000000000..83541d4dd --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +description: Deploy a basic Alpine Linux pod +home: https://helm.sh/helm +name: chart-with-template-with-invalid-template-expr +sources: + - https://github.com/helm/helm +version: 0.1.0 +type: application diff --git a/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/README.md b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/README.md new file mode 100644 index 000000000..fcf7ee017 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml new file mode 100644 index 000000000..bdc0fe39b --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/templates/alpine-pod.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" +spec: + containers: + - name: {{ .Values.nonExistentKey | b64enc }} + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/values.yaml new file mode 100644 index 000000000..807e12aea --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-template-expr/values.yaml @@ -0,0 +1 @@ +Name: my-alpine From ea847fef95702ca53ec5ab3c627c696ea1488598 Mon Sep 17 00:00:00 2001 From: Jeaeun Kim Date: Wed, 27 Aug 2025 20:46:49 +0900 Subject: [PATCH 106/132] chore: store err separately for clarity Signed-off-by: Jeaeun Kim --- pkg/cmd/template.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 13eb1da04..556822f0d 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -113,6 +113,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } return err } + savedErr := err // We ignore a potential error here because, when the --debug flag was specified, // we always want to print the YAML, even if it is not valid. The error is still returned afterwards. @@ -186,9 +187,9 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { missing = false } if missing { - if err != nil && settings.Debug { + if savedErr != nil && settings.Debug { // assume the manifest itself is too malformed to be rendered - return err + return savedErr } else { return fmt.Errorf("could not find template %s in chart", f) } @@ -202,7 +203,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } - return err + return savedErr }, } From 99baa2edd493af15ffd98f39808fac5c4a9b12f7 Mon Sep 17 00:00:00 2001 From: Jeaeun Kim Date: Sun, 8 Mar 2026 22:05:31 +0900 Subject: [PATCH 107/132] chore: fix lint Signed-off-by: Jeaeun Kim --- pkg/cmd/template.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 556822f0d..e56fefc17 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -190,9 +190,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if savedErr != nil && settings.Debug { // assume the manifest itself is too malformed to be rendered return savedErr - } else { - return fmt.Errorf("could not find template %s in chart", f) } + return fmt.Errorf("could not find template %s in chart", f) } } for _, m := range manifestsToRender { From 9c1d5a3b249c6684bfe902455a61e728bbf7dcf6 Mon Sep 17 00:00:00 2001 From: Jeaeun Kim Date: Mon, 18 May 2026 00:22:23 +0900 Subject: [PATCH 108/132] chore: rename savedErr to clear its specific purpose Signed-off-by: Jeaeun Kim --- pkg/cmd/template.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index e56fefc17..bb364231b 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -113,7 +113,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } return err } - savedErr := err + installErr := err // We ignore a potential error here because, when the --debug flag was specified, // we always want to print the YAML, even if it is not valid. The error is still returned afterwards. @@ -187,9 +187,9 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { missing = false } if missing { - if savedErr != nil && settings.Debug { + if installErr != nil && settings.Debug { // assume the manifest itself is too malformed to be rendered - return savedErr + return installErr } return fmt.Errorf("could not find template %s in chart", f) } @@ -202,7 +202,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } - return savedErr + return installErr }, } From ed76a36ab9b4c8f5a56049aa201dd8ce04836ecb Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 15:37:08 +1200 Subject: [PATCH 109/132] refactor(cmd/helm): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- cmd/helm/helm_test.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index a9362f772..05d05dece 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,13 +18,13 @@ package main import ( "bytes" - "errors" "os" "os/exec" "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCliPluginExitCode(t *testing.T) { @@ -62,20 +62,13 @@ func TestCliPluginExitCode(t *testing.T) { err := cmd.Run() exiterr := &exec.ExitError{} - ok := errors.As(err, &exiterr) - if !ok { - t.Fatalf("Unexpected error type returned by os.Exit: %T", err) - } + require.ErrorAs(t, err, &exiterr) assert.Empty(t, stdout.String()) 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()) - } + assert.Equal(t, expectedStderr, stderr.String()) - if exiterr.ExitCode() != 43 { - t.Errorf("Expected exit code 43: Got %d", exiterr.ExitCode()) - } + assert.Equal(t, 43, exiterr.ExitCode()) } } From f4d713abb210230b3935c4a9685f1f34a8330970 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 16:17:14 +1200 Subject: [PATCH 110/132] refactor(pkg): convert tests to testify assert/require Convert pkg/gates, pkg/kubeenv, pkg/ignore, and pkg/helmpath test files from native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) to github.com/stretchr/testify equivalents (assert.X, require.X). Signed-off-by: George Jenkins --- pkg/gates/gates_test.go | 18 ++++------ pkg/helmpath/home_unix_test.go | 20 ++++------- pkg/helmpath/home_windows_test.go | 15 ++++---- pkg/helmpath/lazypath_darwin_test.go | 25 ++++--------- pkg/helmpath/lazypath_unix_test.go | 25 ++++--------- pkg/helmpath/lazypath_windows_test.go | 25 ++++--------- pkg/ignore/rules_test.go | 52 +++++++++------------------ 7 files changed, 56 insertions(+), 124 deletions(-) diff --git a/pkg/gates/gates_test.go b/pkg/gates/gates_test.go index 4d77199e6..217ee1bbe 100644 --- a/pkg/gates/gates_test.go +++ b/pkg/gates/gates_test.go @@ -18,6 +18,8 @@ package gates import ( "os" "testing" + + "github.com/stretchr/testify/assert" ) const name string = "HELM_EXPERIMENTAL_FEATURE" @@ -25,31 +27,23 @@ const name string = "HELM_EXPERIMENTAL_FEATURE" func TestIsEnabled(t *testing.T) { g := Gate(name) - if g.IsEnabled() { - t.Errorf("feature gate shows as available, but the environment variable %s was not set", name) - } + assert.False(t, g.IsEnabled(), "feature gate shows as available, but the environment variable %s was not set", name) t.Setenv(name, "1") - if !g.IsEnabled() { - t.Errorf("feature gate shows as disabled, but the environment variable %s was set", name) - } + assert.True(t, g.IsEnabled(), "feature gate shows as disabled, but the environment variable %s was set", name) } func TestError(t *testing.T) { os.Unsetenv(name) g := Gate(name) - if g.Error().Error() != "this feature has been marked as experimental and is not enabled by default. Please set HELM_EXPERIMENTAL_FEATURE=1 in your environment to use this feature" { - t.Errorf("incorrect error message. Received %s", g.Error().Error()) - } + assert.Equal(t, "this feature has been marked as experimental and is not enabled by default. Please set HELM_EXPERIMENTAL_FEATURE=1 in your environment to use this feature", g.Error().Error()) } func TestString(t *testing.T) { os.Unsetenv(name) g := Gate(name) - if g.String() != "HELM_EXPERIMENTAL_FEATURE" { - t.Errorf("incorrect string representation. Received %s", g.String()) - } + assert.Equal(t, "HELM_EXPERIMENTAL_FEATURE", g.String()) } diff --git a/pkg/helmpath/home_unix_test.go b/pkg/helmpath/home_unix_test.go index a64c9bcd6..84c912502 100644 --- a/pkg/helmpath/home_unix_test.go +++ b/pkg/helmpath/home_unix_test.go @@ -16,9 +16,10 @@ package helmpath import ( - "runtime" "testing" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/helmpath/xdg" ) @@ -26,20 +27,13 @@ func TestHelmHome(t *testing.T) { t.Setenv(xdg.CacheHomeEnvVar, "/cache") t.Setenv(xdg.ConfigHomeEnvVar, "/config") t.Setenv(xdg.DataHomeEnvVar, "/data") - isEq := func(t *testing.T, got, expected string) { - t.Helper() - if expected != got { - t.Error(runtime.GOOS) - t.Errorf("Expected %q, got %q", expected, got) - } - } - - isEq(t, CachePath(), "/cache/helm") - isEq(t, ConfigPath(), "/config/helm") - isEq(t, DataPath(), "/data/helm") + + assert.Equal(t, "/cache/helm", CachePath()) + assert.Equal(t, "/config/helm", ConfigPath()) + assert.Equal(t, "/data/helm", DataPath()) // test to see if lazy-loading environment variables at runtime works t.Setenv(xdg.CacheHomeEnvVar, "/cache2") - isEq(t, CachePath(), "/cache2/helm") + assert.Equal(t, "/cache2/helm", CachePath()) } diff --git a/pkg/helmpath/home_windows_test.go b/pkg/helmpath/home_windows_test.go index 38fe5e4f1..4c2c7d11a 100644 --- a/pkg/helmpath/home_windows_test.go +++ b/pkg/helmpath/home_windows_test.go @@ -19,6 +19,8 @@ import ( "os" "testing" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/helmpath/xdg" ) @@ -26,18 +28,13 @@ func TestHelmHome(t *testing.T) { os.Setenv(xdg.CacheHomeEnvVar, "c:\\") os.Setenv(xdg.ConfigHomeEnvVar, "d:\\") os.Setenv(xdg.DataHomeEnvVar, "e:\\") - isEq := func(t *testing.T, a, b string) { - if a != b { - t.Errorf("Expected %q, got %q", b, a) - } - } - isEq(t, CachePath(), "c:\\helm") - isEq(t, ConfigPath(), "d:\\helm") - isEq(t, DataPath(), "e:\\helm") + assert.Equal(t, "c:\\helm", CachePath()) + assert.Equal(t, "d:\\helm", ConfigPath()) + assert.Equal(t, "e:\\helm", DataPath()) // test to see if lazy-loading environment variables at runtime works os.Setenv(xdg.CacheHomeEnvVar, "f:\\") - isEq(t, CachePath(), "f:\\helm") + assert.Equal(t, "f:\\helm", CachePath()) } diff --git a/pkg/helmpath/lazypath_darwin_test.go b/pkg/helmpath/lazypath_darwin_test.go index e3006d0d5..143750f04 100644 --- a/pkg/helmpath/lazypath_darwin_test.go +++ b/pkg/helmpath/lazypath_darwin_test.go @@ -20,6 +20,7 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" "k8s.io/client-go/util/homedir" "helm.sh/helm/v4/pkg/helmpath/xdg" @@ -36,17 +37,13 @@ func TestDataPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "Library", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) t.Setenv(xdg.DataHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) } func TestConfigPath(t *testing.T) { @@ -54,17 +51,13 @@ func TestConfigPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "Library", "Preferences", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) t.Setenv(xdg.ConfigHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) } func TestCachePath(t *testing.T) { @@ -72,15 +65,11 @@ func TestCachePath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "Library", "Caches", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) t.Setenv(xdg.CacheHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) } diff --git a/pkg/helmpath/lazypath_unix_test.go b/pkg/helmpath/lazypath_unix_test.go index 4b0f2429b..6f8c6c2e8 100644 --- a/pkg/helmpath/lazypath_unix_test.go +++ b/pkg/helmpath/lazypath_unix_test.go @@ -19,6 +19,7 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" "k8s.io/client-go/util/homedir" "helm.sh/helm/v4/pkg/helmpath/xdg" @@ -33,47 +34,35 @@ const ( func TestDataPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), ".local", "share", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) t.Setenv(xdg.DataHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) } func TestConfigPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), ".config", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) t.Setenv(xdg.ConfigHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) } func TestCachePath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), ".cache", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) t.Setenv(xdg.CacheHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) } diff --git a/pkg/helmpath/lazypath_windows_test.go b/pkg/helmpath/lazypath_windows_test.go index ebd95e812..df8ad2d80 100644 --- a/pkg/helmpath/lazypath_windows_test.go +++ b/pkg/helmpath/lazypath_windows_test.go @@ -20,6 +20,7 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" "k8s.io/client-go/util/homedir" "helm.sh/helm/v4/pkg/helmpath/xdg" @@ -37,17 +38,13 @@ func TestDataPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) os.Setenv(xdg.DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) - if lazy.dataPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) - } + assert.Equal(t, expected, lazy.dataPath(testFile)) } func TestConfigPath(t *testing.T) { @@ -56,17 +53,13 @@ func TestConfigPath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) os.Setenv(xdg.ConfigHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) - if lazy.configPath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) - } + assert.Equal(t, expected, lazy.configPath(testFile)) } func TestCachePath(t *testing.T) { @@ -75,15 +68,11 @@ func TestCachePath(t *testing.T) { expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) os.Setenv(xdg.CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) - if lazy.cachePath(testFile) != expected { - t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) - } + assert.Equal(t, expected, lazy.cachePath(testFile)) } diff --git a/pkg/ignore/rules_test.go b/pkg/ignore/rules_test.go index 9581cf09f..02ba63588 100644 --- a/pkg/ignore/rules_test.go +++ b/pkg/ignore/rules_test.go @@ -21,6 +21,9 @@ import ( "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var testdata = "./testdata" @@ -36,22 +39,14 @@ baz/bar/foo.txt one/more ` r, err := parseString(rules) - if err != nil { - t.Fatalf("Error parsing rules: %s", err) - } + require.NoError(t, err, "Error parsing rules") - if len(r.patterns) != 4 { - t.Errorf("Expected 4 rules, got %d", len(r.patterns)) - } + assert.Len(t, r.patterns, 4) expects := []string{"foo", "bar/*", "baz/bar/foo.txt", "one/more"} for i, p := range r.patterns { - if p.raw != expects[i] { - t.Errorf("Expected %q, got %q", expects[i], p.raw) - } - if p.match == nil { - t.Errorf("Expected %s to have a matcher function.", p.raw) - } + assert.Equal(t, expects[i], p.raw) + assert.NotNil(t, p.match, "Expected %s to have a matcher function.", p.raw) } } @@ -59,26 +54,19 @@ func TestParseFail(t *testing.T) { shouldFail := []string{"foo/**/bar", "[z-"} for _, fail := range shouldFail { _, err := parseString(fail) - if err == nil { - t.Errorf("Rule %q should have failed", fail) - } + assert.Error(t, err, "Rule %q should have failed", fail) } } func TestParseFile(t *testing.T) { f := filepath.Join(testdata, HelmIgnore) - if _, err := os.Stat(f); err != nil { - t.Fatalf("Fixture %s missing: %s", f, err) - } + _, err := os.Stat(f) + require.NoError(t, err, "Fixture %s missing", f) r, err := ParseFile(f) - if err != nil { - t.Fatalf("Failed to parse rules file: %s", err) - } + require.NoError(t, err, "Failed to parse rules file") - if len(r.patterns) != 3 { - t.Errorf("Expected 3 patterns, got %d", len(r.patterns)) - } + assert.Len(t, r.patterns, 3) } func TestIgnore(t *testing.T) { @@ -126,17 +114,11 @@ func TestIgnore(t *testing.T) { for _, test := range tests { r, err := parseString(test.pattern) - if err != nil { - t.Fatalf("Failed to parse: %s", err) - } + require.NoError(t, err, "Failed to parse: %s", test.pattern) fi, err := os.Stat(filepath.Join(testdata, test.name)) - if err != nil { - t.Fatalf("Fixture missing: %s", err) - } + require.NoError(t, err, "Fixture missing: %s", test.name) - if r.Ignore(test.name, fi) != test.expect { - t.Errorf("Expected %q to be %v for pattern %q", test.name, test.expect, test.pattern) - } + assert.Equal(t, test.expect, r.Ignore(test.name, fi), "Expected %q to be %v for pattern %q", test.name, test.expect, test.pattern) } } @@ -144,9 +126,7 @@ func TestAddDefaults(t *testing.T) { r := Rules{} r.AddDefaults() - if len(r.patterns) != 1 { - t.Errorf("Expected 1 default patterns, got %d", len(r.patterns)) - } + assert.Len(t, r.patterns, 1) } func parseString(str string) (*Rules, error) { From fa9cb4919423e80d1693275396f9d8fa6b324baf Mon Sep 17 00:00:00 2001 From: devShaik010 <20dpcs044hy@manuu.edu.in> Date: Tue, 16 Jun 2026 20:00:17 +0530 Subject: [PATCH 111/132] docs: improve CONTRIBUTING.md wording Signed-off-by: devShaik010 <20dpcs044hy@manuu.edu.in> --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7aa19972f..0d7bc4f4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing Guidelines The Helm project accepts contributions via GitHub pull requests. This document outlines the process -to help get your contribution accepted. +to help you get your contribution accepted. ## Reporting a Security Issue @@ -110,9 +110,9 @@ Whether you are a user or contributor, official support channels include: - User: [#helm-users](https://kubernetes.slack.com/messages/C0NH30761/details/) - Contributor: [#helm-dev](https://kubernetes.slack.com/messages/C51E88VDG/) -Before opening a new issue or submitting a new pull request, it's helpful to search the project - -it's likely that another user has already reported the issue you're facing, or it's a known issue -that we're already aware of. It is also worth asking on the Slack channels. +Before opening a new issue or submitting a new pull request, it helps to search the project first. +Another user may have already reported the issue you are facing, or it may already be a known issue. +It is also worth asking on the Slack channels. ## Milestones @@ -137,7 +137,7 @@ are removed or substantially modified (unless we need to fix a security issue). We also remain committed to not changing publicly accessible Go library definitions inside of the `pkg/` directory of our source code in a non-backwards-compatible way. -For more details on Helm’s minor and patch release backwards-compatibility rules, please read [HIP-0004](https://github.com/helm/community/blob/main/hips/hip-0004.md) +For more details on Helm’s minor and patch release backwards-compatibility rules, please read [HIP-0004](https://github.com/helm/community/blob/main/hips/hip-0004.md). For a quick summary of our backward compatibility guidelines for releases between 3.0 and 4.0: From 9ad5620edc1b51507d111f7f998cc0f5b95e5726 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 12:16:54 -0700 Subject: [PATCH 112/132] Update pkg/ignore/rules_test.go Co-authored-by: Terry Howe Signed-off-by: George Jenkins --- pkg/ignore/rules_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ignore/rules_test.go b/pkg/ignore/rules_test.go index 02ba63588..30e157a52 100644 --- a/pkg/ignore/rules_test.go +++ b/pkg/ignore/rules_test.go @@ -41,7 +41,7 @@ one/more r, err := parseString(rules) require.NoError(t, err, "Error parsing rules") - assert.Len(t, r.patterns, 4) + require.Len(t, r.patterns, 4) expects := []string{"foo", "bar/*", "baz/bar/foo.txt", "one/more"} for i, p := range r.patterns { From d3bd09147bbae09ca85615452bda31b8215e66f5 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 13:21:05 +1200 Subject: [PATCH 113/132] Revert "fix(kube): prevent spurious early exit in WaitForDelete during informer sync" Signed-off-by: George Jenkins --- pkg/kube/statuswait.go | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index adc7a425a..29de0af2b 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -167,15 +167,7 @@ func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceL rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message)) } if err := ctx.Err(); err != nil { - // context.Canceled and other non-deadline errors always propagate: they signal an - // external interruption regardless of resource state. - // context.DeadlineExceeded is only added when there are resource-specific errors; - // if all resources are Unknown or NotFound the timeout is not itself a failure for - // a delete wait (e.g. resources deleted before the watch started stay Unknown in - // the fake client but are effectively gone). - if !errors.Is(err, context.DeadlineExceeded) || len(errs) > 0 { - errs = append(errs, err) - } + errs = append(errs, err) } if len(errs) > 0 { return errors.Join(errs...) @@ -248,7 +240,6 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl return func(statusCollector *collector.ResourceStatusCollector, _ event.Event) { var rss []*event.ResourceStatus var nonDesiredResources []*event.ResourceStatus - var unknownSkipped int for _, rs := range statusCollector.ResourceStatuses { if rs == nil { continue @@ -256,7 +247,6 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl // If a resource is already deleted before waiting has started, it will show as unknown. // This check ensures we don't wait forever for a resource that is already deleted. if rs.Status == status.UnknownStatus && desired == status.NotFoundStatus { - unknownSkipped++ continue } // Failed is a terminal state. This check ensures we don't wait forever for a resource @@ -270,14 +260,6 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logger *sl } } - // During informer initialization there is a brief window where existing resources - // appear as Unknown before their real status is delivered. If every resource was - // skipped as Unknown, we cannot yet distinguish "all deleted" from "not yet synced", - // so hold off on the early-cancel to avoid a spurious success or premature exit. - if unknownSkipped > 0 && len(rss) == 0 { - return - } - if aggregator.AggregateStatus(rss, desired) == desired { logger.Debug("all resources achieved desired status", "desiredStatus", desired, "resourceCount", len(rss)) cancel() From 16b0760dfd99b5c501cbfc2138a207e1b3512c0f Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 21:28:58 +1200 Subject: [PATCH 114/132] refactor(pkg/cli): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- pkg/cli/environment_test.go | 62 ++++++++++------------------------ pkg/cli/values/options_test.go | 61 +++++++++++---------------------- 2 files changed, 37 insertions(+), 86 deletions(-) diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 52326eeff..f1067f6c6 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -23,6 +23,8 @@ import ( "testing" "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "helm.sh/helm/v4/internal/version" ) @@ -30,14 +32,10 @@ import ( func TestSetNamespace(t *testing.T) { settings := New() - if settings.namespace != "" { - t.Errorf("Expected empty namespace, got %s", settings.namespace) - } + assert.Empty(t, settings.namespace) settings.SetNamespace("testns") - if settings.namespace != "testns" { - t.Errorf("Expected namespace testns, got %s", settings.namespace) - } + assert.Equal(t, "testns", settings.namespace) } func TestEnvSettings(t *testing.T) { @@ -134,36 +132,16 @@ func TestEnvSettings(t *testing.T) { settings.AddFlags(flags) flags.Parse(strings.Split(tt.args, " ")) - if settings.Debug != tt.debug { - t.Errorf("expected debug %t, got %t", tt.debug, settings.Debug) - } - if settings.Namespace() != tt.ns { - t.Errorf("expected namespace %q, got %q", tt.ns, settings.Namespace()) - } - if settings.KubeContext != tt.kcontext { - t.Errorf("expected kube-context %q, got %q", tt.kcontext, settings.KubeContext) - } - if settings.MaxHistory != tt.maxhistory { - t.Errorf("expected maxHistory %d, got %d", tt.maxhistory, settings.MaxHistory) - } - if tt.kubeAsUser != settings.KubeAsUser { - t.Errorf("expected kAsUser %q, got %q", tt.kubeAsUser, settings.KubeAsUser) - } - if !reflect.DeepEqual(tt.kubeAsGroups, settings.KubeAsGroups) { - t.Errorf("expected kAsGroups %+v, got %+v", len(tt.kubeAsGroups), len(settings.KubeAsGroups)) - } - if tt.kubeCaFile != settings.KubeCaFile { - t.Errorf("expected kCaFile %q, got %q", tt.kubeCaFile, settings.KubeCaFile) - } - if tt.burstLimit != settings.BurstLimit { - t.Errorf("expected BurstLimit %d, got %d", tt.burstLimit, settings.BurstLimit) - } - if tt.kubeInsecure != settings.KubeInsecureSkipTLSVerify { - t.Errorf("expected kubeInsecure %t, got %t", tt.kubeInsecure, settings.KubeInsecureSkipTLSVerify) - } - if tt.kubeTLSServer != settings.KubeTLSServerName { - t.Errorf("expected kubeTLSServer %q, got %q", tt.kubeTLSServer, settings.KubeTLSServerName) - } + assert.Equal(t, tt.debug, settings.Debug, "debug") + assert.Equal(t, tt.ns, settings.Namespace(), "namespace") + assert.Equal(t, tt.kcontext, settings.KubeContext, "kube-context") + assert.Equal(t, tt.maxhistory, settings.MaxHistory, "maxHistory") + assert.Equal(t, tt.kubeAsUser, settings.KubeAsUser, "kubeAsUser") + assert.True(t, reflect.DeepEqual(tt.kubeAsGroups, settings.KubeAsGroups), "kubeAsGroups") + assert.Equal(t, tt.kubeCaFile, settings.KubeCaFile, "kubeCaFile") + assert.Equal(t, tt.burstLimit, settings.BurstLimit, "burstLimit") + assert.Equal(t, tt.kubeInsecure, settings.KubeInsecureSkipTLSVerify, "kubeInsecure") + assert.Equal(t, tt.kubeTLSServer, settings.KubeTLSServerName, "kubeTLSServer") }) } } @@ -235,9 +213,7 @@ func TestEnvOrBool(t *testing.T) { t.Setenv(tt.env, tt.val) } actual := envBoolOr(tt.env, tt.def) - if actual != tt.expected { - t.Errorf("expected result %t, got %t", tt.expected, actual) - } + assert.Equal(t, tt.expected, actual) }) } } @@ -247,14 +223,10 @@ func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) { settings := New() restConfig, err := settings.RESTClientGetter().ToRESTConfig() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expectedUserAgent := version.GetUserAgent() - if restConfig.UserAgent != expectedUserAgent { - t.Errorf("expected User-Agent header %q in K8s REST client config, got %q", expectedUserAgent, restConfig.UserAgent) - } + assert.Equal(t, expectedUserAgent, restConfig.UserAgent) } func resetEnv() func() { diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index 571622d80..3a33da7f1 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -22,9 +22,11 @@ import ( "os" "path/filepath" "reflect" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "helm.sh/helm/v4/pkg/getter" ) @@ -90,10 +92,7 @@ func TestReadFile(t *testing.T) { tmpDir := t.TempDir() filePath := filepath.Join(tmpDir, "test.txt") content := []byte("local file content") - err := os.WriteFile(filePath, content, 0644) - if err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(filePath, content, 0644)) return filePath, func() {} // cleanup handled by t.TempDir() }, expectError: false, @@ -154,10 +153,7 @@ func TestReadFile(t *testing.T) { fileName := "ftp_file.txt" // Valid filename for filesystem filePath := filepath.Join(tmpDir, fileName) content := []byte("local fallback content") - err := os.WriteFile(filePath, content, 0644) - if err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(filePath, content, 0644)) return filePath, func() {} }, expectError: false, @@ -202,9 +198,7 @@ func TestReadFile(t *testing.T) { // Create a pipe for stdin r, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer r.Close() defer w.Close() @@ -220,28 +214,21 @@ func TestReadFile(t *testing.T) { // Test the function got, err := readFile(actualFilePath, tt.providers) - if err != nil { - t.Errorf("readFile() error = %v, expected no error for stdin", err) - return - } - - if !bytes.Equal(got, testData) { - t.Errorf("readFile() = %v, want %v", got, testData) - } + assert.NoError(t, err, "readFile() expected no error for stdin") + assert.Equal(t, testData, got) return } // Regular test cases got, err := readFile(actualFilePath, tt.providers) - if (err != nil) != tt.expectError { - t.Errorf("readFile() error = %v, expectError %v", err, tt.expectError) + if tt.expectError { + assert.Error(t, err) return } + assert.NoError(t, err) - if !tt.expectError && tt.expectedData != nil { - if !bytes.Equal(got, tt.expectedData) { - t.Errorf("readFile() = %v, want %v", got, tt.expectedData) - } + if tt.expectedData != nil { + assert.Equal(t, tt.expectedData, got) } }) } @@ -272,13 +259,8 @@ func TestReadFileErrorMessages(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := readFile(tt.filePath, tt.providers) - if err == nil { - t.Errorf("readFile() expected error containing %q, got nil", tt.wantErr) - return - } - if !strings.Contains(err.Error(), tt.wantErr) { - t.Errorf("readFile() error = %v, want error containing %q", err, tt.wantErr) - } + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) }) } } @@ -288,9 +270,7 @@ func TestReadFileOriginal(t *testing.T) { var p getter.Providers filePath := "%a.txt" _, err := readFile(filePath, p) - if err == nil { - t.Error("Expected error when has special strings") - } + assert.Error(t, err, "Expected error when has special strings") } func TestMergeValuesCLI(t *testing.T) { @@ -376,13 +356,12 @@ func TestMergeValuesCLI(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.opts.MergeValues(getter.Providers{}) - if (err != nil) != tt.wantErr { - t.Errorf("MergeValues() error = %v, wantErr %v", err, tt.wantErr) + if tt.wantErr { + assert.Error(t, err) return } - if !tt.wantErr && !reflect.DeepEqual(got, tt.expected) { - t.Errorf("MergeValues() = %v, want %v", got, tt.expected) - } + assert.NoError(t, err) + assert.True(t, reflect.DeepEqual(got, tt.expected), "MergeValues() = %v, want %v", got, tt.expected) }) } } From daa045d4923007c1ab815b39038b81491506dc0a Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 21:52:08 +1200 Subject: [PATCH 115/132] refactor(pkg/strvals): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- pkg/strvals/literal_parser_test.go | 45 +++++------ pkg/strvals/parser_test.go | 119 +++++++++++++---------------- 2 files changed, 73 insertions(+), 91 deletions(-) diff --git a/pkg/strvals/literal_parser_test.go b/pkg/strvals/literal_parser_test.go index cc3d73d50..d7892844f 100644 --- a/pkg/strvals/literal_parser_test.go +++ b/pkg/strvals/literal_parser_test.go @@ -20,6 +20,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" ) @@ -278,27 +281,25 @@ func TestParseLiteral(t *testing.T) { got, err := ParseLiteral(tt.str) if err != nil { if !tt.err { - t.Fatalf("%s: %s", tt.str, err) + require.NoError(t, err, tt.str) } continue } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.str) + assert.Fail(t, "Expected error. Got nil", tt.str) } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.str) } } } @@ -385,33 +386,31 @@ func TestParseLiteralInto(t *testing.T) { for _, tt := range tests { if err := ParseLiteralInto(tt.input, tt.got); err != nil { - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.input) + assert.Fail(t, "Expected error. Got nil", tt.input) } if tt.input2 != "" { if err := ParseLiteralInto(tt.input2, tt.got); err != nil { - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.input2) + assert.Fail(t, "Expected error. Got nil", tt.input2) } } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(tt.got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.input, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.input) } } } @@ -452,30 +451,28 @@ func TestParseLiteralNestedLevels(t *testing.T) { if tt.err { if tt.errStr != "" { if err.Error() != tt.errStr { - t.Errorf("Expected error: %s. Got error: %s", tt.errStr, err.Error()) + assert.Equal(t, tt.errStr, err.Error()) } } continue } - t.Fatalf("%s: %s", tt.str, err) + require.NoError(t, err, tt.str) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.str) + assert.Fail(t, "Expected error. Got nil", tt.str) } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.str) } } } diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index e3f8b9d8e..1a9559236 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -20,6 +20,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" ) @@ -78,24 +81,24 @@ func TestSetIndex(t *testing.T) { got, err := setIndex(tt.initial, tt.add, tt.val) if err != nil && tt.err == false { - t.Fatalf("%s: Expected no error but error returned", tt.name) + require.Fail(t, "Expected no error but error returned", tt.name) } else if err == nil && tt.err == true { - t.Fatalf("%s: Expected error but no error returned", tt.name) + require.Fail(t, "Expected error but no error returned", tt.name) } if len(got) != len(tt.expect) { - t.Fatalf("%s: Expected length %d, got %d", tt.name, len(tt.expect), len(got)) + require.Len(t, got, len(tt.expect), tt.name) } if !tt.err { if gg := got[tt.add].(int); gg != tt.val { - t.Errorf("%s, Expected value %d, got %d", tt.name, tt.val, gg) + assert.Equal(t, tt.val, gg, tt.name) } } for k, v := range got { if v != tt.expect[k] { - t.Errorf("%s, Expected value %d, got %d", tt.name, tt.expect[k], v) + assert.Equal(t, tt.expect[k], v, tt.name) } } } @@ -384,23 +387,21 @@ func TestParseSet(t *testing.T) { if tt.err { continue } - t.Fatalf("%s: %s", tt.str, err) + require.NoError(t, err, tt.str) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.str) + assert.Fail(t, "Expected error. Got nil", tt.str) } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.str) } } for _, tt := range testsString { @@ -409,23 +410,21 @@ func TestParseSet(t *testing.T) { if tt.err { continue } - t.Fatalf("%s: %s", tt.str, err) + require.NoError(t, err, tt.str) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.str) + assert.Fail(t, "Expected error. Got nil", tt.str) } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.str) } } } @@ -513,32 +512,30 @@ func TestParseInto(t *testing.T) { } for _, tt := range tests { if err := ParseInto(tt.input, tt.got); err != nil { - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.input) + assert.Fail(t, "Expected error. Got nil", tt.input) } if tt.input2 != "" { if err := ParseInto(tt.input2, tt.got); err != nil { - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.input2) + assert.Fail(t, "Expected error. Got nil", tt.input2) } } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(tt.got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.input, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.input) } } } @@ -560,20 +557,18 @@ func TestParseIntoString(t *testing.T) { } if err := ParseIntoString(input, got); err != nil { - t.Fatal(err) + require.NoError(t, err) } y1, err := yaml.Marshal(expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + assert.Equal(t, string(y1), string(y2), input) } } @@ -658,22 +653,22 @@ func TestParseJSON(t *testing.T) { if tt.err { continue } - t.Fatalf("%s: %s", tt.input, err) + require.NoError(t, err, tt.input) } if tt.err { - t.Fatalf("%s: Expected error. Got nil", tt.input) + require.Fail(t, "Expected error. Got nil", tt.input) } y1, err := yaml.Marshal(tt.expect) if err != nil { - t.Fatalf("Error serializing expected value: %s", err) + require.NoError(t, err, "Error serializing expected value") } y2, err := yaml.Marshal(tt.got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.input, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.input) } } } @@ -686,28 +681,24 @@ func TestParseFile(t *testing.T) { rs2v := func(rs []rune) (any, error) { v := string(rs) if v != "path1" { - t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) + assert.Equal(t, "path1", v, "%s: runesToVal", input) return "", nil } return "value1", nil } got, err := ParseFile(input, rs2v) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y1, err := yaml.Marshal(expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + assert.Equal(t, string(y1), string(y2), input) } } @@ -720,27 +711,25 @@ func TestParseIntoFile(t *testing.T) { rs2v := func(rs []rune) (any, error) { v := string(rs) if v != "path1" { - t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) + assert.Equal(t, "path1", v, "%s: runesToVal", input) return "", nil } return "value1", nil } if err := ParseIntoFile(input, got, rs2v); err != nil { - t.Fatal(err) + require.NoError(t, err) } y1, err := yaml.Marshal(expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + assert.Equal(t, string(y1), string(y2), input) } } @@ -748,12 +737,10 @@ func TestToYAML(t *testing.T) { // The TestParse does the hard part. We just verify that YAML formatting is // happening. o, err := ToYAML("name=value") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expect := "name: value" if o != expect { - t.Errorf("Expected %q, got %q", expect, o) + assert.Equal(t, expect, o) } } @@ -792,28 +779,26 @@ func TestParseSetNestedLevels(t *testing.T) { if tt.err { if tt.errStr != "" { if err.Error() != tt.errStr { - t.Errorf("Expected error: %s. Got error: %s", tt.errStr, err.Error()) + assert.Equal(t, tt.errStr, err.Error()) } } continue } - t.Fatalf("%s: %s", tt.str, err) + require.NoError(t, err, tt.str) } if tt.err { - t.Errorf("%s: Expected error. Got nil", tt.str) + assert.Fail(t, "Expected error. Got nil", tt.str) } y1, err := yaml.Marshal(tt.expect) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) y2, err := yaml.Marshal(got) if err != nil { - t.Fatalf("Error serializing parsed value: %s", err) + require.NoError(t, err, "Error serializing parsed value") } if string(y1) != string(y2) { - t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2) + assert.Equal(t, string(y1), string(y2), tt.str) } } } From 3d2ff2a1aa3d8696105af5367a254c6824dff4a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:51:52 +0000 Subject: [PATCH 116/132] chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-test.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/govulncheck.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 32c524ed3..f12ca5f00 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 008d6a594..38aa0a8ca 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 734bc682a..eaf36d257 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 6453ddd40..9ca2f643f 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Add variables to environment file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab8a4a509..99c5ddcd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest-16-cores steps: - name: Checkout source code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: fetch-depth: 0 @@ -85,7 +85,7 @@ jobs: if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm' steps: - name: Checkout source code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 55a9aec82..b7a27a64a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -28,7 +28,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false From fa68e6cb094e84076bfad53af494d7db7642ceee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:33:39 +0000 Subject: [PATCH 117/132] chore(deps): bump the k8s-io group across 1 directory with 7 updates Bumps the k8s-io group with 4 updates in the / directory: [k8s.io/api](https://github.com/kubernetes/api), [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver), [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) and [k8s.io/kubectl](https://github.com/kubernetes/kubectl). Updates `k8s.io/api` from 0.36.1 to 0.36.2 - [Commits](https://github.com/kubernetes/api/compare/v0.36.1...v0.36.2) Updates `k8s.io/apiextensions-apiserver` from 0.36.1 to 0.36.2 - [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases) - [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.36.1...v0.36.2) Updates `k8s.io/apimachinery` from 0.36.1 to 0.36.2 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.36.1...v0.36.2) Updates `k8s.io/apiserver` from 0.36.1 to 0.36.2 - [Commits](https://github.com/kubernetes/apiserver/compare/v0.36.1...v0.36.2) Updates `k8s.io/cli-runtime` from 0.36.1 to 0.36.2 - [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.36.1...v0.36.2) Updates `k8s.io/client-go` from 0.36.1 to 0.36.2 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.36.1...v0.36.2) Updates `k8s.io/kubectl` from 0.36.1 to 0.36.2 - [Commits](https://github.com/kubernetes/kubectl/compare/v0.36.1...v0.36.2) --- updated-dependencies: - dependency-name: k8s.io/api dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiextensions-apiserver dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiserver dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/cli-runtime dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/kubectl dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index e04604af0..72a708cf3 100644 --- a/go.mod +++ b/go.mod @@ -39,14 +39,14 @@ require ( golang.org/x/term v0.44.0 golang.org/x/text v0.38.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.36.1 - k8s.io/apiextensions-apiserver v0.36.1 - k8s.io/apimachinery v0.36.1 - k8s.io/apiserver v0.36.1 - k8s.io/cli-runtime v0.36.1 - k8s.io/client-go v0.36.1 + k8s.io/api v0.36.2 + k8s.io/apiextensions-apiserver v0.36.2 + k8s.io/apimachinery v0.36.2 + k8s.io/apiserver v0.36.2 + k8s.io/cli-runtime v0.36.2 + k8s.io/client-go v0.36.2 k8s.io/klog/v2 v2.140.0 - k8s.io/kubectl v0.36.1 + k8s.io/kubectl v0.36.2 oras.land/oras-go/v2 v2.6.1 sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/kustomize/kyaml v0.21.1 @@ -171,7 +171,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.36.1 // indirect + k8s.io/component-base v0.36.2 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index c0da36a37..b5a6777a6 100644 --- a/go.sum +++ b/go.sum @@ -485,26 +485,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= -k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= -k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= -k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= -k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= -k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= -k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY= -k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw= -k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= -k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= -k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= -k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= -k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= -k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4= +k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E= +k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k= +k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM= +k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w= +k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= -k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= +k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk= +k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= From dd442c0cc7c3d7f397bbf93ac5a9551a4e2f0139 Mon Sep 17 00:00:00 2001 From: s3onghyun Date: Thu, 18 Jun 2026 16:03:12 +0900 Subject: [PATCH 118/132] docs: fix typo in helm version command description Signed-off-by: s3onghyun --- pkg/cmd/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 80fb0d712..1099d46ea 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -30,7 +30,7 @@ import ( const versionDesc = ` Show the version for Helm. -This will print a representation the version of Helm. +This will print a representation of the version of Helm. The output will look something like this: version.BuildInfo{Version:"v3.2.1", GitCommit:"fe51cd1e31e6a202cba7dead9552a6d418ded79a", GitTreeState:"clean", GoVersion:"go1.13.10"} From c36e4d52bdf92a05a8d2d9f40e5927a1245dce2d Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 18 Jun 2026 07:56:32 -0600 Subject: [PATCH 119/132] test: use assert.Empty for zero-length assertions in validate_test Signed-off-by: Terry Howe --- pkg/action/validate_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go index 8e983e9ff..3624d3676 100644 --- a/pkg/action/validate_test.go +++ b/pkg/action/validate_test.go @@ -237,7 +237,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) assert.Len(t, ownedList, 2) - assert.Len(t, unownedList, 0) + assert.Empty(t, unownedList) }) // Test mix of owned and unowned resources @@ -261,8 +261,8 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) - assert.Len(t, ownedList, 0) - assert.Len(t, unownedList, 0) + assert.Empty(t, ownedList) + assert.Empty(t, unownedList) }) // Test resource with no ownership metadata @@ -272,7 +272,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) - assert.Len(t, ownedList, 0) + assert.Empty(t, ownedList) assert.Len(t, unownedList, 1) }) @@ -283,7 +283,7 @@ func TestVerifyOwnershipBeforeDelete(t *testing.T) { ownedList, unownedList, _, err := verifyOwnershipBeforeDelete(resources, releaseName, releaseNamespace) assert.NoError(t, err) - assert.Len(t, ownedList, 0) + assert.Empty(t, ownedList) assert.Len(t, unownedList, 1) }) From 5848047c6b1e854fe7d451e451dbf748225232b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:32:39 +0000 Subject: [PATCH 120/132] chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build-test.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/govulncheck.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f12ca5f00..a0fea406c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 38aa0a8ca..323b8ec5b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index eaf36d257..13ea569ef 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 9ca2f643f..ba3714eae 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 with: persist-credentials: false - name: Add variables to environment file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99c5ddcd1..993c45899 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest-16-cores steps: - name: Checkout source code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 with: fetch-depth: 0 @@ -85,7 +85,7 @@ jobs: if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm' steps: - name: Checkout source code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b7a27a64a..03419cbda 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -28,7 +28,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false From a0c7d1e1f5ef222fb1be143e9846cebf8b3a2d54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:33:12 +0000 Subject: [PATCH 121/132] chore(deps): bump github.com/cyphar/filepath-securejoin Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.6.1 to 0.7.0. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Changelog](https://github.com/cyphar/filepath-securejoin/blob/main/CHANGELOG.md) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...v0.7.0) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 72a708cf3..ea6d6c972 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/Masterminds/vcs v1.13.3 github.com/ProtonMail/go-crypto v1.4.1 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/cyphar/filepath-securejoin v0.6.1 + github.com/cyphar/filepath-securejoin v0.7.0 github.com/distribution/distribution/v3 v3.1.1 github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 diff --git a/go.sum b/go.sum index b5a6777a6..f50611b85 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= From 34cfdfdbe57ec9690113e141395c499a7c00ca6b Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Wed, 25 Feb 2026 08:01:45 +0100 Subject: [PATCH 122/132] chore: enable contextcheck, fatcontext and noctx linters Signed-off-by: Matthieu MOREL --- .golangci.yml | 3 + cmd/helm/helm_test.go | 2 +- internal/chart/v3/lint/rules/template.go | 3 +- internal/monocular/search.go | 10 ++- internal/monocular/search_test.go | 2 +- internal/plugin/installer/installer.go | 8 +- internal/plugin/runtime_extismv1_test.go | 2 +- internal/plugin/runtime_subprocess.go | 21 ++--- internal/plugin/runtime_subprocess_getter.go | 6 +- pkg/action/action.go | 7 +- pkg/action/action_test.go | 28 +++---- pkg/action/install.go | 2 +- pkg/action/upgrade.go | 6 +- pkg/chart/common/util/jsonschema.go | 3 +- pkg/chart/v2/lint/rules/template.go | 3 +- pkg/cmd/search_hub.go | 9 +- pkg/engine/engine.go | 36 ++++++-- pkg/engine/engine_test.go | 12 +-- pkg/engine/lookup_func.go | 8 +- pkg/getter/httpgetter.go | 3 +- pkg/kube/ready.go | 2 +- pkg/kubeenv/roundtripper_test.go | 9 +- pkg/registry/registry_test.go | 8 +- pkg/repo/v1/repotest/server.go | 3 +- pkg/repo/v1/repotest/server_test.go | 87 ++++++++------------ 25 files changed, 161 insertions(+), 122 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index e7c8b11a6..b3ced6f1a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,10 +20,12 @@ linters: # Keep sorted alphabetically enable: - bidichk + - contextcheck - depguard - dupl - errorlint - exhaustive + - fatcontext - gocritic - gomodguard - govet @@ -31,6 +33,7 @@ linters: - misspell - modernize - nakedret + - noctx - nolintlint - perfsprint - revive diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index a9362f772..95f6504c1 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -46,7 +46,7 @@ func TestCliPluginExitCode(t *testing.T) { // So that the second run is able to run main() and this first run can verify the exit status returned by that. // // This technique originates from https://talks.golang.org/2014/testing.slide#23. - cmd := exec.Command(os.Args[0], "-test.run=TestCliPluginExitCode") + cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=TestCliPluginExitCode") cmd.Env = append( os.Environ(), "RUN_MAIN_FOR_TESTING=1", diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index a8ae910eb..464acc769 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -19,6 +19,7 @@ package rules import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -104,7 +105,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string } var e engine.Engine e.LintMode = true - renderedContentMap, err := e.Render(chart, valuesToRender) + renderedContentMap, err := e.RenderWithContext(context.Background(), chart, valuesToRender) renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) diff --git a/internal/monocular/search.go b/internal/monocular/search.go index cfae87ded..29cc7f1a4 100644 --- a/internal/monocular/search.go +++ b/internal/monocular/search.go @@ -17,6 +17,7 @@ limitations under the License. package monocular import ( + "context" "encoding/json" "fmt" "net/http" @@ -98,7 +99,14 @@ type ChartVersion struct { } // Search performs a search against the monocular search API +// +// Deprecated: Use SearchWithContext instead. func (c *Client) Search(term string) ([]SearchResult, error) { + return c.SearchWithContext(context.Background(), term) +} + +// SearchWithContext performs a search against the monocular search API +func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) { // Create the URL to the search endpoint // Note, this is currently an internal API for the Hub. This should be // formatted without showing how monocular operates. @@ -113,7 +121,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) { p.RawQuery = "q=" + url.QueryEscape(term) // Create request - req, err := http.NewRequest(http.MethodGet, p.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.String(), http.NoBody) if err != nil { return nil, err } diff --git a/internal/monocular/search_test.go b/internal/monocular/search_test.go index cdf67f69e..e4475f24e 100644 --- a/internal/monocular/search_test.go +++ b/internal/monocular/search_test.go @@ -37,7 +37,7 @@ func TestSearch(t *testing.T) { t.Errorf("unable to create monocular client: %s", err) } - results, err := c.Search("phpmyadmin") + results, err := c.SearchWithContext(t.Context(), "phpmyadmin") if err != nil { t.Errorf("unable to search monocular: %s", err) } diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index f73197629..a7ca35ad4 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -16,6 +16,7 @@ limitations under the License. package installer import ( + "context" "errors" "fmt" "log/slog" @@ -189,12 +190,17 @@ func isRemoteHTTPArchive(source string) bool { } // If no suffix match, try HEAD request to check content type - res, err := http.Head(source) + req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, source, http.NoBody) if err != nil { // If we get an error at the network layer, we can't install it. So // we return false. return false } + res, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer res.Body.Close() // Next, we look for the content type or content disposition headers to see // if they have matching extractors. diff --git a/internal/plugin/runtime_extismv1_test.go b/internal/plugin/runtime_extismv1_test.go index bcd791905..9451c693b 100644 --- a/internal/plugin/runtime_extismv1_test.go +++ b/internal/plugin/runtime_extismv1_test.go @@ -46,7 +46,7 @@ func buildLoadExtismPlugin(t *testing.T, dir string) pluginRaw { require.NoError(t, err) require.Equal(t, "extism/v1", m.Runtime, "expected plugin runtime to be extism/v1") - cmd := exec.Command("make", "-C", dir) + cmd := exec.CommandContext(t.Context(), "make", "-C", dir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoError(t, cmd.Run(), "failed to build plugin in %q", dir) diff --git a/internal/plugin/runtime_subprocess.go b/internal/plugin/runtime_subprocess.go index cd1a0842c..3c0af4a4e 100644 --- a/internal/plugin/runtime_subprocess.go +++ b/internal/plugin/runtime_subprocess.go @@ -96,14 +96,14 @@ func (r *SubprocessPluginRuntime) Metadata() Metadata { return r.metadata } -func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) { switch input.Message.(type) { case schema.InputMessageCLIV1: - return r.runCLI(input) + return r.runCLI(ctx, input) case schema.InputMessageGetterV1: - return r.runGetter(input) + return r.runGetter(ctx, input) case schema.InputMessagePostRendererV1: - return r.runPostrenderer(input) + return r.runPostrenderer(ctx, input) default: return nil, fmt.Errorf("unsupported subprocess plugin type %q", r.metadata.Type) } @@ -113,7 +113,7 @@ func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Outp // This method allows execution with different command/args than the plugin's default func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error { mainCmdExp := os.ExpandEnv(main) - cmd := exec.Command(mainCmdExp, argv...) + cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...) cmd.Env = slices.Clone(os.Environ()) cmd.Env = append( cmd.Env, @@ -149,7 +149,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error { return err } - cmd := exec.Command(main, argv...) + cmd := exec.CommandContext(context.Background(), main, argv...) cmd.Env = FormatEnv(env) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -189,7 +189,7 @@ func executeCmd(prog *exec.Cmd, pluginName string) error { return nil } -func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runCLI(ctx context.Context, input *Input) (*Output, error) { if _, ok := input.Message.(schema.InputMessageCLIV1); !ok { return nil, fmt.Errorf("plugin %q input message does not implement InputMessageCLIV1", r.metadata.Name) } @@ -209,7 +209,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { return nil, fmt.Errorf("failed to prepare plugin command: %w", err) } - cmd := exec.Command(command, args...) + cmd := exec.CommandContext(ctx, command, args...) cmd.Env = FormatEnv(env) cmd.Stdin = input.Stdin @@ -226,7 +226,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { }, nil } -func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runPostrenderer(ctx context.Context, input *Input) (*Output, error) { if _, ok := input.Message.(schema.InputMessagePostRendererV1); !ok { return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name) } @@ -244,7 +244,8 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) return nil, fmt.Errorf("failed to prepare plugin command: %w", err) } - cmd := exec.Command( + cmd := exec.CommandContext( + ctx, command, args...) diff --git a/internal/plugin/runtime_subprocess_getter.go b/internal/plugin/runtime_subprocess_getter.go index c7262e0dd..3f07fa764 100644 --- a/internal/plugin/runtime_subprocess_getter.go +++ b/internal/plugin/runtime_subprocess_getter.go @@ -17,6 +17,7 @@ package plugin import ( "bytes" + "context" "fmt" "log/slog" "maps" @@ -40,7 +41,7 @@ func getProtocolCommand(commands []SubprocessProtocolCommand, protocol string) * } // TODO can we replace a lot of this func with RuntimeSubprocess.invokeWithEnv? -func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runGetter(ctx context.Context, input *Input) (*Output, error) { msg, ok := (input.Message).(schema.InputMessageGetterV1) if !ok { return nil, fmt.Errorf("expected input type schema.InputMessageGetterV1, got %T", input) @@ -81,7 +82,8 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) { buf := bytes.Buffer{} // subprocess getters are expected to write content to stdout pluginCommand := filepath.Join(r.pluginDir, command) - cmd := exec.Command( + cmd := exec.CommandContext( + ctx, pluginCommand, args...) cmd.Env = FormatEnv(env) diff --git a/pkg/action/action.go b/pkg/action/action.go index 4ed718371..6e5d8c15a 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -18,6 +18,7 @@ package action import ( "bytes" + "context" "errors" "fmt" "io" @@ -275,7 +276,7 @@ func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -305,13 +306,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, e.EnableDNS = enableDNS e.CustomTemplateFuncs = cfg.CustomTemplateFuncs - files, err2 = e.Render(ch, values) + files, err2 = e.RenderWithContext(ctx, ch, values) } else { var e engine.Engine e.EnableDNS = enableDNS e.CustomTemplateFuncs = cfg.CustomTemplateFuncs - files, err2 = e.Render(ch, values) + files, err2 = e.RenderWithContext(ctx, ch, values) } if err2 != nil { diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 6a561985f..ecf160500 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1823,7 +1823,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1870,7 +1870,7 @@ func TestRenderResources_PostRenderer_Error(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1898,7 +1898,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1920,7 +1920,7 @@ func TestRenderResources_PostRenderer_SplitError(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1941,7 +1941,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1980,7 +1980,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, nil, false, false, false, PostRenderStrategyCombined, ) @@ -2041,7 +2041,7 @@ spec: } hooks, buf, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2083,7 +2083,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -2119,7 +2119,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategy(""), ) @@ -2153,7 +2153,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2187,7 +2187,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2221,7 +2221,7 @@ metadata: } hooks, manifestDoc, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyNoHooks, ) @@ -2258,7 +2258,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyNoHooks, ) @@ -2280,7 +2280,7 @@ metadata: mockPR := &mockPostRenderer{} _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategy("bogus"), ) diff --git a/pkg/action/install.go b/pkg/action/install.go index 580b8a0cb..fcfae7214 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -375,7 +375,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(ctx, chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 0d7b1b148..81d51164f 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -193,7 +193,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char } u.cfg.Logger().Debug("preparing upgrade", "name", name) - currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals) + currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(ctx, name, chrt, vals) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char } // prepareUpgrade builds an upgraded release for an upgrade operation. -func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]any) (*release.Release, *release.Release, bool, error) { +func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv2.Chart, vals map[string]any) (*release.Release, *release.Release, bool, error) { if chart == nil { return nil, nil, false, errMissingChart } @@ -300,7 +300,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(ctx, chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) if err != nil { return nil, nil, false, err } diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index 63ca0c274..624143993 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -18,6 +18,7 @@ package util import ( "bytes" + "context" "crypto/tls" "errors" "fmt" @@ -41,7 +42,7 @@ type HTTPURLLoader http.Client func (l *HTTPURLLoader) Load(urlStr string) (any, error) { client := (*http.Client)(l) - req, err := http.NewRequest(http.MethodGet, urlStr, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, urlStr, http.NoBody) if err != nil { return nil, fmt.Errorf("failed to create HTTP request for %s: %w", urlStr, err) } diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 25ed31900..47209112e 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -19,6 +19,7 @@ package rules import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -134,7 +135,7 @@ func (t *templateLinter) Lint() { } var e engine.Engine e.LintMode = true - renderedContentMap, err := e.Render(chart, valuesToRender) + renderedContentMap, err := e.RenderWithContext(context.Background(), chart, valuesToRender) renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) diff --git a/pkg/cmd/search_hub.go b/pkg/cmd/search_hub.go index 3081961be..3f7afa3f4 100644 --- a/pkg/cmd/search_hub.go +++ b/pkg/cmd/search_hub.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "context" "errors" "fmt" "io" @@ -65,8 +66,8 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { Use: "hub [KEYWORD]", Short: "search for charts in the Artifact Hub or your own hub instance", Long: searchHubDesc, - RunE: func(_ *cobra.Command, args []string) error { - return o.run(out, args) + RunE: func(c *cobra.Command, args []string) error { + return o.run(c.Context(), out, args) }, } @@ -81,14 +82,14 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { return cmd } -func (o *searchHubOptions) run(out io.Writer, args []string) error { +func (o *searchHubOptions) run(ctx context.Context, out io.Writer, args []string) error { c, err := monocular.New(o.searchEndpoint) if err != nil { return fmt.Errorf("unable to create connection to %q: %w", o.searchEndpoint, err) } q := strings.Join(args, " ") - results, err := c.Search(q) + results, err := c.SearchWithContext(ctx, q) if err != nil { slog.Debug("search failed", slog.Any("error", err)) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 6fd2beed8..4f7fa4590 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -17,6 +17,7 @@ limitations under the License. package engine import ( + "context" "errors" "fmt" "log/slog" @@ -76,9 +77,34 @@ func New(config *rest.Config) Engine { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. +// +// Deprecated: Use RenderWithContext instead. func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { + return e.RenderWithContext(context.Background(), chrt, values) +} + +// RenderWithContext takes a chart, optional values, and value overrides, and attempts to render the Go templates. +// +// RenderWithContext can be called repeatedly on the same engine. +// +// This will look in the chart's 'templates' data (e.g. the 'templates/' directory) +// and attempt to render the templates there using the values passed in. +// +// Values are scoped to their templates. A dependency template will not have +// access to the values set for its parent. If chart "foo" includes chart "bar", +// "bar" will not have access to the values for "foo". +// +// Values should be prepared with something like `chartutils.ReadValues`. +// +// Values are passed through the templates according to scope. If the top layer +// chart includes the chart foo, which includes the chart bar, the values map +// will be examined for a table called "foo". If "foo" is found in vals, +// that section of the values will be passed into the "foo" chart. And if that +// section contains a value named "bar", that value will be passed on to the +// bar chart during render time. +func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values common.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) - return e.render(tmap) + return e.render(ctx, tmap) } // Render takes a chart, optional values, and value overrides, and attempts to @@ -195,7 +221,7 @@ func tplFun(parent *template.Template, includedNames map[string]int, strict bool } // initFunMap creates the Engine's FuncMap and adds context-specific functions. -func (e Engine) initFunMap(t *template.Template) { +func (e Engine) initFunMap(ctx context.Context, t *template.Template) { funcMap := funcMap() includedNames := make(map[string]int) @@ -238,7 +264,7 @@ func (e Engine) initFunMap(t *template.Template) { // If we are not linting and have a cluster connection, provide a Kubernetes-backed // implementation. if !e.LintMode && e.clientProvider != nil { - funcMap["lookup"] = newLookupFunction(*e.clientProvider) + funcMap["lookup"] = newLookupFunction(ctx, *e.clientProvider) } // When DNS lookups are not enabled override the sprig function and return @@ -256,7 +282,7 @@ func (e Engine) initFunMap(t *template.Template) { } // render takes a map of templates/values and renders them. -func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { +func (e Engine) render(ctx context.Context, tpls map[string]renderable) (rendered map[string]string, err error) { // Basically, what we do here is start with an empty parent template and then // build up a list of templates -- one for each file. Once all of the templates // have been parsed, we loop through again and execute every template. @@ -278,7 +304,7 @@ func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, t.Option("missingkey=zero") } - e.initFunMap(t) + e.initFunMap(ctx, t) // We want to parse the templates in a predictable order. The order favors // higher-level (in file system) templates over deeply nested templates. diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 612ab85f0..f666b63a6 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -197,7 +197,7 @@ func TestRenderInternals(t *testing.T) { "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, } - out, err := new(Engine).render(tpls) + out, err := new(Engine).render(t.Context(), tpls) if err != nil { t.Fatalf("Failed template rendering: %s", err) } @@ -443,7 +443,7 @@ func TestParallelRenderInternals(t *testing.T) { vals: map[string]any{"val": tt}, }, } - out, err := e.render(tpls) + out, err := e.render(t.Context(), tpls) if err != nil { t.Errorf("Failed to render %s: %s", tt, err) } @@ -462,7 +462,7 @@ func TestParseErrors(t *testing.T) { tplsUndefinedFunction := map[string]renderable{ "undefined_function": {tpl: `{{foo}}`, vals: vals}, } - _, err := new(Engine).render(tplsUndefinedFunction) + _, err := new(Engine).render(t.Context(), tplsUndefinedFunction) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -525,7 +525,7 @@ linebreak`, for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - _, err := new(Engine).render(tt.tpls) + _, err := new(Engine).render(t.Context(), tt.tpls) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -543,7 +543,7 @@ func TestFailErrors(t *testing.T) { tplsFailed := map[string]renderable{ "failtpl": {tpl: failtpl, vals: vals}, } - _, err := new(Engine).render(tplsFailed) + _, err := new(Engine).render(t.Context(), tplsFailed) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -554,7 +554,7 @@ func TestFailErrors(t *testing.T) { var e Engine e.LintMode = true - out, err := e.render(tplsFailed) + out, err := e.render(t.Context(), tplsFailed) if err != nil { t.Fatal(err) } diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index c5c441c14..b0d0224be 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -36,7 +36,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // // If the resource does not exist, no error is raised. func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive - return newLookupFunction(clientProviderFromConfig{config: config}) + return newLookupFunction(context.Background(), clientProviderFromConfig{config: config}) } type ClientProvider interface { @@ -54,7 +54,7 @@ func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic return getDynamicClientOnKind(apiVersion, kind, c.config) } -func newLookupFunction(clientProvider ClientProvider) lookupFunc { +func newLookupFunction(ctx context.Context, clientProvider ClientProvider) lookupFunc { return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) { var client dynamic.ResourceInterface c, namespaced, err := clientProvider.GetClientFor(apiversion, kind) @@ -68,7 +68,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { } if name != "" { // this will return a single object - obj, err := client.Get(context.Background(), name, metav1.GetOptions{}) + obj, err := client.Get(ctx, name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. @@ -86,7 +86,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { return obj.UnstructuredContent(), nil } // this will return a list - obj, err := client.List(context.Background(), metav1.ListOptions{}) + obj, err := client.List(ctx, metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 2eb2d5d8c..a72a9337a 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -17,6 +17,7 @@ package getter import ( "bytes" + "context" "crypto/tls" "fmt" "io" @@ -49,7 +50,7 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) { // Set a helm specific user agent so that a repo server and metrics can // separate helm calls from other tools interacting with repos. - req, err := http.NewRequest(http.MethodGet, href, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, href, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/kube/ready.go b/pkg/kube/ready.go index a1a3d4a9a..196fe922a 100644 --- a/pkg/kube/ready.go +++ b/pkg/kube/ready.go @@ -108,7 +108,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err return c.pausedAsReady, nil } // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1()) + newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1()) //nolint:contextcheck if err != nil || newReplicaSet == nil { return false, err } diff --git a/pkg/kubeenv/roundtripper_test.go b/pkg/kubeenv/roundtripper_test.go index b921eac82..6f0caf6f8 100644 --- a/pkg/kubeenv/roundtripper_test.go +++ b/pkg/kubeenv/roundtripper_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type fakeRoundTripper struct { @@ -134,15 +135,15 @@ func TestRetryingRoundTripper_RoundTrip(t *testing.T) { rt := RetryingRoundTripper{ Wrapped: fakeRT, } - req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody) + require.NoError(t, err) resp, err := rt.RoundTrip(req) if tt.expectedErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedErr) + require.ErrorContains(t, err, tt.expectedErr) return } - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, tt.expectedCode, resp.StatusCode) assert.Equal(t, tt.expectedCalls, fakeRT.calls) diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 3fc08a900..dc8118c6e 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -18,7 +18,6 @@ package registry import ( "bytes" - "context" "crypto/tls" "fmt" "io" @@ -125,7 +124,8 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { // Registry config config := &configuration.Configuration{} - ln, err := net.Listen("tcp", "127.0.0.1:0") + lnCfg := net.ListenConfig{} + ln, err := lnCfg.Listen(suite.T().Context(), "tcp", "127.0.0.1:0") suite.Require().NoError(err, "no error finding free port for test registry") defer func() { _ = ln.Close() }() @@ -158,7 +158,7 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { config.HTTP.TLS.ClientCAs = []string{tlsCA} } } - suite.dockerRegistry, err = registry.NewRegistry(context.Background(), config) + suite.dockerRegistry, err = registry.NewRegistry(suite.T().Context(), config) suite.Require().NoError(err, "no error creating test registry") suite.FakeRegistryHost = initFakeRegistryTestServer() @@ -170,7 +170,7 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { func teardown(suite *TestRegistry) { if suite.dockerRegistry != nil { - _ = suite.dockerRegistry.Shutdown(context.Background()) + _ = suite.dockerRegistry.Shutdown(suite.T().Context()) } } diff --git a/pkg/repo/v1/repotest/server.go b/pkg/repo/v1/repotest/server.go index f1b5e0744..61dcd6931 100644 --- a/pkg/repo/v1/repotest/server.go +++ b/pkg/repo/v1/repotest/server.go @@ -180,7 +180,8 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { // Registry config config := &configuration.Configuration{} - ln, err := net.Listen("tcp", "127.0.0.1:0") + lnCfg := net.ListenConfig{} + ln, err := lnCfg.Listen(t.Context(), "tcp", "127.0.0.1:0") if err != nil { t.Fatalf("error finding free port for test registry: %v", err) } diff --git a/pkg/repo/v1/repotest/server_test.go b/pkg/repo/v1/repotest/server_test.go index 499091a57..1364fc9c9 100644 --- a/pkg/repo/v1/repotest/server_test.go +++ b/pkg/repo/v1/repotest/server_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" "helm.sh/helm/v4/internal/test/ensure" @@ -52,26 +53,24 @@ func TestServer(t *testing.T) { t.Errorf("Unexpected chart: %s", c[0]) } - res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + client := http.DefaultClient + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.ContentLength < 500 { t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) } - res, err = http.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Fatal(err) - } - + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) data, err := io.ReadAll(res.Body) res.Body.Close() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) m := repo.NewIndexFile() if err := yaml.Unmarshal(data, m); err != nil { @@ -87,11 +86,11 @@ func TestServer(t *testing.T) { t.Errorf("missing %q", expect) } - res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.StatusCode != http.StatusNotFound { t.Fatalf("Expected 404, got %d", res.StatusCode) } @@ -133,70 +132,56 @@ func TestNewTempServer(t *testing.T) { client := srv.Client() { - res, err := client.Head(srv.URL() + "/repositories.yaml") - if err != nil { - t.Error(err) - } - + req, err := http.NewRequestWithContext(t.Context(), http.MethodHead, srv.URL()+"/repositories.yaml", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { t.Errorf("Expected 200, got %d", res.StatusCode) } } - { - res, err := client.Head(srv.URL() + "/examplechart-0.1.0.tgz") - if err != nil { - t.Error(err) - } + req, err := http.NewRequestWithContext(t.Context(), http.MethodHead, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { t.Errorf("Expected 200, got %d", res.StatusCode) } } - - res, err := client.Get(srv.URL() + "/examplechart-0.1.0.tgz") + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.ContentLength < 500 { t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) } - - res, err = client.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Fatal(err) - } - + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) data, err := io.ReadAll(res.Body) res.Body.Close() - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) m := repo.NewIndexFile() if err := yaml.Unmarshal(data, m); err != nil { t.Fatal(err) } - if l := len(m.Entries); l != 1 { t.Fatalf("Expected 1 entry, got %d", l) } - expect := "examplechart" if !m.Has(expect, "0.1.0") { t.Errorf("missing %q", expect) } - - res, err = client.Get(srv.URL() + "/index.yaml-nosuchthing") + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.StatusCode != http.StatusNotFound { t.Fatalf("Expected 404, got %d", res.StatusCode) } From b42faba9d293d45d2654dac5b540adb5b50e5b4f Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 15:15:19 +1200 Subject: [PATCH 123/132] refactor(repotest): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- pkg/repo/v1/repotest/server.go | 76 +++++++++-------------------- pkg/repo/v1/repotest/server_test.go | 70 +++++++------------------- 2 files changed, 40 insertions(+), 106 deletions(-) diff --git a/pkg/repo/v1/repotest/server.go b/pkg/repo/v1/repotest/server.go index 61dcd6931..06cdc9172 100644 --- a/pkg/repo/v1/repotest/server.go +++ b/pkg/repo/v1/repotest/server.go @@ -30,6 +30,8 @@ import ( "github.com/distribution/distribution/v3/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" "sigs.k8s.io/yaml" @@ -44,9 +46,8 @@ func BasicAuthMiddleware(t *testing.T) http.HandlerFunc { t.Helper() return http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() - if !ok || username != "username" || password != "password" { - t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) - } + assert.True(t, ok && username == "username" && password == "password", + "Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) }) } @@ -97,9 +98,8 @@ func NewTempServer(t *testing.T, options ...ServerOption) *Server { t.Cleanup(func() { os.RemoveAll(srv.docroot) }) if srv.chartSourceGlob != "" { - if _, err := srv.CopyCharts(srv.chartSourceGlob); err != nil { - t.Fatal(err) - } + _, err := srv.CopyCharts(srv.chartSourceGlob) + require.NoError(t, err) } return srv @@ -109,9 +109,7 @@ func NewTempServer(t *testing.T, options ...ServerOption) *Server { func newServer(t *testing.T, docroot string, options ...ServerOption) *Server { t.Helper() absdocroot, err := filepath.Abs(docroot) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) s := &Server{ docroot: absdocroot, @@ -131,9 +129,7 @@ func newServer(t *testing.T, docroot string, options ...ServerOption) *Server { s.start() // Add the testing repository as the only repo. Server must be started for the server's URL to be valid - if err := setTestingRepository(s.URL(), filepath.Join(s.docroot, "repositories.yaml")); err != nil { - t.Fatal(err) - } + require.NoError(t, setTestingRepository(s.URL(), filepath.Join(s.docroot, "repositories.yaml"))) return s } @@ -169,22 +165,16 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { testUsername, testPassword := "username", "password" pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) - if err != nil { - t.Fatal("error generating bcrypt password for test htpasswd file") - } + require.NoError(t, err, "error generating bcrypt password for test htpasswd file") htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename) err = os.WriteFile(htpasswdPath, fmt.Appendf(nil, "%s:%s\n", testUsername, string(pwBytes)), 0o644) - if err != nil { - t.Fatal("error creating test htpasswd file") - } + require.NoError(t, err, "error creating test htpasswd file") // Registry config config := &configuration.Configuration{} lnCfg := net.ListenConfig{} ln, err := lnCfg.Listen(t.Context(), "tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("error finding free port for test registry: %v", err) - } + require.NoError(t, err, "error finding free port for test registry") defer ln.Close() port := ln.Addr().(*net.TCPAddr).Port @@ -201,9 +191,7 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { registryURL := fmt.Sprintf("localhost:%d", port) r, err := registry.NewRegistry(t.Context(), config) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return &OCIServer{ Registry: r, @@ -237,53 +225,37 @@ func (srv *OCIServer) RunWithReturn(t *testing.T, opts ...OCIServerOpt) *OCIServ ociRegistry.ClientOptWriter(os.Stdout), ociRegistry.ClientOptCredentialsFile(credentialsFile), ) - if err != nil { - t.Fatalf("error creating registry client: %v", err) - } + require.NoError(t, err, "error creating registry client") err = registryClient.Login( srv.RegistryURL, ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword), ociRegistry.LoginOptInsecure(true), ociRegistry.LoginOptPlainText(true)) - if err != nil { - t.Fatalf("error logging into registry with good credentials: %v", err) - } + require.NoError(t, err, "error logging into registry with good credentials") ref := srv.RegistryURL + "/u/ocitestuser/oci-dependent-chart:0.1.0" err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // valid chart ch, err := loader.LoadDir(filepath.Join(srv.Dir, "oci-dependent-chart")) - if err != nil { - t.Fatal("error loading chart") - } + require.NoError(t, err, "error loading chart") err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart")) - if err != nil { - t.Fatal("error removing chart before push") - } + require.NoError(t, err, "error removing chart before push") // save it back to disk.. absPath, err := chartutil.Save(ch, srv.Dir) - if err != nil { - t.Fatal("could not create chart archive") - } + require.NoError(t, err, "could not create chart archive") // load it into memory... contentBytes, err := os.ReadFile(absPath) - if err != nil { - t.Fatal("could not load chart into memory") - } + require.NoError(t, err, "could not load chart into memory") result, err := registryClient.Push(contentBytes, ref) - if err != nil { - t.Fatalf("error pushing dependent chart: %s", err) - } + require.NoError(t, err, "error pushing dependent chart") t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ "Config.Digest: %s, Config.Size: %d, "+ "Chart.Digest: %s, Chart.Size: %d", @@ -306,14 +278,10 @@ func (srv *OCIServer) RunWithReturn(t *testing.T, opts ...OCIServerOpt) *OCIServ absPath = filepath.Join(srv.Dir, fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) contentBytes, err = os.ReadFile(absPath) - if err != nil { - t.Fatal("could not load chart into memory") - } + require.NoError(t, err, "could not load chart into memory") result, err = registryClient.Push(contentBytes, dependingRef) - if err != nil { - t.Fatalf("error pushing depending chart: %s", err) - } + require.NoError(t, err, "error pushing depending chart") t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ "Config.Digest: %s, Config.Size: %d, "+ "Chart.Digest: %s, Chart.Size: %d", diff --git a/pkg/repo/v1/repotest/server_test.go b/pkg/repo/v1/repotest/server_test.go index 1364fc9c9..7008e2305 100644 --- a/pkg/repo/v1/repotest/server_test.go +++ b/pkg/repo/v1/repotest/server_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" @@ -40,18 +41,11 @@ func TestServer(t *testing.T) { defer srv.Stop() c, err := srv.CopyCharts("testdata/*.tgz") - if err != nil { - // Some versions of Go don't correctly fire defer on Fatal. - t.Fatal(err) - } + require.NoError(t, err) - if len(c) != 1 { - t.Errorf("Unexpected chart count: %d", len(c)) - } + assert.Len(t, c, 1) - if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { - t.Errorf("Unexpected chart: %s", c[0]) - } + assert.Equal(t, "examplechart-0.1.0.tgz", filepath.Base(c[0])) req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) require.NoError(t, err) @@ -60,9 +54,7 @@ func TestServer(t *testing.T) { require.NoError(t, err) res.Body.Close() - if res.ContentLength < 500 { - t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) - } + assert.GreaterOrEqual(t, res.ContentLength, int64(500)) req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) require.NoError(t, err) @@ -73,27 +65,19 @@ func TestServer(t *testing.T) { require.NoError(t, err) m := repo.NewIndexFile() - if err := yaml.Unmarshal(data, m); err != nil { - t.Fatal(err) - } + require.NoError(t, yaml.Unmarshal(data, m)) - if l := len(m.Entries); l != 1 { - t.Fatalf("Expected 1 entry, got %d", l) - } + require.Len(t, m.Entries, 1) expect := "examplechart" - if !m.Has(expect, "0.1.0") { - t.Errorf("missing %q", expect) - } + assert.True(t, m.Has(expect, "0.1.0"), "missing %q", expect) req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) require.NoError(t, err) res, err = client.Do(req) require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusNotFound { - t.Fatalf("Expected 404, got %d", res.StatusCode) - } + require.Equal(t, http.StatusNotFound, res.StatusCode) } func TestNewTempServer(t *testing.T) { @@ -125,9 +109,7 @@ func TestNewTempServer(t *testing.T) { ) defer srv.Stop() - if srv.srv.URL == "" { - t.Fatal("unstarted server") - } + require.NotEmpty(t, srv.srv.URL, "unstarted server") client := srv.Client() @@ -137,9 +119,7 @@ func TestNewTempServer(t *testing.T) { res, err := client.Do(req) require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected 200, got %d", res.StatusCode) - } + assert.Equal(t, http.StatusOK, res.StatusCode) } { req, err := http.NewRequestWithContext(t.Context(), http.MethodHead, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) @@ -147,18 +127,14 @@ func TestNewTempServer(t *testing.T) { res, err := client.Do(req) require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { - t.Errorf("Expected 200, got %d", res.StatusCode) - } + assert.Equal(t, http.StatusOK, res.StatusCode) } req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) require.NoError(t, err) res, err := client.Do(req) require.NoError(t, err) res.Body.Close() - if res.ContentLength < 500 { - t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) - } + assert.GreaterOrEqual(t, res.ContentLength, int64(500)) req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) require.NoError(t, err) res, err = client.Do(req) @@ -167,24 +143,16 @@ func TestNewTempServer(t *testing.T) { res.Body.Close() require.NoError(t, err) m := repo.NewIndexFile() - if err := yaml.Unmarshal(data, m); err != nil { - t.Fatal(err) - } - if l := len(m.Entries); l != 1 { - t.Fatalf("Expected 1 entry, got %d", l) - } + require.NoError(t, yaml.Unmarshal(data, m)) + require.Len(t, m.Entries, 1) expect := "examplechart" - if !m.Has(expect, "0.1.0") { - t.Errorf("missing %q", expect) - } + assert.True(t, m.Has(expect, "0.1.0"), "missing %q", expect) req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) require.NoError(t, err) res, err = client.Do(req) require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusNotFound { - t.Fatalf("Expected 404, got %d", res.StatusCode) - } + require.Equal(t, http.StatusNotFound, res.StatusCode) }) } } @@ -199,7 +167,5 @@ func TestNewTempServer_TLS(t *testing.T) { ) defer srv.Stop() - if !strings.HasPrefix(srv.URL(), "https://") { - t.Fatal("non-TLS server") - } + require.True(t, strings.HasPrefix(srv.URL(), "https://"), "non-TLS server") } From 3ce02c585a18b38c5dba1177a54c3a0322c53d3c Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 16:26:28 +1200 Subject: [PATCH 124/132] refactor(internal): convert tests to testify assert/require Convert internal/fileutil, internal/tlsutil, and internal/resolver test files from native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) to github.com/stretchr/testify equivalents (assert.X, require.X). Signed-off-by: George Jenkins --- internal/fileutil/fileutil_test.go | 70 +++++++++--------------------- internal/resolver/resolver_test.go | 51 +++++++++------------- internal/tlsutil/tls_test.go | 55 +++++++---------------- 3 files changed, 56 insertions(+), 120 deletions(-) diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 71fcae177..07cbe7695 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -22,6 +22,9 @@ import ( "path/filepath" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestAtomicWriteFile tests the happy path of AtomicWriteFile function. @@ -35,28 +38,17 @@ func TestAtomicWriteFile(t *testing.T) { mode := os.FileMode(0644) err := AtomicWriteFile(testpath, reader, mode) - if err != nil { - t.Errorf("AtomicWriteFile error: %s", err) - } + assert.NoError(t, err) got, err := os.ReadFile(testpath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if stringContent != string(got) { - t.Fatalf("expected: %s, got: %s", stringContent, string(got)) - } + require.Equal(t, stringContent, string(got)) gotinfo, err := os.Stat(testpath) - if err != nil { - t.Fatal(err) - } - - if mode != gotinfo.Mode() { - t.Fatalf("expected %s: to be the same mode as %s", - mode, gotinfo.Mode()) - } + require.NoError(t, err) + + require.Equal(t, mode, gotinfo.Mode()) } // TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails @@ -67,9 +59,7 @@ func TestAtomicWriteFile_CreateTempError(t *testing.T) { mode := os.FileMode(0644) err := AtomicWriteFile(invalidPath, reader, mode) - if err == nil { - t.Error("Expected error when CreateTemp fails, but got nil") - } + assert.Error(t, err, "Expected error when CreateTemp fails") } // TestAtomicWriteFile_EmptyContent tests with empty content @@ -81,18 +71,12 @@ func TestAtomicWriteFile_EmptyContent(t *testing.T) { mode := os.FileMode(0644) err := AtomicWriteFile(testpath, reader, mode) - if err != nil { - t.Errorf("AtomicWriteFile error with empty content: %s", err) - } + assert.NoError(t, err, "AtomicWriteFile error with empty content") got, err := os.ReadFile(testpath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if len(got) != 0 { - t.Fatalf("expected empty content, got: %s", string(got)) - } + require.Empty(t, got) } // TestAtomicWriteFile_LargeContent tests with large content @@ -106,18 +90,12 @@ func TestAtomicWriteFile_LargeContent(t *testing.T) { mode := os.FileMode(0644) err := AtomicWriteFile(testpath, reader, mode) - if err != nil { - t.Errorf("AtomicWriteFile error with large content: %s", err) - } + assert.NoError(t, err, "AtomicWriteFile error with large content") got, err := os.ReadFile(testpath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if largeContent != string(got) { - t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got)) - } + require.Equal(t, largeContent, string(got)) } // TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform @@ -127,21 +105,13 @@ func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) { path := filepath.Join(dir, "overwrite_test") first := bytes.NewReader([]byte("first")) - if err := PlatformAtomicWriteFile(path, first, 0644); err != nil { - t.Fatalf("first write failed: %v", err) - } + require.NoError(t, PlatformAtomicWriteFile(path, first, 0644), "first write failed") second := bytes.NewReader([]byte("second")) - if err := PlatformAtomicWriteFile(path, second, 0644); err != nil { - t.Fatalf("second write failed: %v", err) - } + require.NoError(t, PlatformAtomicWriteFile(path, second, 0644), "second write failed") contents, err := os.ReadFile(path) - if err != nil { - t.Fatalf("failed reading result: %v", err) - } + require.NoError(t, err, "failed reading result") - if string(contents) != "second" { - t.Fatalf("expected file to be overwritten, got %q", string(contents)) - } + require.Equal(t, "second", string(contents)) } diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index 1e33837a9..547419e17 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -19,6 +19,9 @@ import ( "runtime" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/registry" ) @@ -149,34 +152,24 @@ func TestResolve(t *testing.T) { if tt.err { return } - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Fatalf("Expected error in test %q", tt.name) + require.Failf(t, "Expected error in test %q", tt.name) } - if h, err := HashReq(tt.req, tt.expect.Dependencies); err != nil { - t.Fatal(err) - } else if h != l.Digest { - t.Errorf("%q: hashes don't match.", tt.name) - } + h, err := HashReq(tt.req, tt.expect.Dependencies) + require.NoError(t, err) + assert.Equal(t, l.Digest, h, "%q: hashes don't match.", tt.name) // Check fields. - if len(l.Dependencies) != len(tt.req) { - t.Errorf("%s: wrong number of dependencies in lock", tt.name) - } + assert.Len(t, l.Dependencies, len(tt.req), "%s: wrong number of dependencies in lock", tt.name) d0 := l.Dependencies[0] e0 := tt.expect.Dependencies[0] - if d0.Name != e0.Name { - t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name) - } - if d0.Repository != e0.Repository { - t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository) - } - if d0.Version != e0.Version { - t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version) - } + assert.Equal(t, e0.Name, d0.Name, tt.name) + assert.Equal(t, e0.Repository, d0.Repository, tt.name) + assert.Equal(t, e0.Version, d0.Version, tt.name) }) } } @@ -231,13 +224,11 @@ func TestHashReq(t *testing.T) { {Name: "alpine", Version: tt.lockVersion, Repository: "http://localhost:8879/charts"}, } h, err := HashReq(req, lock) - if err != nil { - t.Fatal(err) - } - if !tt.wantError && expect != h { - t.Errorf("Expected %q, got %q", expect, h) - } else if tt.wantError && expect == h { - t.Errorf("Expected not %q, but same", expect) + require.NoError(t, err) + if !tt.wantError { + assert.Equal(t, expect, h) + } else { + assert.NotEqual(t, expect, h, "Expected not %q, but same", expect) } }) } @@ -293,18 +284,16 @@ func TestGetLocalPath(t *testing.T) { if tt.err { return } - t.Fatal(err) + require.NoError(t, err) } if tt.err { - t.Fatalf("Expected error in test %q", tt.name) + require.Failf(t, "Expected error in test %q", tt.name) } expect := tt.expect if runtime.GOOS == "windows" { expect = tt.winExpect } - if p != expect { - t.Errorf("%q: expected %q, got %q", tt.name, expect, p) - } + assert.Equal(t, expect, p, tt.name) }) } } diff --git a/internal/tlsutil/tls_test.go b/internal/tlsutil/tls_test.go index e6859f99f..324b00969 100644 --- a/internal/tlsutil/tls_test.go +++ b/internal/tlsutil/tls_test.go @@ -19,6 +19,9 @@ package tlsutil import ( "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const tlsTestDir = "../../testdata" @@ -32,9 +35,7 @@ const ( func testfile(t *testing.T, file string) (path string) { t.Helper() path, err := filepath.Abs(filepath.Join(tlsTestDir, file)) - if err != nil { - t.Fatalf("error getting absolute path to test file %q: %v", file, err) - } + require.NoError(t, err, "error getting absolute path to test file %q", file) return path } @@ -50,38 +51,22 @@ func TestNewTLSConfig(t *testing.T) { WithCertKeyPairFiles(certFile, keyFile), WithCAFile(caCertFile), ) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatal("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatal("mismatch tls RootCAs, expecting non-nil") - } + require.Len(t, cfg.Certificates, 1) + require.False(t, cfg.InsecureSkipVerify, "insecure skip verify mismatch, expecting false") + require.NotNil(t, cfg.RootCAs, "mismatch tls RootCAs, expecting non-nil") } { cfg, err := NewTLSConfig( WithInsecureSkipVerify(insecureSkipTLSVerify), WithCAFile(caCertFile), ) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) - if got := len(cfg.Certificates); got != 0 { - t.Fatalf("expecting 0 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatal("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatal("mismatch tls RootCAs, expecting non-nil") - } + require.Empty(t, cfg.Certificates) + require.False(t, cfg.InsecureSkipVerify, "insecure skip verify mismatch, expecting false") + require.NotNil(t, cfg.RootCAs, "mismatch tls RootCAs, expecting non-nil") } { @@ -89,18 +74,10 @@ func TestNewTLSConfig(t *testing.T) { WithInsecureSkipVerify(insecureSkipTLSVerify), WithCertKeyPairFiles(certFile, keyFile), ) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatal("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs != nil { - t.Fatal("mismatch tls RootCAs, expecting nil") - } + require.Len(t, cfg.Certificates, 1) + require.False(t, cfg.InsecureSkipVerify, "insecure skip verify mismatch, expecting false") + require.Nil(t, cfg.RootCAs, "mismatch tls RootCAs, expecting nil") } } From 8b0a167484a7f6de4a2cb12bf81641873a275d7b Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 21:36:47 +1200 Subject: [PATCH 125/132] refactor(pkg/registry): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- pkg/registry/chart_test.go | 39 +++++++------------ pkg/registry/client_test.go | 31 +++++---------- pkg/registry/plugin_test.go | 17 +++----- pkg/registry/reference_test.go | 66 ++++++++++--------------------- pkg/registry/tag_test.go | 71 +++++++++------------------------- pkg/registry/transport_test.go | 35 ++++++----------- 6 files changed, 79 insertions(+), 180 deletions(-) diff --git a/pkg/registry/chart_test.go b/pkg/registry/chart_test.go index 0b247601b..23a32506d 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -17,11 +17,12 @@ limitations under the License. package registry // import "helm.sh/helm/v4/pkg/registry" import ( - "reflect" "testing" "time" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -147,10 +148,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { for _, tt := range tests { result := generateChartOCIAnnotations(tt.chart, nowString) - - if !reflect.DeepEqual(tt.expect, result) { - t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) - } + assert.Equal(t, tt.expect, result, tt.name) } } @@ -218,10 +216,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { for _, tt := range tests { result := generateOCIAnnotations(tt.chart, nowString) - - if !reflect.DeepEqual(tt.expect, result) { - t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) - } + assert.Equal(t, tt.expect, result, tt.name) } } @@ -237,29 +232,23 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { result := generateOCIAnnotations(testChart, nowTimeString) // Check that created annotation exists - if _, ok := result[ocispec.AnnotationCreated]; !ok { - t.Errorf("%s annotation not created", ocispec.AnnotationCreated) - } + _, ok := result[ocispec.AnnotationCreated] + assert.True(t, ok, "%s annotation not created", ocispec.AnnotationCreated) // Verify value of created artifact in RFC3339 format - if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { - t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) - } + _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]) + assert.NoError(t, err, "%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) // Verify default creation time set result = generateOCIAnnotations(testChart, "") // Check that created annotation exists - if _, ok := result[ocispec.AnnotationCreated]; !ok { - t.Errorf("%s annotation not created", ocispec.AnnotationCreated) - } + _, ok = result[ocispec.AnnotationCreated] + require.True(t, ok, "%s annotation not created", ocispec.AnnotationCreated) - if createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { - t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) + createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]) + require.NoError(t, err, "%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) - // Verify creation annotation after time test began - if !nowTime.Before(createdTimeAnnotation) { - t.Errorf("%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString) - } - } + // Verify creation annotation after (or equals) time test began + assert.False(t, nowTime.Before(createdTimeAnnotation), "%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString) } diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go index 702dfff69..97b7c7c4c 100644 --- a/pkg/registry/client_test.go +++ b/pkg/registry/client_test.go @@ -25,6 +25,7 @@ import ( "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "oras.land/oras-go/v2/content/memory" ) @@ -77,22 +78,15 @@ func TestLogin_ResetsForceAttemptOAuth2_OnSuccess(t *testing.T) { ClientOptWriter(io.Discard), ClientOptCredentialsFile(credFile), ) - if err != nil { - t.Fatalf("NewClient error: %v", err) - } + require.NoError(t, err, "NewClient error") - if c.authorizer == nil || c.authorizer.ForceAttemptOAuth2 { - t.Fatal("expected ForceAttemptOAuth2 default to be false") - } + require.NotNil(t, c.authorizer) + require.False(t, c.authorizer.ForceAttemptOAuth2, "expected ForceAttemptOAuth2 default to be false") // Call Login with plain HTTP against our test server - if err := c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")); err != nil { - t.Fatalf("Login error: %v", err) - } + require.NoError(t, c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")), "Login error") - if c.authorizer.ForceAttemptOAuth2 { - t.Error("ForceAttemptOAuth2 should be false after successful Login") - } + assert.False(t, c.authorizer.ForceAttemptOAuth2, "ForceAttemptOAuth2 should be false after successful Login") } // Verifies that Login restores ForceAttemptOAuth2 to false even when ping fails. @@ -109,16 +103,12 @@ func TestLogin_ResetsForceAttemptOAuth2_OnFailure(t *testing.T) { ClientOptWriter(io.Discard), ClientOptCredentialsFile(credFile), ) - if err != nil { - t.Fatalf("NewClient error: %v", err) - } + require.NoError(t, err, "NewClient error") // Invoke Login, expect an error but ForceAttemptOAuth2 must end false _ = c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")) - if c.authorizer.ForceAttemptOAuth2 { - t.Error("ForceAttemptOAuth2 should be false after failed Login") - } + assert.False(t, c.authorizer.ForceAttemptOAuth2, "ForceAttemptOAuth2 should be false after failed Login") } // TestWarnIfHostHasPath verifies that warnIfHostHasPath correctly detects path components. @@ -159,10 +149,7 @@ func TestWarnIfHostHasPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := warnIfHostHasPath(tt.host) - if got != tt.wantWarn { - t.Errorf("warnIfHostHasPath(%q) = %v, want %v", tt.host, got, tt.wantWarn) - } + assert.Equal(t, tt.wantWarn, warnIfHostHasPath(tt.host)) }) } } diff --git a/pkg/registry/plugin_test.go b/pkg/registry/plugin_test.go index 61995ece9..bb557c5b8 100644 --- a/pkg/registry/plugin_test.go +++ b/pkg/registry/plugin_test.go @@ -18,6 +18,9 @@ package registry import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetPluginName(t *testing.T) { @@ -74,20 +77,12 @@ func TestGetPluginName(t *testing.T) { pluginName, err := GetPluginName(tt.source) if tt.expectErr { - if err == nil { - t.Error("expected error but got none") - } + assert.Error(t, err, "expected error but got none") return } - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if pluginName != tt.expected { - t.Errorf("expected plugin name %q, got %q", tt.expected, pluginName) - } + require.NoError(t, err) + assert.Equal(t, tt.expected, pluginName) }) } } diff --git a/pkg/registry/reference_test.go b/pkg/registry/reference_test.go index b6872cc37..f1a87fefc 100644 --- a/pkg/registry/reference_test.go +++ b/pkg/registry/reference_test.go @@ -16,31 +16,21 @@ limitations under the License. package registry -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func verify(t *testing.T, actual reference, registry, repository, tag, digest string) { t.Helper() - if registry != actual.orasReference.Registry { - t.Errorf("Oras reference registry expected %v actual %v", registry, actual.Registry) - } - if repository != actual.orasReference.Repository { - t.Errorf("Oras reference repository expected %v actual %v", repository, actual.Repository) - } - if tag != actual.orasReference.Reference { - t.Errorf("Oras reference reference expected %v actual %v", tag, actual.Tag) - } - if registry != actual.Registry { - t.Errorf("Registry expected %v actual %v", registry, actual.Registry) - } - if repository != actual.Repository { - t.Errorf("Repository expected %v actual %v", repository, actual.Repository) - } - if tag != actual.Tag { - t.Errorf("Tag expected %v actual %v", tag, actual.Tag) - } - if digest != actual.Digest { - t.Errorf("Digest expected %v actual %v", digest, actual.Digest) - } + assert.Equal(t, registry, actual.orasReference.Registry, "Oras reference registry") + assert.Equal(t, repository, actual.orasReference.Repository, "Oras reference repository") + assert.Equal(t, tag, actual.orasReference.Reference, "Oras reference reference") + assert.Equal(t, registry, actual.Registry, "Registry") + assert.Equal(t, repository, actual.Repository, "Repository") + assert.Equal(t, tag, actual.Tag, "Tag") + assert.Equal(t, digest, actual.Digest, "Digest") expectedString := registry if repository != "" { expectedString = expectedString + "/" + repository @@ -50,51 +40,35 @@ func verify(t *testing.T, actual reference, registry, repository, tag, digest st } else { expectedString = expectedString + "@" + digest } - if actual.String() != expectedString { - t.Errorf("String expected %s actual %s", expectedString, actual.String()) - } + assert.Equal(t, expectedString, actual.String(), "String") } func TestNewReference(t *testing.T) { actual, err := newReference("registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") actual, err = newReference("oci://registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") actual, err = newReference("a/b:1@c") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "a", "b", "1", "c") actual, err = newReference("a/b:@") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "a", "b", "", "") actual, err = newReference("registry.example.com/repository:1.0+001") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "registry.example.com", "repository", "1.0_001", "") actual, err = newReference("thing:1.0") - if err == nil { - t.Errorf("Expect error error %v", err) - } + assert.Error(t, err) verify(t, actual, "", "", "", "") actual, err = newReference("registry.example.com/the/repository@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") - if err != nil { - t.Errorf("Unexpected error %v", err) - } + assert.NoError(t, err) verify(t, actual, "registry.example.com", "the/repository", "", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") } diff --git a/pkg/registry/tag_test.go b/pkg/registry/tag_test.go index e3ec47703..830eac7e1 100644 --- a/pkg/registry/tag_test.go +++ b/pkg/registry/tag_test.go @@ -17,32 +17,25 @@ limitations under the License. package registry import ( - "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestGetTagMatchingVersionOrConstraint_ExactMatch(t *testing.T) { tags := []string{"1.0.0", "1.2.3", "2.0.0"} got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.2.3" { - t.Fatalf("expected exact match '1.2.3', got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.2.3", got, "expected exact match") } func TestGetTagMatchingVersionOrConstraint_EmptyVersionWildcard(t *testing.T) { // Includes a non-semver tag which should be skipped tags := []string{"latest", "0.9.0", "1.0.0"} got, err := GetTagMatchingVersionOrConstraint(tags, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + require.NoError(t, err) // Should pick the first valid semver tag in order, which is 0.9.0 - if got != "0.9.0" { - t.Fatalf("expected '0.9.0', got %q", got) - } + require.Equal(t, "0.9.0", got) } func TestGetTagMatchingVersionOrConstraint_ConstraintRange(t *testing.T) { @@ -50,73 +43,47 @@ func TestGetTagMatchingVersionOrConstraint_ConstraintRange(t *testing.T) { // Caret range got, err := GetTagMatchingVersionOrConstraint(tags, "^1.0.0") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.0.0" { // first match in order - t.Fatalf("expected '1.0.0', got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.0.0", got, "first match in order") // Compound range got, err = GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.0.0" { - t.Fatalf("expected '1.0.0', got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.0.0", got) } func TestGetTagMatchingVersionOrConstraint_InvalidConstraint(t *testing.T) { tags := []string{"1.0.0"} _, err := GetTagMatchingVersionOrConstraint(tags, ">a1") - if err == nil { - t.Fatal("expected error for invalid constraint") - } + require.Error(t, err, "expected error for invalid constraint") } func TestGetTagMatchingVersionOrConstraint_NoMatches(t *testing.T) { tags := []string{"0.1.0", "0.2.0"} _, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0") - if err == nil { - t.Fatal("expected error when no tags match") - } - if !strings.Contains(err.Error(), ">=1.0.0") { - t.Fatalf("expected error to contain version string, got: %v", err) - } + require.Error(t, err, "expected error when no tags match") + require.Contains(t, err.Error(), ">=1.0.0", "expected error to contain version string") } func TestGetTagMatchingVersionOrConstraint_SkipsNonSemverTags(t *testing.T) { tags := []string{"alpha", "1.0.0", "beta", "1.1.0"} got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.0.0" { - t.Fatalf("expected '1.0.0', got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.0.0", got) } func TestGetTagMatchingVersionOrConstraint_OrderMatters_FirstMatchReturned(t *testing.T) { // Both 1.2.0 and 1.3.0 satisfy >=1.2.0 <2.0.0, but the function returns the first in input order tags := []string{"1.3.0", "1.2.0"} got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.2.0 <2.0.0") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.3.0" { - t.Fatalf("expected '1.3.0' (first satisfying tag), got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.3.0", got, "first satisfying tag") } func TestGetTagMatchingVersionOrConstraint_ExactMatchHasPrecedence(t *testing.T) { // Exact match should be returned even if another earlier tag would match the parsed constraint tags := []string{"1.3.0", "1.2.3"} got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "1.2.3" { - t.Fatalf("expected exact match '1.2.3', got %q", got) - } + require.NoError(t, err) + require.Equal(t, "1.2.3", got, "expected exact match") } diff --git a/pkg/registry/transport_test.go b/pkg/registry/transport_test.go index b4990c526..e72c97c0d 100644 --- a/pkg/registry/transport_test.go +++ b/pkg/registry/transport_test.go @@ -22,6 +22,9 @@ import ( "io" "net/http" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var errMockRead = errors.New("mock read error") @@ -127,9 +130,7 @@ func Test_isPrintableContentType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isPrintableContentType(tt.contentType); got != tt.want { - t.Errorf("isPrintableContentType() = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, isPrintableContentType(tt.contentType)) }) } } @@ -292,21 +293,13 @@ func Test_logResponseBody(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := logResponseBody(tt.resp); got != tt.want { - t.Errorf("logResponseBody() = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, logResponseBody(tt.resp)) // validate the response body if tt.resp.Body != nil { readBytes, err := io.ReadAll(tt.resp.Body) - if err != nil { - t.Errorf("failed to read body after logResponseBody(), err= %v", err) - } - if !bytes.Equal(readBytes, tt.wantData) { - t.Errorf("resp.Body after logResponseBody() = %v, want %v", readBytes, tt.wantData) - } - if closeErr := tt.resp.Body.Close(); closeErr != nil { - t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr) - } + require.NoError(t, err, "failed to read body after logResponseBody()") + assert.True(t, bytes.Equal(tt.wantData, readBytes), "resp.Body after logResponseBody()") + assert.NoError(t, tt.resp.Body.Close(), "failed to close body after logResponseBody()") } }) } @@ -331,12 +324,8 @@ func Test_logResponseBody_error(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := logResponseBody(tt.resp); got != tt.want { - t.Errorf("logResponseBody() = %v, want %v", got, tt.want) - } - if closeErr := tt.resp.Body.Close(); closeErr != nil { - t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr) - } + assert.Equal(t, tt.want, logResponseBody(tt.resp)) + assert.NoError(t, tt.resp.Body.Close(), "failed to close body after logResponseBody()") }) } } @@ -391,9 +380,7 @@ func Test_containsCredentials(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := containsCredentials(tt.body); got != tt.want { - t.Errorf("containsCredentials() = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, containsCredentials(tt.body)) }) } } From 4cce0e9e2a870508864c1d497918e201dc0f339c Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 21:32:52 +1200 Subject: [PATCH 126/132] refactor(pkg/release): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- pkg/release/v1/util/filter_test.go | 29 ++++++---------- pkg/release/v1/util/kind_sorter_test.go | 35 ++++++------------- pkg/release/v1/util/manifest_sorter_test.go | 38 ++++++--------------- pkg/release/v1/util/manifest_test.go | 6 ++-- pkg/release/v1/util/sorter_test.go | 6 ++-- 5 files changed, 38 insertions(+), 76 deletions(-) diff --git a/pkg/release/v1/util/filter_test.go b/pkg/release/v1/util/filter_test.go index 1004a4c57..d6bda8f84 100644 --- a/pkg/release/v1/util/filter_test.go +++ b/pkg/release/v1/util/filter_test.go @@ -19,23 +19,19 @@ package util // import "helm.sh/helm/v4/pkg/release/v1/util" import ( "testing" + "github.com/stretchr/testify/require" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) func TestFilterAny(t *testing.T) { ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases) - if len(ls) != 2 { - t.Fatalf("expected 2 results, got '%d'", len(ls)) - } + require.Len(t, ls, 2) r0, r1 := ls[0], ls[1] - switch { - case r0.Info.Status != common.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - case r1.Info.Status != common.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - } + require.Equal(t, common.StatusUninstalled, r0.Info.Status) + require.Equal(t, common.StatusUninstalled, r1.Info.Status) } func TestFilterAll(t *testing.T) { @@ -47,14 +43,9 @@ func TestFilterAll(t *testing.T) { }) ls := All(fn).Filter(releases) - if len(ls) != 1 { - t.Fatalf("expected 1 result, got '%d'", len(ls)) - } - - switch r0 := ls[0]; { - case r0.Version == 4: - t.Fatal("got release with status revision 4") - case r0.Info.Status == common.StatusUninstalled: - t.Fatal("got release with status UNINSTALLED") - } + require.Len(t, ls, 1) + + r0 := ls[0] + require.NotEqual(t, 4, r0.Version, "got release with status revision 4") + require.NotEqual(t, common.StatusUninstalled, r0.Info.Status, "got release with status UNINSTALLED") } diff --git a/pkg/release/v1/util/kind_sorter_test.go b/pkg/release/v1/util/kind_sorter_test.go index 919de24e5..793ef87fe 100644 --- a/pkg/release/v1/util/kind_sorter_test.go +++ b/pkg/release/v1/util/kind_sorter_test.go @@ -20,6 +20,9 @@ import ( "bytes" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + release "helm.sh/helm/v4/pkg/release/v1" ) @@ -193,21 +196,15 @@ func TestKindSorter(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(manifests); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } + require.Equal(t, len(manifests), len(test.expected), "Expected %d names in order", len(manifests)) defer buf.Reset() orig := manifests for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } + assert.Equal(t, test.expected, buf.String()) for i, manifest := range orig { - if manifest != manifests[i] { - t.Fatal("Expected input to sortManifestsByKind to stay the same") - } + require.Equal(t, manifest, manifests[i], "Expected input to sortManifestsByKind to stay the same") } }) } @@ -267,9 +264,7 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) { for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } + assert.Equal(t, test.expected, buf.String()) }) } } @@ -289,9 +284,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { expectedOrder := []Manifest{namespace, unknown} for i, manifest := range manifests { - if expectedOrder[i].Name != manifest.Name { - t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name) - } + assert.Equal(t, expectedOrder[i].Name, manifest.Name) } } @@ -326,22 +319,16 @@ func TestKindSorterForHooks(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(hooks); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } + require.Equal(t, len(hooks), len(test.expected), "Expected %d names in order", len(hooks)) defer buf.Reset() orig := hooks for _, r := range sortHooksByKind(hooks, test.order) { buf.WriteString(r.Name) } for i, hook := range orig { - if hook != hooks[i] { - t.Fatal("Expected input to sortHooksByKind to stay the same") - } - } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) + require.Equal(t, hook, hooks[i], "Expected input to sortHooksByKind to stay the same") } + assert.Equal(t, test.expected, buf.String()) }) } } diff --git a/pkg/release/v1/util/manifest_sorter_test.go b/pkg/release/v1/util/manifest_sorter_test.go index baa320be0..2cdb55dc3 100644 --- a/pkg/release/v1/util/manifest_sorter_test.go +++ b/pkg/release/v1/util/manifest_sorter_test.go @@ -20,6 +20,8 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" release "helm.sh/helm/v4/pkg/release/v1" @@ -138,55 +140,39 @@ metadata: } hs, generic, err := SortManifests(manifests, nil, InstallOrder) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } + require.NoError(t, err) // This test will fail if 'six' or 'seven' was added. - if len(generic) != 2 { - t.Errorf("Expected 2 generic manifests, got %d", len(generic)) - } + assert.Len(t, generic, 2) - if len(hs) != 4 { - t.Errorf("Expected 4 hooks, got %d", len(hs)) - } + assert.Len(t, hs, 4) for _, out := range hs { found := false for _, expect := range data { if out.Path == expect.path { found = true - if out.Path != expect.path { - t.Errorf("Expected path %s, got %s", expect.path, out.Path) - } + assert.Equal(t, expect.path, out.Path) nameFound := false for _, expectedName := range expect.name { if out.Name == expectedName { nameFound = true } } - if !nameFound { - t.Errorf("Got unexpected name %s", out.Name) - } + assert.True(t, nameFound, "Got unexpected name %s", out.Name) kindFound := false for _, expectedKind := range expect.kind { if out.Kind == expectedKind { kindFound = true } } - if !kindFound { - t.Errorf("Got unexpected kind %s", out.Kind) - } + assert.True(t, kindFound, "Got unexpected kind %s", out.Kind) expectedHooks := expect.hooks[out.Name] - if !reflect.DeepEqual(expectedHooks, out.Events) { - t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) - } + assert.True(t, reflect.DeepEqual(expectedHooks, out.Events), "expected events: %v but got: %v", expectedHooks, out.Events) } } - if !found { - t.Errorf("Result not found: %v", out) - } + assert.True(t, found, "Result not found: %v", out) } // Verify the sort order @@ -218,8 +204,6 @@ metadata: sorted = sortManifestsByKind(sorted, InstallOrder) for i, m := range generic { - if m.Content != sorted[i].Content { - t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) - } + assert.Equal(t, sorted[i].Content, m.Content) } } diff --git a/pkg/release/v1/util/manifest_test.go b/pkg/release/v1/util/manifest_test.go index 516ac42d7..6bcf902d7 100644 --- a/pkg/release/v1/util/manifest_test.go +++ b/pkg/release/v1/util/manifest_test.go @@ -19,6 +19,8 @@ package util // import "helm.sh/helm/v4/pkg/release/v1/util" import ( "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestSplitManifests(t *testing.T) { @@ -509,9 +511,7 @@ metadata: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := SplitManifests(tt.input) - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) - } + assert.True(t, reflect.DeepEqual(result, tt.expected), "SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) }) } } diff --git a/pkg/release/v1/util/sorter_test.go b/pkg/release/v1/util/sorter_test.go index f47db7db8..0450b09e6 100644 --- a/pkg/release/v1/util/sorter_test.go +++ b/pkg/release/v1/util/sorter_test.go @@ -20,6 +20,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -45,9 +47,7 @@ func tsRelease(name string, vers int, dur time.Duration, status common.Status) * func check(t *testing.T, by string, fn func(int, int) bool) { t.Helper() for i := len(releases) - 1; i > 0; i-- { - if fn(i, i-1) { - t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by) - } + assert.False(t, fn(i, i-1), "release at positions '(%d,%d)' not sorted by %s", i-1, i, by) } } From 7ab295fcfcf3977599939d76ffbf7a266401bfbc Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 17 Jun 2026 21:48:26 +1200 Subject: [PATCH 127/132] refactor(internal/release): convert tests to testify assert/require Replace native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) with github.com/stretchr/testify equivalents (assert.X, require.X) for improved test readability and error messages. Signed-off-by: George Jenkins --- internal/release/v2/util/filter_test.go | 29 +++++--------- internal/release/v2/util/kind_sorter_test.go | 35 ++++++----------- .../release/v2/util/manifest_sorter_test.go | 39 ++++++------------- internal/release/v2/util/manifest_test.go | 7 ++-- internal/release/v2/util/sorter_test.go | 6 +-- pkg/release/v1/util/kind_sorter_test.go | 4 +- pkg/release/v1/util/manifest_sorter_test.go | 5 +-- pkg/release/v1/util/manifest_test.go | 3 +- 8 files changed, 43 insertions(+), 85 deletions(-) diff --git a/internal/release/v2/util/filter_test.go b/internal/release/v2/util/filter_test.go index 35236498a..8c7363b87 100644 --- a/internal/release/v2/util/filter_test.go +++ b/internal/release/v2/util/filter_test.go @@ -19,23 +19,19 @@ package util // import "helm.sh/helm/v4/internal/release/v2/util" import ( "testing" + "github.com/stretchr/testify/require" + rspb "helm.sh/helm/v4/internal/release/v2" "helm.sh/helm/v4/pkg/release/common" ) func TestFilterAny(t *testing.T) { ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases) - if len(ls) != 2 { - t.Fatalf("expected 2 results, got '%d'", len(ls)) - } + require.Len(t, ls, 2) r0, r1 := ls[0], ls[1] - switch { - case r0.Info.Status != common.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - case r1.Info.Status != common.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - } + require.Equal(t, common.StatusUninstalled, r0.Info.Status) + require.Equal(t, common.StatusUninstalled, r1.Info.Status) } func TestFilterAll(t *testing.T) { @@ -47,14 +43,9 @@ func TestFilterAll(t *testing.T) { }) ls := All(fn).Filter(releases) - if len(ls) != 1 { - t.Fatalf("expected 1 result, got '%d'", len(ls)) - } - - switch r0 := ls[0]; { - case r0.Version == 4: - t.Fatal("got release with status revision 4") - case r0.Info.Status == common.StatusUninstalled: - t.Fatal("got release with status UNINSTALLED") - } + require.Len(t, ls, 1) + + r0 := ls[0] + require.NotEqual(t, 4, r0.Version, "got release with status revision 4") + require.NotEqual(t, common.StatusUninstalled, r0.Info.Status, "got release with status UNINSTALLED") } diff --git a/internal/release/v2/util/kind_sorter_test.go b/internal/release/v2/util/kind_sorter_test.go index 06418a5fc..dba19e287 100644 --- a/internal/release/v2/util/kind_sorter_test.go +++ b/internal/release/v2/util/kind_sorter_test.go @@ -20,6 +20,9 @@ import ( "bytes" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + release "helm.sh/helm/v4/internal/release/v2" ) @@ -193,21 +196,15 @@ func TestKindSorter(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(manifests); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } + require.Len(t, manifests, len(test.expected), "Expected %d names in order", len(manifests)) defer buf.Reset() orig := manifests for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } + assert.Equal(t, test.expected, buf.String()) for i, manifest := range orig { - if manifest != manifests[i] { - t.Fatal("Expected input to sortManifestsByKind to stay the same") - } + require.Equal(t, manifest, manifests[i], "Expected input to sortManifestsByKind to stay the same") } }) } @@ -267,9 +264,7 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) { for _, r := range sortManifestsByKind(manifests, test.order) { buf.WriteString(r.Name) } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } + assert.Equal(t, test.expected, buf.String()) }) } } @@ -289,9 +284,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { expectedOrder := []Manifest{namespace, unknown} for i, manifest := range manifests { - if expectedOrder[i].Name != manifest.Name { - t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name) - } + assert.Equal(t, expectedOrder[i].Name, manifest.Name) } } @@ -326,22 +319,16 @@ func TestKindSorterForHooks(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(hooks); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } + require.Len(t, hooks, len(test.expected), "Expected %d names in order", len(hooks)) defer buf.Reset() orig := hooks for _, r := range sortHooksByKind(hooks, test.order) { buf.WriteString(r.Name) } for i, hook := range orig { - if hook != hooks[i] { - t.Fatal("Expected input to sortHooksByKind to stay the same") - } - } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) + require.Equal(t, hook, hooks[i], "Expected input to sortHooksByKind to stay the same") } + assert.Equal(t, test.expected, buf.String()) }) } } diff --git a/internal/release/v2/util/manifest_sorter_test.go b/internal/release/v2/util/manifest_sorter_test.go index c8851d678..1c932d70c 100644 --- a/internal/release/v2/util/manifest_sorter_test.go +++ b/internal/release/v2/util/manifest_sorter_test.go @@ -17,9 +17,10 @@ limitations under the License. package util // import "helm.sh/helm/v4/internal/release/v2/util" import ( - "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" release "helm.sh/helm/v4/internal/release/v2" @@ -138,55 +139,39 @@ metadata: } hs, generic, err := SortManifests(manifests, nil, InstallOrder) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } + require.NoError(t, err) // This test will fail if 'six' or 'seven' was added. - if len(generic) != 2 { - t.Errorf("Expected 2 generic manifests, got %d", len(generic)) - } + assert.Len(t, generic, 2) - if len(hs) != 4 { - t.Errorf("Expected 4 hooks, got %d", len(hs)) - } + assert.Len(t, hs, 4) for _, out := range hs { found := false for _, expect := range data { if out.Path == expect.path { found = true - if out.Path != expect.path { - t.Errorf("Expected path %s, got %s", expect.path, out.Path) - } + assert.Equal(t, expect.path, out.Path) nameFound := false for _, expectedName := range expect.name { if out.Name == expectedName { nameFound = true } } - if !nameFound { - t.Errorf("Got unexpected name %s", out.Name) - } + assert.True(t, nameFound, "Got unexpected name %s", out.Name) kindFound := false for _, expectedKind := range expect.kind { if out.Kind == expectedKind { kindFound = true } } - if !kindFound { - t.Errorf("Got unexpected kind %s", out.Kind) - } + assert.True(t, kindFound, "Got unexpected kind %s", out.Kind) expectedHooks := expect.hooks[out.Name] - if !reflect.DeepEqual(expectedHooks, out.Events) { - t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) - } + assert.Equal(t, expectedHooks, out.Events, "expected events: %v but got: %v", expectedHooks, out.Events) } } - if !found { - t.Errorf("Result not found: %v", out) - } + assert.True(t, found, "Result not found: %v", out) } // Verify the sort order @@ -218,8 +203,6 @@ metadata: sorted = sortManifestsByKind(sorted, InstallOrder) for i, m := range generic { - if m.Content != sorted[i].Content { - t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) - } + assert.Equal(t, m.Content, sorted[i].Content) } } diff --git a/internal/release/v2/util/manifest_test.go b/internal/release/v2/util/manifest_test.go index 72b095390..463ab5a4d 100644 --- a/internal/release/v2/util/manifest_test.go +++ b/internal/release/v2/util/manifest_test.go @@ -17,8 +17,9 @@ limitations under the License. package util // import "helm.sh/helm/v4/internal/release/v2/util" import ( - "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestSplitManifests(t *testing.T) { @@ -509,9 +510,7 @@ metadata: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := SplitManifests(tt.input) - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) - } + assert.Equal(t, tt.expected, result, "SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) }) } } diff --git a/internal/release/v2/util/sorter_test.go b/internal/release/v2/util/sorter_test.go index 6cb876f69..82a53ae66 100644 --- a/internal/release/v2/util/sorter_test.go +++ b/internal/release/v2/util/sorter_test.go @@ -20,6 +20,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + rspb "helm.sh/helm/v4/internal/release/v2" "helm.sh/helm/v4/pkg/release/common" ) @@ -45,9 +47,7 @@ func tsRelease(name string, vers int, dur time.Duration, status common.Status) * func check(t *testing.T, by string, fn func(int, int) bool) { t.Helper() for i := len(releases) - 1; i > 0; i-- { - if fn(i, i-1) { - t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by) - } + assert.False(t, fn(i, i-1), "release at positions '(%d,%d)' not sorted by %s", i-1, i, by) } } diff --git a/pkg/release/v1/util/kind_sorter_test.go b/pkg/release/v1/util/kind_sorter_test.go index 793ef87fe..de8a79b25 100644 --- a/pkg/release/v1/util/kind_sorter_test.go +++ b/pkg/release/v1/util/kind_sorter_test.go @@ -196,7 +196,7 @@ func TestKindSorter(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - require.Equal(t, len(manifests), len(test.expected), "Expected %d names in order", len(manifests)) + require.Len(t, manifests, len(test.expected), "Expected %d names in order", len(manifests)) defer buf.Reset() orig := manifests for _, r := range sortManifestsByKind(manifests, test.order) { @@ -319,7 +319,7 @@ func TestKindSorterForHooks(t *testing.T) { } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { - require.Equal(t, len(hooks), len(test.expected), "Expected %d names in order", len(hooks)) + require.Len(t, hooks, len(test.expected), "Expected %d names in order", len(hooks)) defer buf.Reset() orig := hooks for _, r := range sortHooksByKind(hooks, test.order) { diff --git a/pkg/release/v1/util/manifest_sorter_test.go b/pkg/release/v1/util/manifest_sorter_test.go index 2cdb55dc3..b02400938 100644 --- a/pkg/release/v1/util/manifest_sorter_test.go +++ b/pkg/release/v1/util/manifest_sorter_test.go @@ -17,7 +17,6 @@ limitations under the License. package util import ( - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -169,7 +168,7 @@ metadata: assert.True(t, kindFound, "Got unexpected kind %s", out.Kind) expectedHooks := expect.hooks[out.Name] - assert.True(t, reflect.DeepEqual(expectedHooks, out.Events), "expected events: %v but got: %v", expectedHooks, out.Events) + assert.Equal(t, expectedHooks, out.Events, "expected events: %v but got: %v", expectedHooks, out.Events) } } assert.True(t, found, "Result not found: %v", out) @@ -204,6 +203,6 @@ metadata: sorted = sortManifestsByKind(sorted, InstallOrder) for i, m := range generic { - assert.Equal(t, sorted[i].Content, m.Content) + assert.Equal(t, m.Content, sorted[i].Content) } } diff --git a/pkg/release/v1/util/manifest_test.go b/pkg/release/v1/util/manifest_test.go index 6bcf902d7..d1efa21e1 100644 --- a/pkg/release/v1/util/manifest_test.go +++ b/pkg/release/v1/util/manifest_test.go @@ -17,7 +17,6 @@ limitations under the License. package util // import "helm.sh/helm/v4/pkg/release/v1/util" import ( - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -511,7 +510,7 @@ metadata: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := SplitManifests(tt.input) - assert.True(t, reflect.DeepEqual(result, tt.expected), "SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) + assert.Equal(t, tt.expected, result, "SplitManifests() =\n%v\nwant:\n%v", result, tt.expected) }) } } From addbab2e6444954eb1a9bde86730ee7d11810b46 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Tue, 16 Jun 2026 15:49:51 +1200 Subject: [PATCH 128/132] refactor(internal): convert tests to testify assert/require Convert internal/urlutil, internal/cli/output, internal/monocular, and internal/sympath test files from native Go testing patterns (t.Errorf, t.Fatalf, t.Error, t.Fatal) to github.com/stretchr/testify equivalents (assert.X, require.X). Signed-off-by: George Jenkins --- internal/cli/output/color_test.go | 18 +++++++---------- internal/monocular/client_test.go | 11 +++++------ internal/monocular/search_test.go | 15 ++++++--------- internal/sympath/walk_test.go | 32 +++++++++++-------------------- internal/urlutil/urlutil_test.go | 23 +++++++++++----------- 5 files changed, 40 insertions(+), 59 deletions(-) diff --git a/internal/cli/output/color_test.go b/internal/cli/output/color_test.go index f0484fc3a..807086c62 100644 --- a/internal/cli/output/color_test.go +++ b/internal/cli/output/color_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release/common" ) @@ -93,14 +95,12 @@ func TestColorizeStatus(t *testing.T) { // In test environment, term.IsTerminal will be false, so we won't get color // unless we're testing the logic without terminal detection - if hasColor && !tt.wantColor { - t.Errorf("ColorizeStatus() returned color when none expected: %q", result) + if hasColor { + assert.True(t, tt.wantColor, "ColorizeStatus() returned color when none expected: %q", result) } // Always check the status text is present - if !strings.Contains(result, tt.status.String()) { - t.Errorf("ColorizeStatus() = %q, want to contain %q", result, tt.status.String()) - } + assert.Contains(t, result, tt.status.String()) }) } } @@ -139,9 +139,7 @@ func TestColorizeHeader(t *testing.T) { result := ColorizeHeader(tt.header, tt.noColor) // Always check the header text is present - if !strings.Contains(result, tt.header) { - t.Errorf("ColorizeHeader() = %q, want to contain %q", result, tt.header) - } + assert.Contains(t, result, tt.header) }) } } @@ -180,9 +178,7 @@ func TestColorizeNamespace(t *testing.T) { result := ColorizeNamespace(tt.namespace, tt.noColor) // Always check the namespace text is present - if !strings.Contains(result, tt.namespace) { - t.Errorf("ColorizeNamespace() = %q, want to contain %q", result, tt.namespace) - } + assert.Contains(t, result, tt.namespace) }) } } diff --git a/internal/monocular/client_test.go b/internal/monocular/client_test.go index abf914ef5..6456557d8 100644 --- a/internal/monocular/client_test.go +++ b/internal/monocular/client_test.go @@ -18,14 +18,13 @@ package monocular import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { c, err := New("https://hub.helm.sh") - if err != nil { - t.Errorf("error creating client: %s", err) - } - if c.BaseURL != "https://hub.helm.sh" { - t.Errorf("incorrect BaseURL. Expected \"https://hub.helm.sh\" but got %q", c.BaseURL) - } + require.NoError(t, err) + assert.Equal(t, "https://hub.helm.sh", c.BaseURL) } diff --git a/internal/monocular/search_test.go b/internal/monocular/search_test.go index e4475f24e..85327cba7 100644 --- a/internal/monocular/search_test.go +++ b/internal/monocular/search_test.go @@ -21,6 +21,9 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // A search response for phpmyadmin containing 2 results @@ -33,16 +36,10 @@ func TestSearch(t *testing.T) { defer ts.Close() c, err := New(ts.URL) - if err != nil { - t.Errorf("unable to create monocular client: %s", err) - } + require.NoError(t, err, "unable to create monocular client") results, err := c.SearchWithContext(t.Context(), "phpmyadmin") - if err != nil { - t.Errorf("unable to search monocular: %s", err) - } + require.NoError(t, err, "unable to search monocular") - if len(results) != 2 { - t.Error("Did not receive the expected number of results") - } + assert.Len(t, results, 2) } diff --git a/internal/sympath/walk_test.go b/internal/sympath/walk_test.go index 1eba8b996..b5bffbdab 100644 --- a/internal/sympath/walk_test.go +++ b/internal/sympath/walk_test.go @@ -24,6 +24,9 @@ import ( "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type Node struct { @@ -80,21 +83,14 @@ func makeTree(t *testing.T) { walkTree(tree, tree.name, func(path string, n *Node) { if n.entries == nil { if n.symLinkedTo != "" { - if err := os.Symlink(n.symLinkedTo, path); err != nil { - t.Fatalf("makeTree: %v", err) - } + require.NoError(t, os.Symlink(n.symLinkedTo, path), "makeTree") } else { fd, err := os.Create(path) - if err != nil { - t.Fatalf("makeTree: %v", err) - return - } + require.NoError(t, err, "makeTree") fd.Close() } } else { - if err := os.Mkdir(path, 0770); err != nil { - t.Fatalf("makeTree: %v", err) - } + require.NoError(t, os.Mkdir(path, 0770), "makeTree") } }) } @@ -102,8 +98,8 @@ func makeTree(t *testing.T) { func checkMarks(t *testing.T, report bool) { t.Helper() walkTree(tree, tree.name, func(path string, n *Node) { - if n.marks != n.expectedMarks && report { - t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks) + if report { + assert.Equal(t, n.expectedMarks, n.marks, "node %s", path) } n.marks = 0 }) @@ -137,16 +133,10 @@ func TestWalk(t *testing.T) { } // Expect no errors. err := Walk(tree.name, markFn) - if err != nil { - t.Fatalf("no error expected, found: %s", err) - } - if len(errors) != 0 { - t.Fatalf("unexpected errors: %s", errors) - } + require.NoError(t, err) + require.Empty(t, errors, "unexpected errors") checkMarks(t, true) // cleanup - if err := os.RemoveAll(tree.name); err != nil { - t.Errorf("removeTree: %v", err) - } + assert.NoError(t, os.RemoveAll(tree.name), "removeTree") } diff --git a/internal/urlutil/urlutil_test.go b/internal/urlutil/urlutil_test.go index 82acc40fe..d6b3f5256 100644 --- a/internal/urlutil/urlutil_test.go +++ b/internal/urlutil/urlutil_test.go @@ -16,7 +16,11 @@ limitations under the License. package urlutil -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestURLJoin(t *testing.T) { tests := []struct { @@ -31,11 +35,9 @@ func TestURLJoin(t *testing.T) { } for _, tt := range tests { - if got, err := URLJoin(tt.url, tt.paths...); err != nil { - t.Errorf("%s: error %q", tt.name, err) - } else if got != tt.expect { - t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) - } + got, err := URLJoin(tt.url, tt.paths...) + assert.NoError(t, err, tt.name) + assert.Equal(t, tt.expect, got, tt.name) } } @@ -60,9 +62,7 @@ func TestEqual(t *testing.T) { {"%/1234", "%/123", false}, {"/1234", "%/1234", false}, } { - if tt.match != Equal(tt.a, tt.b) { - t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match) - } + assert.Equal(t, tt.match, Equal(tt.a, tt.b), "Expected %q==%q to be %t", tt.a, tt.b, tt.match) } } @@ -74,8 +74,7 @@ func TestExtractHostname(t *testing.T) { "https://example.com:31337/not/with/a/bang/but/a/whimper": "example.com", } for start, expect := range tests { - if got, _ := ExtractHostname(start); got != expect { - t.Errorf("Got %q, expected %q", got, expect) - } + got, _ := ExtractHostname(start) + assert.Equal(t, expect, got) } } From 07faec5593aabbde72c357d9c10123b6a3da33c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:32:37 +0000 Subject: [PATCH 129/132] chore(deps): bump actions/setup-go from 6.4.0 to 6.5.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.4.0 to 6.5.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...924ae3a1cded613372ab5595356fb5720e22ba16) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build-test.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/govulncheck.yml | 2 +- .github/workflows/release.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a0fea406c..b4ce89f26 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -22,7 +22,7 @@ jobs: - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0 with: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 13ea569ef..e13d8950e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,7 +17,7 @@ jobs: - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0 with: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index ba3714eae..93ab559fc 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -25,7 +25,7 @@ jobs: - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0 with: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 993c45899..ee2612917 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0 with: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true @@ -93,7 +93,7 @@ jobs: run: cat ".github/env" >> "$GITHUB_ENV" - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0 with: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true From 144246cd2893f92ee8e477c691837ea27e8c2733 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Mon, 22 Jun 2026 19:31:18 -0700 Subject: [PATCH 130/132] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: George Jenkins --- pkg/cli/environment_test.go | 3 +-- pkg/cli/values/options_test.go | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index f1067f6c6..9303389a3 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -18,7 +18,6 @@ package cli import ( "os" - "reflect" "strings" "testing" @@ -137,7 +136,7 @@ func TestEnvSettings(t *testing.T) { assert.Equal(t, tt.kcontext, settings.KubeContext, "kube-context") assert.Equal(t, tt.maxhistory, settings.MaxHistory, "maxHistory") assert.Equal(t, tt.kubeAsUser, settings.KubeAsUser, "kubeAsUser") - assert.True(t, reflect.DeepEqual(tt.kubeAsGroups, settings.KubeAsGroups), "kubeAsGroups") + assert.Equal(t, tt.kubeAsGroups, settings.KubeAsGroups, "kubeAsGroups") assert.Equal(t, tt.kubeCaFile, settings.KubeCaFile, "kubeCaFile") assert.Equal(t, tt.burstLimit, settings.BurstLimit, "burstLimit") assert.Equal(t, tt.kubeInsecure, settings.KubeInsecureSkipTLSVerify, "kubeInsecure") diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index 3a33da7f1..f6a116f18 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -21,7 +21,6 @@ import ( "errors" "os" "path/filepath" - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -214,7 +213,7 @@ func TestReadFile(t *testing.T) { // Test the function got, err := readFile(actualFilePath, tt.providers) - assert.NoError(t, err, "readFile() expected no error for stdin") + require.NoError(t, err, "readFile() expected no error for stdin") assert.Equal(t, testData, got) return } @@ -360,8 +359,8 @@ func TestMergeValuesCLI(t *testing.T) { assert.Error(t, err) return } - assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(got, tt.expected), "MergeValues() = %v, want %v", got, tt.expected) + require.NoError(t, err) + assert.Equal(t, tt.expected, got) }) } } From f76136d39f8c1fd6110a23317786c4c53f6f9cb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:32:44 +0000 Subject: [PATCH 131/132] chore(deps): bump golangci/golangci-lint-action from 9.2.1 to 9.3.0 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.1 to 9.3.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/82606bf257cbaff209d206a39f5134f0cfbfd2ee...ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e13d8950e..6355db230 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,6 +22,6 @@ jobs: go-version: '${{ env.GOLANG_VERSION }}' check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #pin@9.2.1 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #pin@9.3.0 with: version: ${{ env.GOLANGCI_LINT_VERSION }} From 5a9b8a788ff260c031f2d2f3e1b6903bd31b3496 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:33:40 +0000 Subject: [PATCH 132/132] chore(deps): bump github.com/fluxcd/cli-utils from 1.2.1 to 1.2.2 Bumps [github.com/fluxcd/cli-utils](https://github.com/fluxcd/cli-utils) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/fluxcd/cli-utils/releases) - [Commits](https://github.com/fluxcd/cli-utils/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: github.com/fluxcd/cli-utils dependency-version: 1.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ea6d6c972..d85d0387a 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.2.1 + github.com/fluxcd/cli-utils v1.2.2 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -115,7 +115,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/gomega v1.39.1 // indirect + github.com/onsi/gomega v1.42.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect @@ -158,7 +158,7 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect @@ -173,7 +173,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/component-base v0.36.2 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect - k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index f50611b85..a92d35dfe 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY= -github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw= +github.com/fluxcd/cli-utils v1.2.2 h1:adDOmwE+LSwTzmYUaoEFPblruOuaQEKAg1ZNTmPJObE= +github.com/fluxcd/cli-utils v1.2.2/go.mod h1:FsghNGY+3Sr70c0FOB7I5So0kzoYVdvQ8GTid3XXVWM= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -233,8 +233,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -400,8 +400,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -505,8 +505,8 @@ k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hk k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk= k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0= -k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= -k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=