From 1f90b812768894f07669fef6d28c7908d7f53e16 Mon Sep 17 00:00:00 2001
From: Cyril Jouve <jv.cyril@gmail.com>
Date: Tue, 29 Aug 2023 23:31:16 +0200
Subject: [PATCH] use .helmignore when identifying changed charts

Signed-off-by: Cyril Jouve <jv.cyril@gmail.com>
---
 ct/cmd/root.go              |  1 +
 doc/ct_install.md           |  1 +
 doc/ct_lint-and-install.md  |  1 +
 doc/ct_lint.md              |  2 +-
 doc/ct_list-changed.md      |  2 +-
 go.mod                      |  6 ++-
 go.sum                      | 15 ++++++-
 pkg/chart/chart.go          | 32 +++++++++++---
 pkg/chart/chart_test.go     | 47 +++++++++++++++++++++
 pkg/config/config.go        |  1 +
 pkg/config/config_test.go   |  1 +
 pkg/config/test_config.json |  3 +-
 pkg/config/test_config.yaml |  1 +
 pkg/ignore/ignore.go        | 83 +++++++++++++++++++++++++++++++++++++
 pkg/ignore/ignore_test.go   | 19 +++++++++
 15 files changed, 205 insertions(+), 10 deletions(-)
 create mode 100644 pkg/ignore/ignore.go
 create mode 100644 pkg/ignore/ignore_test.go

diff --git a/ct/cmd/root.go b/ct/cmd/root.go
index 16b40485..3afb2621 100644
--- a/ct/cmd/root.go
+++ b/ct/cmd/root.go
@@ -80,6 +80,7 @@ func addCommonFlags(flags *pflag.FlagSet) {
 	flags.Bool("github-groups", false, heredoc.Doc(`
 		Change the delimiters for github to create collapsible groups
 		for command output`))
+	flags.Bool("use-helmignore", false, "Use .helmignore when identifying changed charts")
 }
 
 func addCommonLintAndInstallFlags(flags *pflag.FlagSet) {
diff --git a/doc/ct_install.md b/doc/ct_install.md
index 355a14c3..fe1e72d3 100644
--- a/doc/ct_install.md
+++ b/doc/ct_install.md
@@ -77,6 +77,7 @@ ct install [flags]
       --target-branch string                 The name of the target branch used to identify changed charts (default "main")
       --upgrade                              Whether to test an in-place upgrade of each chart from its previous revision if the
                                              current version should not introduce a breaking change according to the SemVer spec
+      --use-helmignore                       Use .helmignore when identifying changed charts
 ```
 
 ### SEE ALSO
diff --git a/doc/ct_lint-and-install.md b/doc/ct_lint-and-install.md
index 765448fc..215a70fe 100644
--- a/doc/ct_lint-and-install.md
+++ b/doc/ct_lint-and-install.md
@@ -72,6 +72,7 @@ ct lint-and-install [flags]
       --target-branch string                 The name of the target branch used to identify changed charts (default "main")
       --upgrade                              Whether to test an in-place upgrade of each chart from its previous revision if the
                                              current version should not introduce a breaking change according to the SemVer spec
+      --use-helmignore                       Use .helmignore when identifying changed charts
       --validate-chart-schema                Enable schema validation of 'Chart.yaml' using Yamale (default true)
       --validate-maintainers                 Enable validation of maintainer account names in chart.yml.
                                              Works for GitHub, GitLab, and Bitbucket (default true)
diff --git a/doc/ct_lint.md b/doc/ct_lint.md
index 5fb4ef62..2b6e57b9 100644
--- a/doc/ct_lint.md
+++ b/doc/ct_lint.md
@@ -70,6 +70,7 @@ ct lint [flags]
       --remote string                        The name of the Git remote used to identify changed charts (default "origin")
       --since string                         The Git reference used to identify changed charts (default "HEAD")
       --target-branch string                 The name of the target branch used to identify changed charts (default "main")
+      --use-helmignore                       Use .helmignore when identifying changed charts
       --validate-chart-schema                Enable schema validation of 'Chart.yaml' using Yamale (default true)
       --validate-maintainers                 Enable validation of maintainer account names in chart.yml.
                                              Works for GitHub, GitLab, and Bitbucket (default true)
@@ -79,4 +80,3 @@ ct lint [flags]
 ### SEE ALSO
 
 * [ct](ct.md)	 - The Helm chart testing tool
-
diff --git a/doc/ct_list-changed.md b/doc/ct_list-changed.md
index 5080a62f..d757a711 100644
--- a/doc/ct_list-changed.md
+++ b/doc/ct_list-changed.md
@@ -28,9 +28,9 @@ ct list-changed [flags]
       --remote string             The name of the Git remote used to identify changed charts (default "origin")
       --since string              The Git reference used to identify changed charts (default "HEAD")
       --target-branch string      The name of the target branch used to identify changed charts (default "main")
+      --use-helmignore            Use .helmignore when identifying changed charts
 ```
 
 ### SEE ALSO
 
 * [ct](ct.md)	 - The Helm chart testing tool
-
diff --git a/go.mod b/go.mod
index a88fd0a6..a516d392 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,8 @@
 module github.com/helm/chart-testing/v3
 
-go 1.20
+go 1.21
+
+toolchain go1.21.6
 
 require (
 	github.com/MakeNowJust/heredoc v1.0.0
@@ -14,6 +16,7 @@ require (
 	github.com/spf13/viper v1.18.2
 	github.com/stretchr/testify v1.8.4
 	gopkg.in/yaml.v2 v2.4.0
+	helm.sh/helm/v3 v3.14.0
 )
 
 require (
@@ -27,6 +30,7 @@ require (
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
diff --git a/go.sum b/go.sum
index 19f35acb..6cb2958e 100644
--- a/go.sum
+++ b/go.sum
@@ -9,10 +9,13 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
+github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -20,6 +23,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
+github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
@@ -30,13 +34,17 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -45,10 +53,13 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
 github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
@@ -99,3 +110,5 @@ 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=
+helm.sh/helm/v3 v3.14.0 h1:TaZIH6uOchn7L27ptwnnuHJiFrT/BsD4dFdp/HLT2nM=
+helm.sh/helm/v3 v3.14.0/go.mod h1:2itvvDv2WSZXTllknfQo6j7u3VVgMAvm8POCDgYH424=
diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go
index 0f1e1f92..35a2d9d4 100644
--- a/pkg/chart/chart.go
+++ b/pkg/chart/chart.go
@@ -22,9 +22,11 @@ import (
 	"strings"
 
 	"github.com/Masterminds/semver"
+	helmignore "helm.sh/helm/v3/pkg/ignore"
 
 	"github.com/helm/chart-testing/v3/pkg/config"
 	"github.com/helm/chart-testing/v3/pkg/exec"
+	"github.com/helm/chart-testing/v3/pkg/ignore"
 	"github.com/helm/chart-testing/v3/pkg/tool"
 	"github.com/helm/chart-testing/v3/pkg/util"
 )
@@ -242,6 +244,7 @@ type Testing struct {
 	directoryLister          DirectoryLister
 	utils                    Utils
 	previousRevisionWorktree string
+	loadRules                func(string) (*helmignore.Rules, error)
 }
 
 // TestResults holds results and overall status
@@ -272,6 +275,7 @@ func NewTesting(config config.Configuration, extraSetArgs string) (Testing, erro
 		accountValidator: tool.AccountValidator{},
 		directoryLister:  util.DirectoryLister{},
 		utils:            util.Utils{},
+		loadRules:        ignore.LoadRules,
 	}
 
 	versionString, err := testing.helm.Version()
@@ -746,7 +750,7 @@ func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
 		return nil, fmt.Errorf("failed creating diff: %w", err)
 	}
 
-	var changedChartDirs []string
+	changedChartFiles := map[string][]string{}
 	for _, file := range allChangedChartFiles {
 		pathElements := strings.SplitN(filepath.ToSlash(file), "/", 3)
 		if len(pathElements) < 2 || util.StringSliceContains(cfg.ExcludedCharts, pathElements[1]) {
@@ -763,15 +767,33 @@ func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
 					continue
 				}
 			}
-			// Only add it if not already in the list
-			if !util.StringSliceContains(changedChartDirs, chartDir) {
-				changedChartDirs = append(changedChartDirs, chartDir)
-			}
+			changedChartFiles[chartDir] = append(changedChartFiles[chartDir], strings.TrimPrefix(file, chartDir+"/"))
 		} else {
 			fmt.Fprintf(os.Stderr, "Directory %q is not a valid chart directory. Skipping...\n", dir)
 		}
 	}
 
+	changedChartDirs := []string{}
+	if t.config.UseHelmignore {
+		for chartDir, changedChartFiles := range changedChartFiles {
+			rules, err := t.loadRules(chartDir)
+			if err != nil {
+				return nil, err
+			}
+			filteredChartFiles, err := ignore.FilterFiles(changedChartFiles, rules)
+			if err != nil {
+				return nil, err
+			}
+			if len(filteredChartFiles) > 0 {
+				changedChartDirs = append(changedChartDirs, chartDir)
+			}
+		}
+	} else {
+		for chartDir := range changedChartFiles {
+			changedChartDirs = append(changedChartDirs, chartDir)
+		}
+	}
+
 	return changedChartDirs, nil
 }
 
diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go
index a45ff829..678bb801 100644
--- a/pkg/chart/chart_test.go
+++ b/pkg/chart/chart_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/helm/chart-testing/v3/pkg/util"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	helmignore "helm.sh/helm/v3/pkg/ignore"
 )
 
 type fakeGit struct{}
@@ -152,6 +153,26 @@ func newTestingMock(cfg config.Configuration) Testing {
 		accountValidator: fakeAccountValidator{},
 		linter:           fakeMockLinter,
 		helm:             new(fakeHelm),
+		loadRules: func(dir string) (*helmignore.Rules, error) {
+			rules := helmignore.Empty()
+			if dir == "test_charts/foo" {
+				var err error
+				rules, err = helmignore.Parse(strings.NewReader("Chart.yaml\n"))
+				if err != nil {
+					return nil, err
+				}
+				rules.AddDefaults()
+			}
+			if dir == "test_chart_at_multi_level/foo/baz" {
+				var err error
+				rules, err = helmignore.Parse(strings.NewReader("Chart.yaml\n"))
+				if err != nil {
+					return nil, err
+				}
+				rules.AddDefaults()
+			}
+			return rules, nil
+		},
 	}
 }
 
@@ -165,6 +186,19 @@ func TestComputeChangedChartDirectories(t *testing.T) {
 	assert.Nil(t, err)
 }
 
+func TestComputeChangedChartDirectoriesWithHelmignore(t *testing.T) {
+	cfg := config.Configuration{
+		ExcludedCharts: []string{"excluded"},
+		ChartDirs:      []string{"test_charts", "."},
+		UseHelmignore:  true,
+	}
+	ct := newTestingMock(cfg)
+	actual, err := ct.ComputeChangedChartDirectories()
+	expected := []string{"test_charts/bar", "test_chart_at_root"}
+	assert.Nil(t, err)
+	assert.ElementsMatch(t, expected, actual)
+}
+
 func TestComputeChangedChartDirectoriesWithMultiLevelChart(t *testing.T) {
 	cfg := config.Configuration{
 		ExcludedCharts: []string{"excluded"},
@@ -180,6 +214,19 @@ func TestComputeChangedChartDirectoriesWithMultiLevelChart(t *testing.T) {
 	assert.Nil(t, err)
 }
 
+func TestComputeChangedChartDirectoriesWithMultiLevelChartWithHelmIgnore(t *testing.T) {
+	cfg := config.Configuration{
+		ExcludedCharts: []string{"excluded"},
+		ChartDirs:      []string{"test_chart_at_multi_level/foo"},
+		UseHelmignore:  true,
+	}
+	ct := newTestingMock(cfg)
+	actual, err := ct.ComputeChangedChartDirectories()
+	expected := []string{"test_chart_at_multi_level/foo/bar"}
+	assert.Nil(t, err)
+	assert.ElementsMatch(t, expected, actual)
+}
+
 func TestReadAllChartDirectories(t *testing.T) {
 	actual, err := ct.ReadAllChartDirectories()
 	expected := []string{
diff --git a/pkg/config/config.go b/pkg/config/config.go
index c6b82874..989c467b 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -73,6 +73,7 @@ type Configuration struct {
 	KubectlTimeout          time.Duration `mapstructure:"kubectl-timeout"`
 	PrintLogs               bool          `mapstructure:"print-logs"`
 	GithubGroups            bool          `mapstructure:"github-groups"`
+	UseHelmignore           bool          `mapstructure:"use-helmignore"`
 }
 
 func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*Configuration, error) {
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 80b33f0e..f07d912e 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -61,6 +61,7 @@ func loadAndAssertConfigFromFile(t *testing.T, configFile string) {
 	require.Equal(t, true, cfg.ExcludeDeprecated)
 	require.Equal(t, 120*time.Second, cfg.KubectlTimeout)
 	require.Equal(t, true, cfg.SkipCleanUp)
+	require.Equal(t, true, cfg.UseHelmignore)
 }
 
 func Test_findConfigFile(t *testing.T) {
diff --git a/pkg/config/test_config.json b/pkg/config/test_config.json
index 4dc445a8..73cd4574 100644
--- a/pkg/config/test_config.json
+++ b/pkg/config/test_config.json
@@ -32,5 +32,6 @@
     "release-label": "release",
     "exclude-deprecated": true,
     "kubectl-timeout": "120s",
-    "skip-clean-up": true
+    "skip-clean-up": true,
+    "use-helmignore": true
 }
diff --git a/pkg/config/test_config.yaml b/pkg/config/test_config.yaml
index 6fe391de..0d1c7c02 100644
--- a/pkg/config/test_config.yaml
+++ b/pkg/config/test_config.yaml
@@ -28,3 +28,4 @@ release-label: release
 exclude-deprecated: true
 kubectl-timeout: 120s
 skip-clean-up: true
+use-helmignore: true
diff --git a/pkg/ignore/ignore.go b/pkg/ignore/ignore.go
new file mode 100644
index 00000000..99d19c2c
--- /dev/null
+++ b/pkg/ignore/ignore.go
@@ -0,0 +1,83 @@
+/*
+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 ignore
+
+import (
+	"io/fs"
+	"os"
+	"path/filepath"
+	"testing/fstest"
+
+	helmignore "helm.sh/helm/v3/pkg/ignore"
+)
+
+func LoadRules(dir string) (*helmignore.Rules, error) {
+	rules, err := helmignore.ParseFile(filepath.Join(dir, helmignore.HelmIgnore))
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+	if rules == nil {
+		rules = helmignore.Empty()
+	}
+	rules.AddDefaults()
+	return rules, nil
+}
+
+func FilterFiles(files []string, rules *helmignore.Rules) ([]string, error) {
+	fsys := fstest.MapFS{}
+	for _, file := range files {
+		fsys[file] = &fstest.MapFile{}
+	}
+
+	filteredFiles := []string{}
+
+	err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		fi, err := d.Info()
+		if err != nil {
+			return err
+		}
+
+		// Normalize to / since it will also work on Windows
+		path = filepath.ToSlash(path)
+
+		if fi.IsDir() {
+			// Directory-based ignore rules should involve skipping the entire
+			// contents of that directory.
+			if rules.Ignore(path, fi) {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+
+		// If a .helmignore file matches, skip this file.
+		if rules.Ignore(path, fi) {
+			return nil
+		}
+
+		filteredFiles = append(filteredFiles, path)
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return filteredFiles, nil
+}
diff --git a/pkg/ignore/ignore_test.go b/pkg/ignore/ignore_test.go
new file mode 100644
index 00000000..b1e94349
--- /dev/null
+++ b/pkg/ignore/ignore_test.go
@@ -0,0 +1,19 @@
+package ignore
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	helmignore "helm.sh/helm/v3/pkg/ignore"
+)
+
+func TestFilter(t *testing.T) {
+	rules, err := helmignore.Parse(strings.NewReader("/bar/\nREADME.md\n"))
+	assert.Nil(t, err)
+	files := []string{"Chart.yaml", "bar/xxx", "template/svc.yaml", "baz/bar/biz.txt", "README.md"}
+	actual, err := FilterFiles(files, rules)
+	assert.Nil(t, err)
+	expected := []string{"Chart.yaml", "baz/bar/biz.txt", "template/svc.yaml"}
+	assert.ElementsMatch(t, expected, actual)
+}