Skip to content

Commit 7b64d72

Browse files
committed
ct lint: add --ignore-ci-changes flag to skip version bump
When only files in a chart's 'ci' directory are modified, the version increment check can be skipped during linting. This is useful because ci/ contains test values files that don't warrant a chart version bump. The flag is available for 'ct lint' and 'ct lint-and-install'. Charts with CI-only changes are still processed by 'ct install'. Signed-off-by: Alexander Bayandin <alexander@bayandin.dev>
1 parent 59825b8 commit 7b64d72

7 files changed

Lines changed: 162 additions & 25 deletions

File tree

ct/cmd/lint.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func addLintFlags(flags *flag.FlagSet) {
7171
Enable linting of 'Chart.yaml' and values files`))
7272
flags.Bool("skip-helm-dependencies", false, heredoc.Doc(`
7373
Skip running 'helm dependency build' before linting`))
74+
flags.Bool("ignore-ci-changes", false, "If set, changes only in the chart's 'ci' directory will not trigger a chart version bump requirement")
7475
flags.StringSlice("additional-commands", []string{}, heredoc.Doc(`
7576
Additional commands to run per chart (default: [])
7677
Commands will be executed in the same order as provided in the list and will

pkg/chart/chart.go

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,9 @@ func (t *Testing) computePreviousRevisionPath(fileOrDirPath string) string {
307307
return filepath.Join(t.previousRevisionWorktree, fileOrDirPath)
308308
}
309309

310-
func (t *Testing) processCharts(action func(chart *Chart) TestResult) ([]TestResult, error) {
310+
func (t *Testing) processCharts(action func(chart *Chart, ciOnly bool) TestResult) ([]TestResult, error) {
311311
var results []TestResult // nolint: prealloc
312-
chartDirs, err := t.FindChartDirsToBeProcessed()
312+
chartDirs, ciOnlyCharts, err := t.findChartDirsToBeProcessed()
313313
if err != nil {
314314
return nil, fmt.Errorf("failed identifying charts to process: %w", err)
315315
} else if len(chartDirs) == 0 {
@@ -408,7 +408,8 @@ func (t *Testing) processCharts(action func(chart *Chart) TestResult) ([]TestRes
408408
}
409409
}
410410

411-
result := action(chart)
411+
_, ciOnly := ciOnlyCharts[chart.Path()]
412+
result := action(chart, ciOnly)
412413
if result.Error != nil {
413414
testResults.OverallSuccess = false
414415
}
@@ -423,17 +424,19 @@ func (t *Testing) processCharts(action func(chart *Chart) TestResult) ([]TestRes
423424

424425
// LintCharts lints charts (changed, all, specific) depending on the configuration.
425426
func (t *Testing) LintCharts() ([]TestResult, error) {
426-
return t.processCharts(t.LintChart)
427+
return t.processCharts(t.lintChart)
427428
}
428429

429430
// InstallCharts install charts (changed, all, specific) depending on the configuration.
430431
func (t *Testing) InstallCharts() ([]TestResult, error) {
431-
return t.processCharts(t.InstallChart)
432+
return t.processCharts(func(chart *Chart, _ bool) TestResult {
433+
return t.InstallChart(chart)
434+
})
432435
}
433436

434437
// LintAndInstallCharts first lints and then installs charts (changed, all, specific) depending on the configuration.
435438
func (t *Testing) LintAndInstallCharts() ([]TestResult, error) {
436-
return t.processCharts(t.LintAndInstallChart)
439+
return t.processCharts(t.lintAndInstallChart)
437440
}
438441

439442
// PrintResults writes test results to stdout.
@@ -465,12 +468,18 @@ func (t *Testing) PrintResults(results []TestResult) {
465468

466469
// LintChart lints the specified chart.
467470
func (t *Testing) LintChart(chart *Chart) TestResult {
471+
return t.lintChart(chart, false)
472+
}
473+
474+
func (t *Testing) lintChart(chart *Chart, ciOnly bool) TestResult {
468475
fmt.Printf("Linting chart %q\n", chart)
469476

470477
result := TestResult{Chart: chart}
471478

472479
if t.config.CheckVersionIncrement {
473-
if err := t.CheckVersionIncrement(chart); err != nil {
480+
if ciOnly {
481+
fmt.Printf("Skipping version increment check for %q because only 'ci' directory files changed\n", chart)
482+
} else if err := t.CheckVersionIncrement(chart); err != nil {
474483
result.Error = err
475484
return result
476485
}
@@ -713,7 +722,11 @@ func (t *Testing) generateInstallConfig(chart *Chart) (namespace, release, relea
713722

714723
// LintAndInstallChart first lints and then installs the specified chart.
715724
func (t *Testing) LintAndInstallChart(chart *Chart) TestResult {
716-
result := t.LintChart(chart)
725+
return t.lintAndInstallChart(chart, false)
726+
}
727+
728+
func (t *Testing) lintAndInstallChart(chart *Chart, ciOnly bool) TestResult {
729+
result := t.lintChart(chart, ciOnly)
717730
if result.Error != nil {
718731
return result
719732
}
@@ -723,13 +736,19 @@ func (t *Testing) LintAndInstallChart(chart *Chart) TestResult {
723736
// FindChartDirsToBeProcessed identifies charts to be processed depending on the configuration
724737
// (changed charts, all charts, or specific charts).
725738
func (t *Testing) FindChartDirsToBeProcessed() ([]string, error) {
739+
dirs, _, err := t.findChartDirsToBeProcessed()
740+
return dirs, err
741+
}
742+
743+
func (t *Testing) findChartDirsToBeProcessed() ([]string, map[string]struct{}, error) {
726744
cfg := t.config
727745
if cfg.ProcessAllCharts {
728-
return t.ReadAllChartDirectories()
746+
dirs, err := t.ReadAllChartDirectories()
747+
return dirs, nil, err
729748
} else if len(cfg.Charts) > 0 {
730-
return t.config.Charts, nil
749+
return cfg.Charts, nil, nil
731750
}
732-
return t.ComputeChangedChartDirectories()
751+
return t.computeChangedChartDirectories()
733752
}
734753

735754
func (t *Testing) computeMergeBase() (string, error) {
@@ -749,16 +768,23 @@ func (t *Testing) computeMergeBase() (string, error) {
749768
// ComputeChangedChartDirectories takes the merge base of HEAD and the configured remote and target branch and computes a
750769
// slice of changed charts from that in the configured chart directories excluding those configured to be excluded.
751770
func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
771+
dirs, _, err := t.computeChangedChartDirectories()
772+
return dirs, err
773+
}
774+
775+
// computeChangedChartDirectories is the internal implementation that also returns
776+
// a set of chart directories where only files in the 'ci' directory changed.
777+
func (t *Testing) computeChangedChartDirectories() ([]string, map[string]struct{}, error) {
752778
cfg := t.config
753779

754780
mergeBase, err := t.computeMergeBase()
755781
if err != nil {
756-
return nil, err
782+
return nil, nil, err
757783
}
758784

759785
allChangedChartFiles, err := t.git.ListChangedFilesInDirs(mergeBase, cfg.ChartDirs...)
760786
if err != nil {
761-
return nil, fmt.Errorf("failed creating diff: %w", err)
787+
return nil, nil, fmt.Errorf("failed creating diff: %w", err)
762788
}
763789

764790
changedChartFiles := map[string][]string{}
@@ -784,28 +810,49 @@ func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
784810
}
785811
}
786812

787-
changedChartDirs := []string{}
813+
// Apply helmignore filtering
788814
if t.config.UseHelmignore {
789-
for chartDir, changedChartFiles := range changedChartFiles {
815+
for chartDir, files := range changedChartFiles {
790816
rules, err := t.loadRules(chartDir)
791817
if err != nil {
792-
return nil, err
818+
return nil, nil, err
793819
}
794-
filteredChartFiles, err := ignore.FilterFiles(changedChartFiles, rules)
820+
filteredFiles, err := ignore.FilterFiles(files, rules)
795821
if err != nil {
796-
return nil, err
822+
return nil, nil, err
797823
}
798-
if len(filteredChartFiles) > 0 {
799-
changedChartDirs = append(changedChartDirs, chartDir)
824+
if len(filteredFiles) > 0 {
825+
changedChartFiles[chartDir] = filteredFiles
826+
} else {
827+
delete(changedChartFiles, chartDir)
800828
}
801829
}
802-
} else {
803-
for chartDir := range changedChartFiles {
804-
changedChartDirs = append(changedChartDirs, chartDir)
830+
}
831+
832+
// Detect charts where only 'ci' directory files changed (after helmignore filtering)
833+
var ciOnlyCharts map[string]struct{}
834+
if cfg.IgnoreCIChanges {
835+
ciOnlyCharts = make(map[string]struct{})
836+
for chartDir, files := range changedChartFiles {
837+
allCI := true
838+
for _, f := range files {
839+
if !strings.HasPrefix(filepath.ToSlash(f), "ci/") {
840+
allCI = false
841+
break
842+
}
843+
}
844+
if allCI {
845+
ciOnlyCharts[chartDir] = struct{}{}
846+
}
805847
}
806848
}
807849

808-
return changedChartDirs, nil
850+
changedChartDirs := make([]string, 0, len(changedChartFiles))
851+
for chartDir := range changedChartFiles {
852+
changedChartDirs = append(changedChartDirs, chartDir)
853+
}
854+
855+
return changedChartDirs, ciOnlyCharts, nil
809856
}
810857

811858
// ReadAllChartDirectories returns a slice of all charts in the configured chart directories except those

pkg/chart/chart_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ func (g fakeGit) BranchExists(_ string) bool {
7575
return true
7676
}
7777

78+
type fakeGitCIOnlyChanges struct{ fakeGit }
79+
80+
func (g fakeGitCIOnlyChanges) Show(_ string, _ string, _ string) (string, error) {
81+
return "name: test\nversion: 1.0.0\n", nil
82+
}
83+
84+
func (g fakeGitCIOnlyChanges) ListChangedFilesInDirs(_ string, _ ...string) ([]string, error) {
85+
return []string{
86+
"test_charts/foo/ci/test-values.yaml",
87+
"test_charts/bar/Chart.yaml",
88+
}, nil
89+
}
90+
7891
type fakeAccountValidator struct{}
7992

8093
func (v fakeAccountValidator) Validate(_ string, account string) error {
@@ -227,6 +240,78 @@ func TestComputeChangedChartDirectoriesWithMultiLevelChartWithHelmIgnore(t *test
227240
assert.ElementsMatch(t, expected, actual)
228241
}
229242

243+
func TestComputeChangedChartDirectoriesIgnoreCIChanges(t *testing.T) {
244+
cfg := config.Configuration{
245+
ExcludedCharts: []string{"excluded"},
246+
ChartDirs: []string{"test_charts", "."},
247+
IgnoreCIChanges: true,
248+
}
249+
ct := newTestingMock(cfg)
250+
ct.git = fakeGitCIOnlyChanges{}
251+
actual, err := ct.ComputeChangedChartDirectories()
252+
assert.Nil(t, err)
253+
// Both charts are still returned — CI-only charts are not excluded from change detection
254+
assert.ElementsMatch(t, []string{"test_charts/foo", "test_charts/bar"}, actual)
255+
}
256+
257+
func TestLintChartsIgnoreCIChanges(t *testing.T) {
258+
cfg := config.Configuration{
259+
ExcludedCharts: []string{"excluded"},
260+
ChartDirs: []string{"test_charts"},
261+
IgnoreCIChanges: true,
262+
CheckVersionIncrement: true,
263+
SkipHelmDependencies: true,
264+
}
265+
ct := newTestingMock(cfg)
266+
ct.git = fakeGitCIOnlyChanges{}
267+
268+
results, err := ct.LintCharts()
269+
// bar fails version check, so overall error is returned
270+
assert.NotNil(t, err)
271+
assert.Len(t, results, 2)
272+
273+
for _, result := range results {
274+
switch result.Chart.Path() {
275+
case "test_charts/foo":
276+
// CI-only: version check skipped, lint passes
277+
assert.Nil(t, result.Error)
278+
case "test_charts/bar":
279+
// Non-CI-only: version check runs and fails (fakeGit returns old version 1.0.0)
280+
assert.NotNil(t, result.Error)
281+
default:
282+
t.Fatalf("unexpected chart: %s", result.Chart.Path())
283+
}
284+
}
285+
}
286+
287+
func TestLintChartsVersionCheckWithoutIgnoreCIChanges(t *testing.T) {
288+
cfg := config.Configuration{
289+
ExcludedCharts: []string{"excluded"},
290+
ChartDirs: []string{"test_charts"},
291+
IgnoreCIChanges: false,
292+
CheckVersionIncrement: true,
293+
SkipHelmDependencies: true,
294+
}
295+
ct := newTestingMock(cfg)
296+
ct.git = fakeGitCIOnlyChanges{}
297+
298+
results, err := ct.LintCharts()
299+
assert.NotNil(t, err)
300+
assert.Len(t, results, 2)
301+
302+
for _, result := range results {
303+
switch result.Chart.Path() {
304+
case "test_charts/foo":
305+
// Without --ignore-ci-changes, CI-only chart still gets version-checked and fails
306+
assert.NotNil(t, result.Error)
307+
case "test_charts/bar":
308+
assert.NotNil(t, result.Error)
309+
default:
310+
t.Fatalf("unexpected chart: %s", result.Chart.Path())
311+
}
312+
}
313+
}
314+
230315
func TestReadAllChartDirectories(t *testing.T) {
231316
actual, err := ct.ReadAllChartDirectories()
232317
expected := []string{

pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type Configuration struct {
7777
PrintLogs bool `mapstructure:"print-logs"`
7878
GithubGroups bool `mapstructure:"github-groups"`
7979
UseHelmignore bool `mapstructure:"use-helmignore"`
80+
IgnoreCIChanges bool `mapstructure:"ignore-ci-changes"`
8081
}
8182

8283
func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*Configuration, error) {

pkg/config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func loadAndAssertConfigFromFile(t *testing.T, configFile string) {
6262
require.Equal(t, 120*time.Second, cfg.KubectlTimeout)
6363
require.Equal(t, true, cfg.SkipCleanUp)
6464
require.Equal(t, true, cfg.UseHelmignore)
65+
require.Equal(t, true, cfg.IgnoreCIChanges)
6566
}
6667

6768
func Test_findConfigFile(t *testing.T) {

pkg/config/test_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@
3333
"exclude-deprecated": true,
3434
"kubectl-timeout": "120s",
3535
"skip-clean-up": true,
36-
"use-helmignore": true
36+
"use-helmignore": true,
37+
"ignore-ci-changes": true
3738
}

pkg/config/test_config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ exclude-deprecated: true
2929
kubectl-timeout: 120s
3030
skip-clean-up: true
3131
use-helmignore: true
32+
ignore-ci-changes: true

0 commit comments

Comments
 (0)