Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions internal/core/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"errors"
"fmt"
"regexp"
"strings"

"golang.org/x/mod/semver"
Expand All @@ -19,18 +20,34 @@ var ErrNoVersionFound = errors.New("no version found")
// MinimumStackVersion is the minimum Stack version the operator supports deploying.
const MinimumStackVersion = "v2.2.0"

// ValidateMinimumVersion checks that a Versions resource name meets the minimum requirement.
// Non-semver names (dev tags, SHA refs) are allowed through.
func ValidateMinimumVersion(version string) error {
if strings.TrimPrefix(version, "v") == "0.0.0-e2e" {
return nil
}
// partialSemverRe matches `v<major>` or `v<major>.<minor>` — semver-shaped
// names that are missing the patch (and optionally the minor) component.
// These are routinely used as Versions resource names (`v3`, `v3.2`) and
// would otherwise sail past [semver.IsValid] and silently bypass the
// minimum-version check.
var partialSemverRe = regexp.MustCompile(`^v(\d+)(?:\.(\d+))?$`)

normalizedVersion := version
if !strings.HasPrefix(normalizedVersion, "v") {
normalizedVersion = "v" + normalizedVersion
// normalizePartialSemver expands `v3` → `v3.0.0` and `v3.2` → `v3.2.0` so
// partial-semver Versions resource names are gated by the same min-version
// check as their canonical form. Non-matching inputs (canonical semver,
// dev tags, SHA refs, non-`v`-prefixed strings) are returned unchanged.
func normalizePartialSemver(v string) string {
m := partialSemverRe.FindStringSubmatch(v)
if m == nil {
return v
}
minor := m[2]
if minor == "" {
minor = "0"
}
if semver.IsValid(normalizedVersion) && semver.Compare(normalizedVersion, MinimumStackVersion) < 0 {
return fmt.Sprintf("v%s.%s.0", m[1], minor)
}

// ValidateMinimumVersion checks that a Versions resource name meets the minimum requirement.
// Non-semver names (dev tags, SHA refs, non-`v`-prefixed strings) are allowed through.
func ValidateMinimumVersion(version string) error {
normalized := normalizePartialSemver(version)
if semver.IsValid(normalized) && semver.Compare(normalized, MinimumStackVersion) < 0 {
return fmt.Errorf("version %s is not supported, minimum required: %s - please upgrade your stack", version, MinimumStackVersion)
}
return nil
Expand Down
19 changes: 15 additions & 4 deletions internal/core/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,32 @@ func TestValidateMinimumVersion(t *testing.T) {
version string
wantError bool
}{
// Canonical semver below minimum is rejected.
{name: "v2.0.0 rejected", version: "v2.0.0", wantError: true},
{name: "v2.1.0 rejected", version: "v2.1.0", wantError: true},
{name: "2.1.0 rejected without v prefix", version: "2.1.0", wantError: true},
{name: "v2.1.9 rejected", version: "v2.1.9", wantError: true},
{name: "v2.0.0-rc.5 rejected", version: "v2.0.0-rc.5", wantError: true},
{name: "v2.2.0-alpha pre-release rejected", version: "v2.2.0-alpha", wantError: true},

// Canonical semver at or above minimum is accepted.
{name: "v2.2.0 accepted", version: "v2.2.0", wantError: false},
{name: "2.2.0 accepted without v prefix", version: "2.2.0", wantError: false},
{name: "v0.0.0-e2e accepted as test tag", version: "v0.0.0-e2e", wantError: false},
{name: "0.0.0-e2e accepted as test tag without v prefix", version: "0.0.0-e2e", wantError: false},
{name: "v2.3.0 accepted", version: "v2.3.0", wantError: false},
{name: "v3.0.0 accepted", version: "v3.0.0", wantError: false},

// Partial semver (`v3`, `v3.2`) is expanded to its canonical form
// (`v3.0.0`, `v3.2.0`) and gated by the same minimum-version check.
{name: "v3 accepted (expands to v3.0.0)", version: "v3", wantError: false},
{name: "v3.2 accepted (expands to v3.2.0)", version: "v3.2", wantError: false},
{name: "v2.2 accepted as equal to minimum (expands to v2.2.0)", version: "v2.2", wantError: false},
{name: "v2 rejected (expands to v2.0.0)", version: "v2", wantError: true},
{name: "v2.1 rejected (expands to v2.1.0)", version: "v2.1", wantError: true},

// Non-semver names (dev tags, SHA refs, non-`v`-prefixed strings)
// pass through unchecked.
{name: "non-semver accepted", version: "main", wantError: false},
{name: "sha ref accepted", version: "abc123def", wantError: false},
{name: "latest accepted", version: "latest", wantError: false},
{name: "non-v-prefixed accepted as passthrough", version: "2.2.0", wantError: false},
}

for _, tt := range tests {
Expand Down
Loading