From 3120d85753574ada8eeed3540ce99d7358a596e1 Mon Sep 17 00:00:00 2001 From: Jan Braje <12571346+janluak@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:19:08 +0200 Subject: [PATCH] add `helm bump [VERSION_TYPE] [CHART]` for setting chart versions Signed-off-by: Jan Braje <12571346+janluak@users.noreply.github.com> --- pkg/action/bump.go | 353 +++++++++++++++++ pkg/action/bump_test.go | 437 ++++++++++++++++++++++ pkg/cmd/bump.go | 84 +++++ pkg/cmd/bump_test.go | 63 ++++ pkg/cmd/root.go | 1 + pkg/cmd/testdata/output/bump-default.txt | 1 + pkg/cmd/testdata/output/bump-explicit.txt | 1 + pkg/cmd/testdata/output/bump-minor.txt | 1 + 8 files changed, 941 insertions(+) create mode 100644 pkg/action/bump.go create mode 100644 pkg/action/bump_test.go create mode 100644 pkg/cmd/bump.go create mode 100644 pkg/cmd/bump_test.go create mode 100644 pkg/cmd/testdata/output/bump-default.txt create mode 100644 pkg/cmd/testdata/output/bump-explicit.txt create mode 100644 pkg/cmd/testdata/output/bump-minor.txt diff --git a/pkg/action/bump.go b/pkg/action/bump.go new file mode 100644 index 000000000..f01034a49 --- /dev/null +++ b/pkg/action/bump.go @@ -0,0 +1,353 @@ +/* +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 action + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "strconv" + "strings" + + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/chart/v2/util" +) + +// Bump is the action for bumping a chart version. +// +// It provides the implementation of 'helm bump'. +type Bump struct { + ChartPathOptions + cfg *Configuration + + bump string + chart *chart.Chart +} + +const defaultBumpType = "patch" + +// NewBump creates a new Bump object with the given configuration. +func NewBump(cfg *Configuration) *Bump { + return &Bump{ + cfg: cfg, + } +} + +// Run executes 'helm bump' against the given chart. +func (b *Bump) Run(bumpType string, chartpath string) (string, error) { + if b.chart == nil { + chrt, err := loader.Load(chartpath) + if err != nil { + return "", err + } + b.chart = chrt + } + cv, err := yaml.Marshal(b.chart.Metadata.Version) + if err != nil { + return "", err + } + // Determine new version based on bump type or explicit version + b.bump = bumpType + if b.bump == "" { + // Default to "patch" if no version specified + b.bump = defaultBumpType + } + + currentVersion := strings.TrimSpace(string(cv)) + if !isValidVersion(currentVersion) { + return "", fmt.Errorf("invalid origin version: %s", currentVersion) + } + + var newVersion string + switch b.bump { + case "major": + newVersion, err = bumpMajor(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump major version: %w", err) + } + case "minor": + newVersion, err = bumpMinor(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump minor version: %w", err) + } + case "patch": + newVersion, err = bumpPatch(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump patch version: %w", err) + } + case "stable": + newVersion, err = bumpStable(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump stable version: %w", err) + } + case "alpha": + newVersion, err = bumpAlpha(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump alpha version: %w", err) + } + case "beta": + newVersion, err = bumpBeta(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump beta version: %w", err) + } + case "rc": + newVersion, err = bumpRC(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump rc version: %w", err) + } + case "post": + newVersion, err = bumpPost(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump post version: %w", err) + } + case "dev": + newVersion, err = bumpDev(currentVersion) + if err != nil { + return "", fmt.Errorf("failed to bump dev version: %w", err) + } + default: + if !isValidVersion(b.bump) { + return "", fmt.Errorf("invalid bump type or version: %s", b.bump) + } + newVersion = b.bump + } + + // Update the chart metadata with the new version + b.chart.Metadata.Version = newVersion + + // Save the updated chart to disk (this will update Chart.yaml) + err = util.SaveChartfile(filepath.Join(chartpath, "Chart.yaml"), b.chart.Metadata) + if err != nil { + return "", fmt.Errorf("failed to save updated chart: %w", err) + } + + return newVersion, nil +} + +// bumpMajor increases the major version number (e.g., 1.2.3 -> 2.0.0) +func bumpMajor(version string) (string, error) { + parts := strings.Split(version, ".") + if len(parts) < 3 { + return "", errors.New("invalid version format for major bump") + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return "", errors.New("invalid major version number") + } + + newMajor := major + 1 + return fmt.Sprintf("%d.0.0", newMajor), nil +} + +// bumpMinor increases the minor version number (e.g., 1.2.3 -> 1.3.0) +func bumpMinor(version string) (string, error) { + parts := strings.Split(version, ".") + if len(parts) < 3 { + return "", errors.New("invalid version format for minor bump") + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return "", errors.New("invalid major version number") + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return "", errors.New("invalid minor version number") + } + + newMinor := minor + 1 + return fmt.Sprintf("%d.%d.0", major, newMinor), nil +} + +// bumpPatch increases the patch version number (e.g., 1.2.3 -> 1.2.4) +func bumpPatch(version string) (string, error) { + parts := strings.Split(version, ".") + if len(parts) < 3 { + return "", errors.New("invalid version format for patch bump") + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return "", errors.New("invalid major version number") + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return "", errors.New("invalid minor version number") + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return "", errors.New("invalid patch version number") + } + + newPatch := patch + 1 + return fmt.Sprintf("%d.%d.%d", major, minor, newPatch), nil +} + +// bumpStable removes any pre-release suffix (e.g., 1.2.3-alpha -> 1.2.3) +func bumpStable(version string) (string, error) { + // Remove any pre-release suffixes (like -alpha, -beta, etc.) + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-.+)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for stable bump") + } + return matches[1], nil +} + +// bumpAlpha increases the pre-release version (e.g., 1.2.3-alpha -> 1.2.3-alpha.1) +func bumpAlpha(version string) (string, error) { + // Check if version has an alpha suffix - simple regex that works with all cases + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-alpha(?:\.(\d+))?)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for alpha bump") + } + + baseVersion := matches[1] + var preRelease string + if len(matches) >= 4 && matches[3] != "" { + // Increment the pre-release number + num, err := strconv.Atoi(matches[3]) + if err != nil { + return "", errors.New("invalid alpha pre-release number") + } + preRelease = fmt.Sprintf(".%d", num+1) + } else { + // Start with .1 if no pre-release number exists + preRelease = ".1" + } + + return baseVersion + "-alpha" + preRelease, nil +} + +// bumpBeta increases the pre-release version (e.g., 1.2.3-beta -> 1.2.3-beta.1) +func bumpBeta(version string) (string, error) { + // Check if version has a beta suffix - simple regex that works with all cases + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-beta(?:\.(\d+))?)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for beta bump") + } + + baseVersion := matches[1] + var preRelease string + if len(matches) >= 4 && matches[3] != "" { + // Increment the pre-release number + num, err := strconv.Atoi(matches[3]) + if err != nil { + return "", errors.New("invalid beta pre-release number") + } + preRelease = fmt.Sprintf(".%d", num+1) + } else { + // Start with .1 if no pre-release number exists + preRelease = ".1" + } + + return baseVersion + "-beta" + preRelease, nil +} + +// bumpRC increases the pre-release version (e.g., 1.2.3-rc -> 1.2.3-rc.1) +func bumpRC(version string) (string, error) { + // Check if version has a rc suffix - simple regex that works with all cases + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-rc(?:\.(\d+))?)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for rc bump") + } + + baseVersion := matches[1] + var preRelease string + if len(matches) >= 4 && matches[3] != "" { + // Increment the pre-release number + num, err := strconv.Atoi(matches[3]) + if err != nil { + return "", errors.New("invalid rc pre-release number") + } + preRelease = fmt.Sprintf(".%d", num+1) + } else { + // Start with .1 if no pre-release number exists + preRelease = ".1" + } + + return baseVersion + "-rc" + preRelease, nil +} + +// bumpPost increases the post-release version (e.g., 1.2.3-post -> 1.2.3-post.1) +func bumpPost(version string) (string, error) { + // Check if version has a post-suffix + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-post(\.(\d+))?)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for post bump") + } + + baseVersion := matches[1] + var preRelease string + if len(matches) >= 4 && matches[4] != "" { + // Increment the pre-release number + num, err := strconv.Atoi(matches[4]) + if err != nil { + return "", errors.New("invalid post pre-release number") + } + preRelease = fmt.Sprintf(".%d", num+1) + } else { + // Start with .1 if no pre-release number exists + preRelease = ".1" + } + + return baseVersion + "-post" + preRelease, nil +} + +// bumpDev increases the dev version (e.g., 1.2.3-dev -> 1.2.3-dev.1) +func bumpDev(version string) (string, error) { + // Check if version has a dev suffix + re := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-dev(\.(\d+))?)?$`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + return "", errors.New("invalid version format for dev bump") + } + + baseVersion := matches[1] + var preRelease string + if len(matches) >= 4 && matches[4] != "" { + // Increment the pre-release number + num, err := strconv.Atoi(matches[4]) + if err != nil { + return "", errors.New("invalid dev pre-release number") + } + preRelease = fmt.Sprintf(".%d", num+1) + } else { + // Start with .1 if no pre-release number exists + preRelease = ".1" + } + + return baseVersion + "-dev" + preRelease, nil +} + +// isValidVersion checks if a string is a valid semantic version format +func isValidVersion(version string) bool { + re := regexp.MustCompile(`^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+(\.\d+)?)?$`) + return re.MatchString(version) +} diff --git a/pkg/action/bump_test.go b/pkg/action/bump_test.go new file mode 100644 index 000000000..cfe1f38ba --- /dev/null +++ b/pkg/action/bump_test.go @@ -0,0 +1,437 @@ +/* +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 action + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + chart "helm.sh/helm/v4/pkg/chart/v2" +) + +func TestNewBump(t *testing.T) { + cfg := actionConfigFixture(t) + client := NewBump(cfg) + + assert.NotNil(t, client) + require.Equal(t, cfg, client.cfg) +} + +func TestBump_Run_Major(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Major bump with valid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("major", "") + require.NoError(t, err) + require.Equal(t, "2.0.0", result) + }) + + t.Run("Major bump with invalid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("major", "") + require.Error(t, err) + require.Empty(t, result) + }) + + t.Run("Major bump with invalid major version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.a" + result, err := client.Run("major", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Minor(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Minor bump with valid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("minor", "") + require.NoError(t, err) + require.Equal(t, "1.3.0", result) + }) + + t.Run("Minor bump with invalid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("minor", "") + require.Error(t, err) + require.Empty(t, result) + }) + + t.Run("Minor bump with invalid minor version", func(t *testing.T) { + client.chart.Metadata.Version = "1.a.3" + result, err := client.Run("minor", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Patch(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Patch bump with valid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("patch", "") + require.NoError(t, err) + require.Equal(t, "1.2.4", result) + }) + + t.Run("Patch bump with invalid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("patch", "") + require.Error(t, err) + require.Empty(t, result) + }) + + t.Run("Patch bump with invalid patch version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.a" + result, err := client.Run("patch", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Stable(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Stable bump with pre-release version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-alpha" + result, err := client.Run("stable", "") + require.NoError(t, err) + require.Equal(t, "1.2.3", result) + }) + + t.Run("Stable bump with pre-release version with number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-alpha.1" + result, err := client.Run("stable", "") + require.NoError(t, err) + require.Equal(t, "1.2.3", result) + }) + + t.Run("Stable bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("stable", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Alpha(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Alpha bump with version without pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("alpha", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-alpha.1", result) + }) + + t.Run("Alpha bump with version with pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-alpha" + result, err := client.Run("alpha", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-alpha.1", result) + }) + + t.Run("Alpha bump with version with pre-release number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-alpha.1" + result, err := client.Run("alpha", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-alpha.2", result) + }) + + t.Run("Alpha bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("alpha", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Beta(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Beta bump with version without pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("beta", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-beta.1", result) + }) + + t.Run("Beta bump with version with pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-beta" + result, err := client.Run("beta", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-beta.1", result) + }) + + t.Run("Beta bump with version with pre-release number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-beta.1" + result, err := client.Run("beta", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-beta.2", result) + }) + + t.Run("Beta bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("beta", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_RC(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("RC bump with version without pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("rc", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-rc.1", result) + }) + + t.Run("RC bump with version with pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-rc" + result, err := client.Run("rc", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-rc.1", result) + }) + + t.Run("RC bump with version with pre-release number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-rc.1" + result, err := client.Run("rc", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-rc.2", result) + }) + + t.Run("RC bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("rc", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Post(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Post bump with version without pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("post", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-post.1", result) + }) + + t.Run("Post bump with version with post-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-post" + result, err := client.Run("post", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-post.1", result) + }) + + t.Run("Post bump with version with pre-release number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-post.1" + result, err := client.Run("post", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-post.2", result) + }) + + t.Run("Post bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("post", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_Dev(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Dev bump with version without pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("dev", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-dev.1", result) + }) + + t.Run("Dev bump with version with pre-release", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-dev" + result, err := client.Run("dev", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-dev.1", result) + }) + + t.Run("Dev bump with version with pre-release number", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3-dev.1" + result, err := client.Run("dev", "") + require.NoError(t, err) + require.Equal(t, "1.2.3-dev.2", result) + }) + + t.Run("Dev bump with invalid version format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2" + result, err := client.Run("dev", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_Run_ExplicitVersion(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + t.Run("Explicit version with valid format", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("2.0.1", "") + require.NoError(t, err) + require.Equal(t, "2.0.1", result) + }) + + t.Run("Explicit version with post version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("1.5.3-post.1", "") + require.NoError(t, err) + require.Equal(t, "1.5.3-post.1", result) + }) +} + +func TestBump_Run_InvalidVersion(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + // We cannot test actual bumping since chart loading is not implemented, + // but we can test error handling for invalid bump types + t.Run("Invalid bump type", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("invalid", "") + require.Error(t, err) + require.Empty(t, result) + }) +} + +func TestBump_IsValidVersion(t *testing.T) { + t.Parallel() + + testCases := []struct { + version string + expected bool + }{ + {"1.2.3", true}, + {"1.2.3-alpha", true}, + {"1.2.3-beta.1", true}, + {"1.2.3-rc.2", true}, + {"1.2.3-post.1", true}, + {"1.2.3-dev.1", true}, + {"1.2", false}, + {"invalid", false}, + {"1.2.3.4", false}, + {"1.a.3", false}, + } + + for _, tc := range testCases { + t.Run("Version "+tc.version, func(t *testing.T) { + result := isValidVersion(tc.version) + require.Equal(t, tc.expected, result) + }) + } +} + +func TestBump_Run_WithDefault(t *testing.T) { + t.Parallel() + + cfg := actionConfigFixture(t) + client := NewBump(cfg) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + } + + // Test that it properly handles the default case + t.Run("Patch bump with valid version", func(t *testing.T) { + client.chart.Metadata.Version = "1.2.3" + result, err := client.Run("patch", "") + require.NoError(t, err) + require.Equal(t, "1.2.4", result) + require.Equal(t, "patch", client.bump) + }) +} diff --git a/pkg/cmd/bump.go b/pkg/cmd/bump.go new file mode 100644 index 000000000..6467709ef --- /dev/null +++ b/pkg/cmd/bump.go @@ -0,0 +1,84 @@ +/* +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 cmd + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/cmd/require" +) + +const bumpDesc = ` +Bump the version of a chart. +` + +type bumpOptions struct { + bumpType string + chart string +} + +// newBumpCmd creates a new bump command with the given configuration and output writer. +func newBumpCmd(actionConfig *action.Configuration, out io.Writer) *cobra.Command { + o := &bumpOptions{} + + cmd := &cobra.Command{ + Use: "bump [VERSION_TYPE] [CHART]", + Short: "bump the version of a chart", + Long: bumpDesc, + Args: require.MaximumNArgs(2), + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(_ *cobra.Command, args []string) error { + if len(args) == 2 { + o.bumpType = args[0] + o.chart = args[1] + } else { + o.bumpType = "" + o.chart = args[0] + } + + return o.run(actionConfig, out) + }, + } + f := cmd.Flags() + f.StringVar(&o.chart, "chart", "", "path to the chart directory") + + return cmd +} + +func (o *bumpOptions) run(actionConfig *action.Configuration, out io.Writer) error { + // Resolve the chart path to an absolute path + absPath, err := filepath.Abs(o.chart) + if err != nil { + return fmt.Errorf("failed to resolve chart path: %w", err) + } + o.chart = absPath + + bump := action.NewBump(actionConfig) + + newVersion, err := bump.Run(o.bumpType, o.chart) + if err != nil { + return err + } + + fmt.Fprintf(out, "changed chart version to %q\n", newVersion) + return nil +} diff --git a/pkg/cmd/bump_test.go b/pkg/cmd/bump_test.go new file mode 100644 index 000000000..edceaffde --- /dev/null +++ b/pkg/cmd/bump_test.go @@ -0,0 +1,63 @@ +/* +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 cmd + +import ( + "io" + "os" + "path/filepath" + "testing" +) + +func TestBump(t *testing.T) { + tempChartDir := t.TempDir() + + testChartFile := filepath.Join("testdata", "testcharts", "empty", "Chart.yaml") + + destFile := filepath.Join(tempChartDir, "Chart.yaml") + + srcFile, err := os.Open(testChartFile) + if err != nil { + t.Fatalf("error on opening test file: %v", err) + } + defer srcFile.Close() + + destFileHandle, err := os.Create(destFile) + if err != nil { + t.Fatalf("error on creating test file: %v", err) + } + defer destFileHandle.Close() + + _, err = io.Copy(destFileHandle, srcFile) + if err != nil { + t.Fatalf("error on copying test file: %v", err) + } + + tests := []cmdTestCase{{ + name: "default", + cmd: "bump " + tempChartDir, + golden: "output/bump-default.txt", + }, { + name: "with bump type", + cmd: "bump minor " + tempChartDir, + golden: "output/bump-minor.txt", + }, { + name: "with explicit version", + cmd: "bump 2.0.0 " + tempChartDir, + golden: "output/bump-explicit.txt", + }} + runTestCmd(t, tests) +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 410a9162c..9cf813f00 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -286,6 +286,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg newRollbackCmd(actionConfig, out), newStatusCmd(actionConfig, out), newTemplateCmd(actionConfig, out), + newBumpCmd(actionConfig, out), newUninstallCmd(actionConfig, out), newUpgradeCmd(actionConfig, out), diff --git a/pkg/cmd/testdata/output/bump-default.txt b/pkg/cmd/testdata/output/bump-default.txt new file mode 100644 index 000000000..f642a03e6 --- /dev/null +++ b/pkg/cmd/testdata/output/bump-default.txt @@ -0,0 +1 @@ +changed chart version to "0.1.1" diff --git a/pkg/cmd/testdata/output/bump-explicit.txt b/pkg/cmd/testdata/output/bump-explicit.txt new file mode 100644 index 000000000..f77c44c47 --- /dev/null +++ b/pkg/cmd/testdata/output/bump-explicit.txt @@ -0,0 +1 @@ +changed chart version to "2.0.0" diff --git a/pkg/cmd/testdata/output/bump-minor.txt b/pkg/cmd/testdata/output/bump-minor.txt new file mode 100644 index 000000000..952cc4fc7 --- /dev/null +++ b/pkg/cmd/testdata/output/bump-minor.txt @@ -0,0 +1 @@ +changed chart version to "0.2.0"