From fa902708f4c6aa96fab102dcfbccfbe85fc05228 Mon Sep 17 00:00:00 2001 From: Ben Meier Date: Tue, 9 Jul 2024 16:30:59 +0100 Subject: [PATCH] feat: added check-version subcommand Signed-off-by: Ben Meier --- internal/command/check_version.go | 36 ++++++++++++++++ internal/command/check_version_test.go | 58 ++++++++++++++++++++++++++ internal/command/root_test.go | 11 ++--- internal/version/version.go | 49 +++++++++++++++++++++- internal/version/version_test.go | 37 ++++++++++++++++ 5 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 internal/command/check_version.go create mode 100644 internal/command/check_version_test.go create mode 100644 internal/version/version_test.go diff --git a/internal/command/check_version.go b/internal/command/check_version.go new file mode 100644 index 0000000..652bde6 --- /dev/null +++ b/internal/command/check_version.go @@ -0,0 +1,36 @@ +package command + +import ( + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/version" +) + +var checkVersionCmd = &cobra.Command{ + Use: "check-version [constraint]", + Short: "Assert that the version of score-compose matches the required constraint", + Long: `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality +or a particular default provisioner provided by score-compose init. This command provides a common way to check that +the version of score-compose matches a required version. +`, + Example: ` + # check that the version is exactly 1.2.3 + score-compose check-version =v1.2.3 + + # check that the version is 1.3.0 or greater + score-compose check-version >v1.2 + + # check that the version is equal or greater to 1.2.3 + score-compose check-version >=1.2.3`, + Args: cobra.ExactArgs(1), + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + return version.AssertVersion(args[0], version.Version) + }, +} + +func init() { + rootCmd.AddCommand(checkVersionCmd) +} diff --git a/internal/command/check_version_test.go b/internal/command/check_version_test.go new file mode 100644 index 0000000..f6351fb --- /dev/null +++ b/internal/command/check_version_test.go @@ -0,0 +1,58 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckVersionHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality +or a particular default provisioner provided by score-compose init. This command provides a common way to check that +the version of score-compose matches a required version. + +Usage: + score-compose check-version [constraint] [flags] + +Examples: + + # check that the version is exactly 1.2.3 + score-compose check-version =v1.2.3 + + # check that the version is 1.3.0 or greater + score-compose check-version >v1.2 + + # check that the version is equal or greater to 1.2.3 + score-compose check-version >=1.2.3 + +Flags: + -h, --help help for check-version + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "check-version"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestCheckVersionPass(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">=0.0.0"}) + assert.NoError(t, err) + assert.Equal(t, stdout, "") + assert.Equal(t, "", stderr) +} + +func TestCheckVersionFail(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">99"}) + assert.EqualError(t, err, "current version 0.0.0 does not match requested constraint >99") + assert.Equal(t, stdout, "") + assert.Equal(t, "", stderr) +} diff --git a/internal/command/root_test.go b/internal/command/root_test.go index f502aeb..415d636 100644 --- a/internal/command/root_test.go +++ b/internal/command/root_test.go @@ -33,11 +33,12 @@ Usage: score-compose [command] Available Commands: - completion Generate the autocompletion script for the specified shell - generate Convert one or more Score files into a Docker compose manifest - help Help about any command - init Initialise a new score-compose project with local state directory and score file - resources Subcommands related to provisioned resources + check-version Assert that the version of score-compose matches the required constraint + completion Generate the autocompletion script for the specified shell + generate Convert one or more Score files into a Docker compose manifest + help Help about any command + init Initialise a new score-compose project with local state directory and score file + resources Subcommands related to provisioned resources Flags: -h, --help help for score-compose diff --git a/internal/version/version.go b/internal/version/version.go index 98f00ef..4dc52a0 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -9,11 +9,15 @@ package version import ( "fmt" + "regexp" "runtime/debug" + "strconv" ) var ( - Version string = "0.0.0" + Version string = "0.0.0" + semverPattern = regexp.MustCompile(`^(?:v?)(\d+)(?:\.(\d+))?(?:\.(\d+))?$`) + constraintAndSemver = regexp.MustCompile("^(>|>=|=)?" + semverPattern.String()[1:]) ) // BuildVersionString constructs a version string by looking at the build metadata injected at build time. @@ -39,3 +43,46 @@ func BuildVersionString() string { } return fmt.Sprintf("%s (build: %s, sha: %s%s)", versionNumber, buildTime, gitSha, isDirtySuffix) } + +func semverToI(x string) (int, error) { + cpm := semverPattern.FindStringSubmatch(x) + if cpm == nil { + return 0, fmt.Errorf("invalid version: %s", x) + } + major, _ := strconv.Atoi(cpm[1]) + minor, patch := 999, 999 + if len(cpm) > 2 { + minor, _ = strconv.Atoi(cpm[2]) + if len(cpm) > 3 { + patch, _ = strconv.Atoi(cpm[3]) + } + } + return (major*1_000+minor)*1_000 + patch, nil +} + +func AssertVersion(constraint string, current string) error { + if currentI, err := semverToI(current); err != nil { + return fmt.Errorf("current version is missing or invalid '%s'", current) + } else if m := constraintAndSemver.FindStringSubmatch(constraint); m == nil { + return fmt.Errorf("invalid constraint '%s'", constraint) + } else { + op := m[1] + compareI, err := semverToI(m[0][len(op):]) + if err != nil { + return fmt.Errorf("failed to parse constraint: %w", err) + } + match := false + switch op { + case ">": + match = currentI > compareI + case ">=": + match = currentI >= compareI + case "=": + match = currentI == compareI + } + if !match { + return fmt.Errorf("current version %s does not match requested constraint %s", current, constraint) + } + return nil + } +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..cbb406f --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,37 @@ +package version + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAssertVersion_good(t *testing.T) { + for _, tup := range [][2]string{ + {"=1.2.3", "v1.2.3"}, + {">=1.2.3", "v1.2.3"}, + {">=1.2.3", "v1.2.4"}, + {">1.2.3", "v1.2.4"}, + {">=1.1", "1.1.0"}, + {">=1.1", "1.2.0"}, + {">=1", "1.0.0"}, + {">1", "2.0.0"}, + } { + t.Run(fmt.Sprintf("%v", tup), func(t *testing.T) { + assert.NoError(t, AssertVersion(tup[0], tup[1])) + }) + } +} + +func TestAssertVersion_bad(t *testing.T) { + for _, tup := range [][3]string{ + {"=1.2.3", "v1.2.0", "current version v1.2.0 does not match requested constraint =1.2.3"}, + {">2", "v1.2.0", "current version v1.2.0 does not match requested constraint >2"}, + {">1.2", "v1.2.0", "current version v1.2.0 does not match requested constraint >1.2"}, + } { + t.Run(fmt.Sprintf("%v", tup), func(t *testing.T) { + assert.EqualError(t, AssertVersion(tup[0], tup[1]), tup[2]) + }) + } +}