Repository: helm/helm
Pull request #31019 (2025-07-01 21:43:07)

Return early when linting if the `templates/` directory does not exist
**What this PR does / why we need it**:

The comments in pkg/lint/rules/template.go say that it will return early if the `templates/` directory does not exist, but `validateTemplatesDir` silently discards the error from `os.Stat` and still passes if `templates/` does not exist. So check first if file path exists before running any lint checks.

**Special notes for your reviewer**:

Discovered in https://github.com/helm/helm/pull/31015, which adds linter support for the `crds/` directory.

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility



Patch:
diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go
index 463bd5341..b36153ec6 100644
--- a/pkg/lint/rules/template.go
+++ b/pkg/lint/rules/template.go
@@ -54,10 +54,14 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
 	fpath := "templates/"
 	templatesPath := filepath.Join(linter.ChartDir, fpath)
 
-	templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath))
-
 	// Templates directory is optional for now
-	if !templatesDirExist {
+	templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath))
+	if !templatesDirExists {
+		return
+	}
+
+	validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath))
+	if !validTemplatesDir {
 		return
 	}
 
@@ -194,11 +198,21 @@ func validateTopIndentLevel(content string) error {
 }
 
 // Validation functions
+func templatesDirExists(templatesPath string) error {
+	_, err := os.Stat(templatesPath)
+	if errors.Is(err, os.ErrNotExist) {
+		return errors.New("directory does not exist")
+	}
+	return nil
+}
+
 func validateTemplatesDir(templatesPath string) error {
-	if fi, err := os.Stat(templatesPath); err == nil {
-		if !fi.IsDir() {
-			return errors.New("not a directory")
-		}
+	fi, err := os.Stat(templatesPath)
+	if err != nil {
+		return err
+	}
+	if !fi.IsDir() {
+		return errors.New("not a directory")
 	}
 	return nil
 }


Test patch:
diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go
index a01580b0a..613149a4d 100644
--- a/pkg/action/lint_test.go
+++ b/pkg/action/lint_test.go
@@ -154,12 +154,12 @@ func TestLint_ChartWithWarnings(t *testing.T) {
 		}
 	})
 
-	t.Run("should pass with no errors when strict", func(t *testing.T) {
+	t.Run("should fail with one error when strict", func(t *testing.T) {
 		testCharts := []string{chartWithNoTemplatesDir}
 		testLint := NewLint()
 		testLint.Strict = true
-		if result := testLint.Run(testCharts, values); len(result.Errors) != 0 {
-			t.Error("expected no errors, but got", len(result.Errors))
+		if result := testLint.Run(testCharts, values); len(result.Errors) != 1 {
+			t.Error("expected one error, but got", len(result.Errors))
 		}
 	})
 }
diff --git a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt
index 6e2efcecd..7b445a69a 100644
--- a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt
+++ b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt
@@ -1,6 +1,6 @@
 ==> Linting testdata/testcharts/chart-with-bad-subcharts
 [INFO] Chart.yaml: icon is recommended
-[ERROR] templates/: error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required
+[WARNING] templates/: directory does not exist
 [ERROR] : unable to load chart
 	error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required
 
@@ -9,11 +9,12 @@
 [ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2"
 [ERROR] Chart.yaml: version is required
 [INFO] Chart.yaml: icon is recommended
-[ERROR] templates/: validation: chart.metadata.name is required
+[WARNING] templates/: directory does not exist
 [ERROR] : unable to load chart
 	validation: chart.metadata.name is required
 
 ==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart
 [INFO] Chart.yaml: icon is recommended
+[WARNING] templates/: directory does not exist
 
 Error: 3 chart(s) linted, 2 chart(s) failed
diff --git a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt
index af533797b..5a1c388bb 100644
--- a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt
+++ b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt
@@ -1,6 +1,6 @@
 ==> Linting testdata/testcharts/chart-with-bad-subcharts
 [INFO] Chart.yaml: icon is recommended
-[ERROR] templates/: error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required
+[WARNING] templates/: directory does not exist
 [ERROR] : unable to load chart
 	error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required
 
diff --git a/pkg/cmd/testdata/output/lint-quiet-with-error.txt b/pkg/cmd/testdata/output/lint-quiet-with-error.txt
index e3d29a5a3..0731a07d1 100644
--- a/pkg/cmd/testdata/output/lint-quiet-with-error.txt
+++ b/pkg/cmd/testdata/output/lint-quiet-with-error.txt
@@ -1,7 +1,7 @@
 ==> Linting testdata/testcharts/chart-bad-requirements
 [ERROR] Chart.yaml: unable to parse YAML
 	error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator
-[ERROR] templates/: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator
+[WARNING] templates/: directory does not exist
 [ERROR] : unable to load chart
 	cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator
 
diff --git a/pkg/cmd/testdata/output/lint-quiet-with-warning.txt b/pkg/cmd/testdata/output/lint-quiet-with-warning.txt
index e69de29bb..ebf6c1989 100644
--- a/pkg/cmd/testdata/output/lint-quiet-with-warning.txt
+++ b/pkg/cmd/testdata/output/lint-quiet-with-warning.txt
@@ -0,0 +1,4 @@
+==> Linting testdata/testcharts/chart-with-only-crds
+[WARNING] templates/: directory does not exist
+
+1 chart(s) linted, 0 chart(s) failed
diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go
index 888d3dfe6..63cf017e4 100644
--- a/pkg/lint/lint_test.go
+++ b/pkg/lint/lint_test.go
@@ -43,14 +43,19 @@ func TestBadChart(t *testing.T) {
 		t.Errorf("Number of errors %v", len(m))
 		t.Errorf("All didn't fail with expected errors, got %#v", m)
 	}
-	// There should be one INFO, and 2 ERROR messages, check for them
-	var i, e, e2, e3, e4, e5, e6 bool
+	// There should be one INFO, one WARNING, and 2 ERROR messages, check for them
+	var i, w, e, e2, e3, e4, e5, e6 bool
 	for _, msg := range m {
 		if msg.Severity == support.InfoSev {
 			if strings.Contains(msg.Err.Error(), "icon is recommended") {
 				i = true
 			}
 		}
+		if msg.Severity == support.WarningSev {
+			if strings.Contains(msg.Err.Error(), "does not exist") {
+				w = true
+			}
+		}
 		if msg.Severity == support.ErrorSev {
 			if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") {
 				e = true
@@ -76,7 +81,7 @@ func TestBadChart(t *testing.T) {
 			}
 		}
 	}
-	if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 {
+	if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w {
 		t.Errorf("Didn't find all the expected errors, got %#v", m)
 	}
 }
@@ -93,7 +98,7 @@ func TestInvalidYaml(t *testing.T) {
 
 func TestInvalidChartYaml(t *testing.T) {
 	m := RunAll(invalidChartFileDir, values, namespace).Messages
-	if len(m) != 1 {
+	if len(m) != 2 {
 		t.Fatalf("All didn't fail with expected errors, got %#v", m)
 	}
 	if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") {