├── .gitignore ├── pkg ├── semver │ ├── map.go │ ├── parse.go │ ├── trim.go │ ├── find.go │ ├── parse_test.go │ ├── find_test.go │ └── trim_test.go ├── versions │ ├── prefix.go │ ├── suffix.go │ ├── prefix_test.go │ ├── suffix_test.go │ ├── api.go │ └── api_test.go ├── modes │ ├── mode.go │ ├── api.go │ ├── major.go │ ├── minor.go │ ├── patch.go │ ├── auto.go │ ├── gitcommit.go │ ├── gitbranch.go │ ├── detect.go │ ├── minor_test.go │ ├── patch_test.go │ ├── api_test.go │ ├── major_test.go │ ├── auto_test.go │ ├── gitcommit_test.go │ ├── gitbranch_test.go │ └── detect_test.go ├── cli │ ├── errors.go │ ├── commands │ │ ├── v1 │ │ │ ├── get.go │ │ │ ├── push.go │ │ │ ├── release.go │ │ │ ├── update.go │ │ │ ├── predict.go │ │ │ ├── root.go │ │ │ ├── get-version.go │ │ │ ├── update-version.go │ │ │ ├── init.go │ │ │ ├── push-version.go │ │ │ ├── version.go │ │ │ ├── predict-version.go │ │ │ └── release-version.go │ │ └── root.go │ ├── flags.go │ ├── exec │ │ └── entrypoint.go │ ├── configs.go │ └── defaults.go ├── core │ ├── get.go │ ├── update.go │ ├── push.go │ ├── release.go │ ├── init.go │ └── predict.go ├── ext │ └── viperx │ │ └── viper.go └── git │ ├── api.go │ ├── cli.go │ └── cli_test.go ├── cmd └── sbot │ └── main.go ├── main.go ├── internal ├── ldflags │ └── x.go ├── mocks │ ├── mode.go │ ├── commander.go │ └── git.go ├── constants.go ├── util │ ├── strings.go │ └── strings_test.go └── fakes │ └── git.go ├── .semverbot.toml ├── CONTRIBUTING.md ├── .github └── workflows │ ├── development.yml │ └── main.yaml ├── Makefile ├── main.nu ├── go.mod ├── go.sum ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Jetbrains 2 | .idea 3 | 4 | # Project 5 | bin 6 | out 7 | -------------------------------------------------------------------------------- /pkg/semver/map.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | // Map a type to keep things shorter and more readable. 4 | type Map map[string][]string 5 | -------------------------------------------------------------------------------- /cmd/sbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/restechnica/semverbot/pkg/cli/exec" 4 | 5 | // main bootstraps the `sbot` CLI. 6 | func main() { 7 | _ = exec.Run() 8 | } 9 | -------------------------------------------------------------------------------- /pkg/versions/prefix.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import "fmt" 4 | 5 | func AddPrefix(version string, prefix string) string { 6 | return fmt.Sprintf("%s%s", prefix, version) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/versions/suffix.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import "fmt" 4 | 5 | func AddSuffix(version string, suffix string) string { 6 | return fmt.Sprintf("%s%s", version, suffix) 7 | } 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/cli/exec" 5 | ) 6 | 7 | // main bootstraps the `sbot` CLI app. 8 | func main() { 9 | _ = exec.Run() 10 | } 11 | -------------------------------------------------------------------------------- /internal/ldflags/x.go: -------------------------------------------------------------------------------- 1 | package ldflags 2 | 3 | // -X key=value ldflags definitions, injected at build time. 4 | var ( 5 | // Version The release version of the sbot binary. 6 | Version string = "dev" 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/modes/mode.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | // Mode interface which increments a specific semver level. 4 | type Mode interface { 5 | Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) 6 | String() string 7 | } 8 | -------------------------------------------------------------------------------- /pkg/cli/errors.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | func NewCommandError(err error) CommandError { 4 | return CommandError{Err: err} 5 | } 6 | 7 | type CommandError struct { 8 | Err error 9 | } 10 | 11 | func (e CommandError) Error() string { 12 | return e.Err.Error() 13 | } 14 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/get.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // NewGetCommand creates a new get command. 8 | // Returns a new init spf13/cobra command. 9 | func NewGetCommand() *cobra.Command { 10 | var command = &cobra.Command{ 11 | Use: "get", 12 | } 13 | 14 | command.AddCommand(NewGetVersionCommand()) 15 | 16 | return command 17 | } 18 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/push.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // NewPushCommand creates a new push command. 8 | // Returns the new spf13/cobra command. 9 | func NewPushCommand() *cobra.Command { 10 | var command = &cobra.Command{ 11 | Use: "push", 12 | } 13 | 14 | command.AddCommand(NewPushVersionCommand()) 15 | 16 | return command 17 | } 18 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/release.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | // NewReleaseCommand creates a new release command. 6 | // Returns the new spf13/cobra command. 7 | func NewReleaseCommand() *cobra.Command { 8 | var command = &cobra.Command{ 9 | Use: "release", 10 | } 11 | 12 | command.AddCommand(NewReleaseVersionCommand()) 13 | 14 | return command 15 | } 16 | -------------------------------------------------------------------------------- /.semverbot.toml: -------------------------------------------------------------------------------- 1 | mode = "auto" 2 | 3 | [git] 4 | 5 | [git.config] 6 | email = "semverbot@github.com" 7 | name = "semverbot" 8 | 9 | [git.tags] 10 | prefix = "v" 11 | suffix = "" 12 | 13 | [semver] 14 | patch = ["fix", "bug"] 15 | minor = ["feature"] 16 | major = ["release"] 17 | 18 | [modes] 19 | 20 | [modes.git-branch] 21 | delimiters = "/" 22 | 23 | [modes.git-commit] 24 | delimiters = "[]/" 25 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/update.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // NewUpdateCommand creates a new update command. 8 | // Returns the new spf13/cobra command. 9 | func NewUpdateCommand() *cobra.Command { 10 | var command = &cobra.Command{ 11 | Use: "update", 12 | } 13 | 14 | command.AddCommand(NewUpdateVersionCommand()) 15 | 16 | return command 17 | } 18 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/predict.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // NewPredictCommand creates a new predict command. 8 | // Returns the new spf13/cobra command. 9 | func NewPredictCommand() *cobra.Command { 10 | var command = &cobra.Command{ 11 | Use: "predict", 12 | } 13 | 14 | command.AddCommand(NewPredictVersionCommand()) 15 | 16 | return command 17 | } 18 | -------------------------------------------------------------------------------- /pkg/cli/flags.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | var ( 4 | // ConfigFlag a flag which configures the config file location. 5 | ConfigFlag string 6 | 7 | // DebugFlag a flag which sets the log level verbosity to Debug if true 8 | DebugFlag bool 9 | 10 | // ModeFlag a flag which indicates the semver mode to increment the current version with. 11 | ModeFlag string 12 | 13 | // VerboseFlag a flag which increases log level verbosity to Info if true 14 | VerboseFlag bool 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/core/get.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/versions" 5 | ) 6 | 7 | type GetVersionOptions struct { 8 | GitTagPrefix string 9 | GitTagSuffix string 10 | DefaultVersion string 11 | } 12 | 13 | // GetVersion gets the current version. 14 | // Returns the current version. 15 | func GetVersion(options *GetVersionOptions) string { 16 | var versionAPI = versions.NewAPI(options.GitTagPrefix, options.GitTagSuffix) 17 | return versionAPI.GetVersionOrDefault(options.DefaultVersion) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/core/update.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/versions" 5 | ) 6 | 7 | type UpdateVersionOptions struct { 8 | GitTagsPrefix string 9 | GitTagsSuffix string 10 | } 11 | 12 | // UpdateVersion updates to the latest version. 13 | // Returns an error if updating the version went wrong. 14 | func UpdateVersion(updateOptions *UpdateVersionOptions) error { 15 | var versionAPI = versions.NewAPI(updateOptions.GitTagsPrefix, updateOptions.GitTagsSuffix) 16 | return versionAPI.UpdateVersion() 17 | } 18 | -------------------------------------------------------------------------------- /pkg/cli/exec/entrypoint.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/restechnica/semverbot/pkg/cli" 8 | "github.com/restechnica/semverbot/pkg/cli/commands" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // Run will execute the CLI root command. 13 | func Run() (err error) { 14 | var command = commands.NewRootCommand() 15 | 16 | if err = command.Execute(); err != nil { 17 | if errors.As(err, &cli.CommandError{}) { 18 | log.Error().Err(err).Msg("") 19 | } 20 | 21 | os.Exit(1) 22 | } 23 | 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /pkg/core/push.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "github.com/restechnica/semverbot/pkg/versions" 4 | 5 | type PushVersionOptions struct { 6 | DefaultVersion string 7 | GitTagsPrefix string 8 | GitTagsSuffix string 9 | } 10 | 11 | // PushVersion pushes the current version. 12 | // Returns an error if the push went wrong. 13 | func PushVersion(options *PushVersionOptions) (err error) { 14 | var versionAPI = versions.NewAPI(options.GitTagsPrefix, options.GitTagsSuffix) 15 | var version = versionAPI.GetVersionOrDefault(options.DefaultVersion) 16 | return versionAPI.PushVersion(version) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/core/release.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/versions" 5 | ) 6 | 7 | // ReleaseVersion releases a new version. 8 | // Returns an error if anything went wrong with the prediction or releasing. 9 | func ReleaseVersion(predictOptions *PredictVersionOptions) error { 10 | var versionAPI = versions.NewAPI(predictOptions.GitTagsPrefix, predictOptions.GitTagsSuffix) 11 | var predictedVersion, err = PredictVersion(predictOptions) 12 | 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return versionAPI.ReleaseVersion(predictedVersion) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/versions/prefix_test.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAddPrefix(t *testing.T) { 11 | type Test struct { 12 | Name string 13 | Version string 14 | Prefix string 15 | } 16 | 17 | var tests = []Test{ 18 | {Name: "HappyPath", Version: "0.0.0", Prefix: "v"}, 19 | } 20 | 21 | for _, test := range tests { 22 | t.Run(test.Name, func(t *testing.T) { 23 | var got = AddPrefix(test.Version, test.Prefix) 24 | assert.True(t, strings.HasPrefix(got, test.Prefix)) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/mocks/mode.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | // MockMode a semver mode interface mock implementation. 6 | type MockMode struct { 7 | mock.Mock 8 | } 9 | 10 | // NewMockMode creates a new MockMode. 11 | // Returns the new MockMode. 12 | func NewMockMode() *MockMode { 13 | return &MockMode{} 14 | } 15 | 16 | // Increment mock increments a version. 17 | // Returns an incremented mock version. 18 | func (mock *MockMode) Increment(_ string, _ string, targetVersion string) (nextVersion string, err error) { 19 | args := mock.Called(targetVersion) 20 | return args.String(0), args.Error(1) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/ext/viperx/viper.go: -------------------------------------------------------------------------------- 1 | package viperx 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // LoadConfig loads a configuration file. 12 | // Returns an error if it fails. 13 | func LoadConfig(path string) (err error) { 14 | viper.AddConfigPath(filepath.Dir(path)) 15 | viper.SetConfigName(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) 16 | viper.SetConfigType(strings.Split(filepath.Ext(path), ".")[1]) 17 | 18 | log.Debug().Str("path", path).Msg("loading config file...") 19 | 20 | viper.AutomaticEnv() 21 | 22 | return viper.ReadInConfig() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/root.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // NewV1Command creates a new V1 root command. 8 | // Returns the new V1 root command. 9 | func NewV1Command() *cobra.Command { 10 | var command = &cobra.Command{ 11 | Use: "v1", 12 | Short: "v1 sbot API", 13 | } 14 | 15 | command.AddCommand(NewGetCommand()) 16 | command.AddCommand(NewInitCommand()) 17 | command.AddCommand(NewPredictCommand()) 18 | command.AddCommand(NewPushCommand()) 19 | command.AddCommand(NewReleaseCommand()) 20 | command.AddCommand(NewUpdateCommand()) 21 | command.AddCommand(NewVersionCommand()) 22 | 23 | return command 24 | } 25 | -------------------------------------------------------------------------------- /pkg/git/api.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | // API interface to interact with git. 4 | type API interface { 5 | CreateAnnotatedTag(tag string) (err error) 6 | FetchTags() (output string, err error) 7 | FetchUnshallow() (output string, err error) 8 | GetConfig(key string) (value string, err error) 9 | GetLatestAnnotatedTag() (tag string, err error) 10 | GetLatestCommitMessage() (message string, err error) 11 | GetMergedBranchName() (name string, err error) 12 | GetTags() (tags string, err error) 13 | PushTag(tag string) (err error) 14 | SetConfig(key string, value string) (err error) 15 | SetConfigIfNotSet(key string, value string) (actual string, err error) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/versions/suffix_test.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAddSuffix(t *testing.T) { 11 | type Test struct { 12 | Name string 13 | Version string 14 | Suffix string 15 | } 16 | 17 | var tests = []Test{ 18 | {Name: "HappyPath", Version: "0.0.0", Suffix: "a"}, 19 | {Name: "HappyPathAlt", Version: "0.0.0", Suffix: "-alt"}, 20 | } 21 | 22 | for _, test := range tests { 23 | t.Run(test.Name, func(t *testing.T) { 24 | var got = AddSuffix(test.Version, test.Suffix) 25 | assert.True(t, strings.HasSuffix(got, test.Suffix)) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/semver/parse.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "strings" 5 | 6 | blangsemver "github.com/blang/semver/v4" 7 | ) 8 | 9 | // Parse parses a version string into a semver version struct. 10 | // It tolerates certain version specifications that do not strictly adhere to semver specs. 11 | // See the library documentation for more information. 12 | // Returns the parsed blang/semver/v4 Version. 13 | func Parse(prefix string, suffix string, version string) (blangsemver.Version, error) { 14 | var versionWithoutPrefix = strings.Replace(version, prefix, "v", 1) 15 | var versionWithoutPrefixAndSuffix = strings.Replace(versionWithoutPrefix, suffix, "", 1) 16 | return blangsemver.ParseTolerant(versionWithoutPrefixAndSuffix) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/semver/trim.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "strings" 5 | 6 | blangsemver "github.com/blang/semver/v4" 7 | ) 8 | 9 | // Trim trims a semver version string of anything but major.minor.patch information. 10 | // Returns the trimmed semver version. 11 | func Trim(prefix string, suffix string, version string) (string, error) { 12 | var semverVersion blangsemver.Version 13 | var err error 14 | 15 | var versionWithoutPrefix = strings.Replace(version, prefix, prefix, 1) 16 | var versionWithoutPrefixOrSuffix = strings.Replace(versionWithoutPrefix, suffix, suffix, 1) 17 | 18 | if semverVersion, err = Parse(prefix, suffix, versionWithoutPrefixOrSuffix); err != nil { 19 | return version, err 20 | } 21 | 22 | return semverVersion.FinalizeVersion(), err 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # SemverBot Contributions 2 | 3 | ## How to setup your development environment 4 | 5 | 1. Install Go v1.21.x, for example: 6 | - [Using the Go Installation](https://go.dev/doc/manage-install) 7 | - or [Using GoEnv](https://github.com/go-nv/goenv/blob/master/INSTALL.md#installation) 8 | 9 | 2. [Install Nushell](https://www.nushell.sh) 10 | 11 | 3. Run following command to install all dependencies 12 | 13 | ```sh 14 | make provision 15 | ``` 16 | 17 | 4. During development use following commands: 18 | 19 | To format your code, run: 20 | 21 | ```sh 22 | make format 23 | ``` 24 | 25 | To perform quality assessment checks, run: 26 | 27 | ```sh 28 | make check 29 | ``` 30 | 31 | To test your code, run: 32 | 33 | ```sh 34 | make test 35 | ``` -------------------------------------------------------------------------------- /internal/mocks/commander.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | // MockCommander a commander interface mock implementation. 6 | type MockCommander struct { 7 | mock.Mock 8 | } 9 | 10 | // NewMockCommander creates a new MockCommander. 11 | // Returns the new MockCommander. 12 | func NewMockCommander() *MockCommander { 13 | return &MockCommander{} 14 | } 15 | 16 | // Output runs a mock command. 17 | // Returns mocked output or a mocked error. 18 | func (mock *MockCommander) Output(name string, arg ...string) (string, error) { 19 | args := mock.Called(name, arg) 20 | return args.String(0), args.Error(1) 21 | } 22 | 23 | // Run runs a mock command. 24 | // Returns a mocked error. 25 | func (mock *MockCommander) Run(name string, arg ...string) error { 26 | args := mock.Called(name, arg) 27 | return args.Error(0) 28 | } 29 | -------------------------------------------------------------------------------- /internal/constants.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "github.com/restechnica/semverbot/pkg/modes" 4 | 5 | const ( 6 | // DefaultConfigFilePath the default relative filepath to the config file. 7 | DefaultConfigFilePath = ".semverbot.toml" 8 | 9 | // DefaultGitBranchDelimiters the default delimiters used by the git-branch mode. 10 | DefaultGitBranchDelimiters = "/" 11 | 12 | // DefaultGitCommitDelimiters the default delimiters used by the git-commit mode. 13 | DefaultGitCommitDelimiters = "[]/" 14 | 15 | // DefaultGitTagsPrefix the default prefix prepended to git tags. 16 | DefaultGitTagsPrefix = "v" 17 | 18 | // DefaultGitTagsSuffix the default prefix prepended to git tags. 19 | DefaultGitTagsSuffix = "" 20 | 21 | // DefaultMode the default mode for incrementing versions. 22 | DefaultMode = modes.Auto 23 | 24 | // DefaultVersion the default version when no other version can be found. 25 | DefaultVersion = "0.0.0" 26 | ) 27 | -------------------------------------------------------------------------------- /pkg/cli/configs.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | const ( 4 | // GitConfigEmailConfigKey key for the git email config. 5 | GitConfigEmailConfigKey = "git.config.email" 6 | 7 | // GitConfigNameConfigKey key for the git name config. 8 | GitConfigNameConfigKey = "git.config.name" 9 | 10 | // GitTagsPrefixConfigKey key for the git tags prefix config. 11 | GitTagsPrefixConfigKey = "git.tags.prefix" 12 | 13 | // GitTagsSuffixConfigKey key for the git tags suffix config. 14 | GitTagsSuffixConfigKey = "git.tags.suffix" 15 | 16 | // ModeConfigKey key for the mode config. 17 | ModeConfigKey = "mode" 18 | 19 | // ModesGitBranchDelimitersConfigKey key for the git-branch delimiters config. 20 | ModesGitBranchDelimitersConfigKey = "modes.git-branch.delimiters" 21 | 22 | // ModesGitCommitDelimitersConfigKey key for the git-commit delimiters config. 23 | ModesGitCommitDelimitersConfigKey = "modes.git-commit.delimiters" 24 | 25 | // SemverMapConfigKey key for the semver map config. 26 | SemverMapConfigKey = "semver" 27 | ) 28 | -------------------------------------------------------------------------------- /internal/util/strings.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | // Contains returns true if a slice, created by splitting a target string by delimiters, contains a value. 6 | func Contains(target string, value string, delimiters string) bool { 7 | var slice = SplitByDelimiterString(target, delimiters) 8 | return SliceContainsString(slice, value) 9 | } 10 | 11 | // SplitByDelimiterString splits a string by multiple delimiters. 12 | // Returns the resulting slice of strings. 13 | func SplitByDelimiterString(target string, delimiters string) []string { 14 | var splitDelimiters = strings.Split(delimiters, "") 15 | 16 | return strings.FieldsFunc(target, func(r rune) bool { 17 | for _, delimiter := range splitDelimiters { 18 | if delimiter == string(r) { 19 | return true 20 | } 21 | } 22 | return false 23 | }) 24 | } 25 | 26 | // SliceContainsString returns true if a string equals an element in the slice. 27 | func SliceContainsString(slice []string, value string) bool { 28 | for _, element := range slice { 29 | if element == value { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /pkg/modes/api.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | // API an API to work with different modes. 6 | type API struct { 7 | GitBranchMode GitBranchMode 8 | GitCommitMode GitCommitMode 9 | } 10 | 11 | // NewAPI creates a new semver mode API. 12 | // Returns the new API. 13 | func NewAPI(gitBranchMode GitBranchMode, gitCommitMode GitCommitMode) API { 14 | return API{ 15 | GitBranchMode: gitBranchMode, 16 | GitCommitMode: gitCommitMode, 17 | } 18 | } 19 | 20 | // SelectMode selects the mode corresponding to the mode string. 21 | // Returns the corresponding mode. 22 | func (api API) SelectMode(mode string) Mode { 23 | switch mode { 24 | case Auto: 25 | return NewAutoMode([]Mode{api.GitBranchMode, api.GitCommitMode}) 26 | case GitCommit: 27 | return api.GitCommitMode 28 | case GitBranch: 29 | return api.GitBranchMode 30 | case Patch: 31 | return NewPatchMode() 32 | case Minor: 33 | return NewMinorMode() 34 | case Major: 35 | return NewMajorMode() 36 | default: 37 | log.Warn().Msg("mode invalid, falling back to patch mode") 38 | return NewPatchMode() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/core/init.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/AlecAivazis/survey/v2" 8 | ) 9 | 10 | type InitOptions struct { 11 | ConfigFilePath string 12 | Config string 13 | } 14 | 15 | // Init initializes a config file with defaults. 16 | // It will prompt for confirmation before overwriting existing files. 17 | // Returns an error if something went wrong with IO operations or the prompt. 18 | func Init(options *InitOptions) (err error) { 19 | var file *os.File 20 | 21 | if _, err = os.Stat(options.ConfigFilePath); !os.IsNotExist(err) { 22 | var prompt = &survey.Confirm{ 23 | Message: "Do you wish to overwrite your current config?", 24 | } 25 | 26 | var isOk = false 27 | 28 | if err = survey.AskOne(prompt, &isOk); err != nil { 29 | return err 30 | } 31 | 32 | if !isOk { 33 | return 34 | } 35 | } 36 | 37 | if file, err = os.Create(options.ConfigFilePath); err != nil { 38 | return err 39 | } 40 | 41 | if _, err = io.WriteString(file, options.Config); err != nil { 42 | _ = file.Close() 43 | return err 44 | } 45 | 46 | return file.Close() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/get-version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rs/zerolog/log" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/restechnica/semverbot/pkg/cli" 11 | "github.com/restechnica/semverbot/pkg/core" 12 | ) 13 | 14 | // NewGetVersionCommand creates a new get version command. 15 | // Returns the new spf13/cobra command. 16 | func NewGetVersionCommand() *cobra.Command { 17 | var command = &cobra.Command{ 18 | Use: "version", 19 | Run: GetVersionCommandRun, 20 | } 21 | 22 | return command 23 | } 24 | 25 | // GetVersionCommandRun runs the command. 26 | func GetVersionCommandRun(cmd *cobra.Command, args []string) { 27 | log.Debug().Str("command", "v1.get-version").Msg("starting run...") 28 | 29 | var options = &core.GetVersionOptions{ 30 | GitTagPrefix: viper.GetString(cli.GitTagsPrefixConfigKey), 31 | GitTagSuffix: viper.GetString(cli.GitTagsSuffixConfigKey), 32 | DefaultVersion: cli.DefaultVersion, 33 | } 34 | 35 | log.Debug().Str("default", options.DefaultVersion).Msg("options") 36 | 37 | var version = core.GetVersion(options) 38 | fmt.Println(version) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/update-version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | 8 | "github.com/restechnica/semverbot/pkg/cli" 9 | "github.com/restechnica/semverbot/pkg/core" 10 | ) 11 | 12 | // NewUpdateVersionCommand creates a new update version command. 13 | // Returns the new spf13/cobra command. 14 | func NewUpdateVersionCommand() *cobra.Command { 15 | var command = &cobra.Command{ 16 | Use: "version", 17 | RunE: UpdateVersionCommandRunE, 18 | } 19 | 20 | return command 21 | } 22 | 23 | // UpdateVersionCommandRunE runs the commands. 24 | // Returns an error if it fails. 25 | func UpdateVersionCommandRunE(cmd *cobra.Command, args []string) (err error) { 26 | log.Debug().Str("command", "v1.update-version").Msg("starting run...") 27 | 28 | var updateOptions = &core.UpdateVersionOptions{ 29 | GitTagsPrefix: viper.GetString(cli.GitTagsPrefixConfigKey), 30 | GitTagsSuffix: viper.GetString(cli.GitTagsSuffixConfigKey), 31 | } 32 | 33 | if err = core.UpdateVersion(updateOptions); err != nil { 34 | err = cli.NewCommandError(err) 35 | } 36 | 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/init.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rs/zerolog/log" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/restechnica/semverbot/pkg/cli" 10 | "github.com/restechnica/semverbot/pkg/core" 11 | ) 12 | 13 | // NewInitCommand creates a new init command. 14 | // Returns a new init spf13/cobra command. 15 | func NewInitCommand() *cobra.Command { 16 | var command = &cobra.Command{ 17 | Use: "init", 18 | RunE: InitCommandRunE, 19 | Short: fmt.Sprintf(`Creates a default "%s" config`, cli.DefaultConfigFilePath), 20 | } 21 | 22 | return command 23 | } 24 | 25 | // InitCommandRunE runs the init command. 26 | // Returns an error if the command failed. 27 | func InitCommandRunE(cmd *cobra.Command, args []string) (err error) { 28 | log.Debug().Str("command", "v1.init").Msg("starting run...") 29 | 30 | var options = &core.InitOptions{ 31 | Config: cli.GetDefaultConfig(), 32 | ConfigFilePath: cli.ConfigFlag, 33 | } 34 | 35 | log.Debug().Str("config", options.ConfigFilePath).Msg("options") 36 | 37 | if err = core.Init(options); err != nil { 38 | err = cli.NewCommandError(err) 39 | } 40 | 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /pkg/modes/major.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | blangsemver "github.com/blang/semver/v4" 5 | "github.com/restechnica/semverbot/pkg/semver" 6 | ) 7 | 8 | // Major semver version level for major 9 | const Major = "major" 10 | 11 | // MajorMode implementation of the Mode interface. 12 | // It makes use of the major level of semver versions. 13 | type MajorMode struct{} 14 | 15 | // NewMajorMode creates a new MajorMode. 16 | // Returns the new MajorMode. 17 | func NewMajorMode() MajorMode { 18 | return MajorMode{} 19 | } 20 | 21 | // Increment increments a given version using the MajorMode. 22 | // Returns the incremented version. 23 | func (mode MajorMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 24 | var version blangsemver.Version 25 | 26 | if version, err = semver.Parse(prefix, suffix, targetVersion); err != nil { 27 | return 28 | } 29 | 30 | // at point of writing IncrementMajor always returns a nil value error 31 | _ = version.IncrementMajor() 32 | 33 | return version.FinalizeVersion(), err 34 | } 35 | 36 | // String returns a string representation of an instance. 37 | func (mode MajorMode) String() string { 38 | return Major 39 | } 40 | -------------------------------------------------------------------------------- /pkg/modes/minor.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | blangsemver "github.com/blang/semver/v4" 5 | 6 | "github.com/restechnica/semverbot/pkg/semver" 7 | ) 8 | 9 | // Minor semver version level for minor 10 | const Minor = "minor" 11 | 12 | // MinorMode implementation of the Mode interface. 13 | // It makes use of the minor level of semver versions. 14 | type MinorMode struct{} 15 | 16 | // NewMinorMode creates a new MinorMode. 17 | // Returns the new MinorMode. 18 | func NewMinorMode() MinorMode { 19 | return MinorMode{} 20 | } 21 | 22 | // Increment increments a given version using the MinorMode. 23 | // Returns the incremented version. 24 | func (mode MinorMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 25 | var version blangsemver.Version 26 | 27 | if version, err = semver.Parse(prefix, suffix, targetVersion); err != nil { 28 | return 29 | } 30 | 31 | // at point of writing IncrementMinor always returns a nil value error 32 | _ = version.IncrementMinor() 33 | 34 | return version.FinalizeVersion(), err 35 | } 36 | 37 | // String returns a string representation of an instance. 38 | func (mode MinorMode) String() string { 39 | return Minor 40 | } 41 | -------------------------------------------------------------------------------- /pkg/modes/patch.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | blangsemver "github.com/blang/semver/v4" 5 | 6 | "github.com/restechnica/semverbot/pkg/semver" 7 | ) 8 | 9 | // Patch semver version level for patch 10 | const Patch = "patch" 11 | 12 | // PatchMode implementation of the Mode interface. 13 | // It makes use of the patch level of semver versions. 14 | type PatchMode struct{} 15 | 16 | // NewPatchMode creates a new PatchMode. 17 | // Returns the new PatchMode. 18 | func NewPatchMode() PatchMode { 19 | return PatchMode{} 20 | } 21 | 22 | // Increment increments a given version using the PatchMode. 23 | // Returns the incremented version. 24 | func (mode PatchMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 25 | var version blangsemver.Version 26 | 27 | if version, err = semver.Parse(prefix, suffix, targetVersion); err != nil { 28 | return 29 | } 30 | 31 | // at point of writing IncrementPatch always returns a nil value error 32 | _ = version.IncrementPatch() 33 | 34 | return version.FinalizeVersion(), err 35 | } 36 | 37 | // String returns a string representation of an instance. 38 | func (mode PatchMode) String() string { 39 | return Patch 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: development 2 | 3 | on: 4 | push: 5 | branches-ignore: [ main ] 6 | 7 | env: 8 | GO_VERSION: 1.21 9 | NUSHELL_VERSION: 0.91.0 10 | 11 | jobs: 12 | build: 13 | name: pipeline 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: set up go 19 | uses: actions/setup-go@v5 20 | with: 21 | cache-dependency-path: go.sum 22 | go-version: ${{ env.GO_VERSION }} 23 | 24 | - name: set up nushell 25 | uses: hustcer/setup-nu@v3.8 26 | with: 27 | version: ${{ env.NUSHELL_VERSION }} 28 | 29 | - name: set up path 30 | run: | 31 | mkdir bin 32 | echo "$(pwd)/bin" >> $GITHUB_PATH 33 | 34 | - name: provision 35 | run: make provision 36 | 37 | - name: check 38 | run: make check 39 | 40 | - name: test 41 | run: make test 42 | 43 | - name: build prerelease 44 | run: | 45 | make build 46 | sbot version 47 | sbot update version --debug 48 | 49 | - name: build all 50 | run: | 51 | make build-all 52 | cp bin/sbot-linux-amd64 bin/sbot 53 | sbot version 54 | -------------------------------------------------------------------------------- /pkg/semver/find.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | blangsemver "github.com/blang/semver/v4" 8 | ) 9 | 10 | // Find finds the biggest valid semver version in a slice of strings. 11 | // The initial order of the versions does not matter. 12 | // Returns the biggest valid semver version if found, otherwise an error stating no valid semver version has been found. 13 | func Find(prefix string, suffix string, versions []string) (found string, err error) { 14 | var parsedVersions blangsemver.Versions 15 | var parsedVersion blangsemver.Version 16 | 17 | for _, version := range versions { 18 | if parsedVersion, err = Parse(prefix, suffix, version); err != nil { 19 | continue 20 | } 21 | 22 | parsedVersions = append(parsedVersions, parsedVersion) 23 | } 24 | 25 | if len(parsedVersions) == 0 { 26 | return found, fmt.Errorf("could not find a valid semver version") 27 | } 28 | 29 | blangsemver.Sort(parsedVersions) 30 | 31 | var targetVersion = parsedVersions[len(parsedVersions)-1] 32 | 33 | // necessary because blangsemver's Version.String() strips any prefix 34 | for _, version := range versions { 35 | if strings.Contains(version, targetVersion.String()) { 36 | found = version 37 | break 38 | } 39 | } 40 | 41 | return found, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/push-version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | 8 | "github.com/restechnica/semverbot/pkg/cli" 9 | "github.com/restechnica/semverbot/pkg/core" 10 | ) 11 | 12 | // NewPushVersionCommand creates a new push version command. 13 | // Returns the new spf13/cobra command. 14 | func NewPushVersionCommand() *cobra.Command { 15 | var command = &cobra.Command{ 16 | Use: "version", 17 | RunE: PushVersionCommandRunE, 18 | } 19 | 20 | return command 21 | } 22 | 23 | // PushVersionCommandRunE runs the command. 24 | // Returns an error if the command fails. 25 | func PushVersionCommandRunE(cmd *cobra.Command, args []string) (err error) { 26 | log.Debug().Str("command", "v1.push-version").Msg("starting run...") 27 | 28 | var options = &core.PushVersionOptions{ 29 | DefaultVersion: cli.DefaultVersion, 30 | GitTagsPrefix: viper.GetString(cli.GitTagsPrefixConfigKey), 31 | GitTagsSuffix: viper.GetString(cli.GitTagsSuffixConfigKey), 32 | } 33 | 34 | log.Debug(). 35 | Str("default", options.DefaultVersion). 36 | Str("prefix", options.GitTagsPrefix). 37 | Str("suffix", options.GitTagsSuffix). 38 | Msg("options") 39 | 40 | if err = core.PushVersion(options); err != nil { 41 | err = cli.NewCommandError(err) 42 | } 43 | 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /pkg/core/predict.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/modes" 5 | "github.com/restechnica/semverbot/pkg/semver" 6 | "github.com/restechnica/semverbot/pkg/versions" 7 | ) 8 | 9 | type PredictVersionOptions struct { 10 | DefaultVersion string 11 | GitBranchDelimiters string 12 | GitCommitDelimiters string 13 | GitTagsPrefix string 14 | GitTagsSuffix string 15 | Mode string 16 | SemverMap semver.Map 17 | } 18 | 19 | // PredictVersion predicts a version based on a modes.Mode and a modes.Map. 20 | // The modes.Map values will be matched against git information to detect which semver level to increment. 21 | // Returns the next version or an error if the prediction failed. 22 | func PredictVersion(options *PredictVersionOptions) (prediction string, err error) { 23 | var gitBranchMode = modes.NewGitBranchMode(options.GitBranchDelimiters, options.SemverMap) 24 | var gitCommitMode = modes.NewGitCommitMode(options.GitCommitDelimiters, options.SemverMap) 25 | 26 | var versionAPI = versions.NewAPI(options.GitTagsPrefix, options.GitTagsSuffix) 27 | var version = versionAPI.GetVersionOrDefault(options.DefaultVersion) 28 | 29 | var modeAPI = modes.NewAPI(gitBranchMode, gitCommitMode) 30 | var mode = modeAPI.SelectMode(options.Mode) 31 | 32 | return versionAPI.PredictVersion(version, mode) 33 | } 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # make sure targets do not conflict with file and folder names 2 | .PHONY: build clean test 3 | 4 | # build the project 5 | build: build-prerelease 6 | 7 | build-prerelease: 8 | @nu main.nu build 9 | 10 | build-all: 11 | @nu main.nu build --all --version "$$(sbot predict version --debug)" 12 | 13 | # run quality assessment checks 14 | check: 15 | @echo "Running go version check ..." 16 | @go version | grep -q 'go1.21' || (echo "Go version 1.21 required. Your version is '$$(go version)'" && exit 1) 17 | @echo "Ok!" 18 | 19 | @echo "Running gofmt ..." 20 | @! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/' 21 | @echo "Ok!" 22 | 23 | @echo "Running go vet ..." 24 | @go vet ./... 25 | @echo "Ok!" 26 | 27 | @echo "Running goimports ..." 28 | @! goimports -l . | grep -vF 'No Exceptions' 29 | @echo "Ok!" 30 | 31 | # clean 32 | clean: 33 | rm -rf bin out 34 | 35 | # format 36 | format: 37 | go fmt ./... 38 | goimports -w . 39 | 40 | # get all dependencies 41 | provision: 42 | @echo "Getting dependencies ..." 43 | @go install golang.org/x/tools/cmd/goimports@latest 44 | @go install github.com/gregoryv/uncover/cmd/uncover@latest 45 | @go mod download 46 | @echo "Done!" 47 | 48 | # run the binary 49 | run: 50 | $(env) && ./bin/sbot $(args) 51 | 52 | # run tests 53 | test: 54 | mkdir -p ./out 55 | go test ./... -cover -v -coverprofile ./out/coverage.txt 56 | uncover ./out/coverage.txt 57 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | GO_VERSION: 1.21 9 | NUSHELL_VERSION: 0.91.0 10 | 11 | jobs: 12 | build: 13 | name: pipeline 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: set up go 19 | uses: actions/setup-go@v5 20 | with: 21 | cache-dependency-path: go.sum 22 | go-version: ${{ env.GO_VERSION }} 23 | 24 | - name: set up nushell 25 | uses: hustcer/setup-nu@v3.8 26 | with: 27 | version: ${{ env.NUSHELL_VERSION }} 28 | 29 | - name: set up path 30 | run: | 31 | mkdir bin 32 | echo "$(pwd)/bin" >> $GITHUB_PATH 33 | 34 | - name: provision 35 | run: make provision 36 | 37 | - name: check 38 | run: make check 39 | 40 | - name: test 41 | run: make test 42 | 43 | - name: build prerelease 44 | run: | 45 | make build 46 | sbot version 47 | sbot update version --debug 48 | 49 | - name: build all 50 | run: | 51 | make build-all 52 | cp bin/sbot-linux-amd64 bin/sbot 53 | sbot version 54 | 55 | - name: release 56 | run: | 57 | sbot update version --debug 58 | sbot release version --debug 59 | sbot push version --debug 60 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime/debug" 7 | 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/restechnica/semverbot/internal/ldflags" 12 | "github.com/restechnica/semverbot/pkg/cli" 13 | ) 14 | 15 | // NewVersionCommand creates a new version command. 16 | // It prints out useful version information about semverbot. 17 | // Returns the new version command. 18 | func NewVersionCommand() *cobra.Command { 19 | var command = &cobra.Command{ 20 | Use: "version", 21 | RunE: VersionCommandRunE, 22 | } 23 | 24 | return command 25 | } 26 | 27 | // VersionCommandRunE runs the command. 28 | // Returns an error if the command fails. 29 | func VersionCommandRunE(cmd *cobra.Command, args []string) (err error) { 30 | log.Debug().Str("command", "v1.version").Msg("starting run...") 31 | 32 | var info *debug.BuildInfo 33 | var ok bool 34 | 35 | if info, ok = debug.ReadBuildInfo(); !ok { 36 | return cli.NewCommandError(errors.New("failed to read build info")) 37 | } 38 | 39 | var arch, os string 40 | 41 | for _, setting := range info.Settings { 42 | if setting.Key == "GOARCH" { 43 | arch = setting.Value 44 | } 45 | 46 | if setting.Key == "GOOS" { 47 | os = setting.Value 48 | } 49 | } 50 | 51 | fmt.Printf( 52 | "sbot-cli %s %s %s/%s\n", 53 | ldflags.Version, 54 | info.GoVersion, 55 | os, 56 | arch, 57 | ) 58 | 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /pkg/modes/auto.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | // Auto mode name for AutoMode. 6 | const Auto = "auto" 7 | 8 | // AutoMode implementation of the Mode interface. 9 | // It makes use of several modes and defaults to PatchMode as a last resort. 10 | type AutoMode struct { 11 | Modes []Mode 12 | } 13 | 14 | // NewAutoMode creates a new AutoMode. 15 | // The order of modes in the modes slices is important and determines in which order the modes are applied in AutoMode.Increment. 16 | // Returns the new AutoMode. 17 | func NewAutoMode(modes []Mode) AutoMode { 18 | return AutoMode{Modes: modes} 19 | } 20 | 21 | // Increment increments a given version using AutoMode. 22 | // It will attempt to increment the target version with its internal modes and defaults to PatchMode as a last resort. 23 | // Returns the incremented version or an error if anything went wrong. 24 | func (autoMode AutoMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 25 | for _, mode := range autoMode.Modes { 26 | if nextVersion, err = mode.Increment(prefix, suffix, targetVersion); err == nil { 27 | return nextVersion, err 28 | } 29 | 30 | log.Debug().Err(err).Msgf("tried %s", mode) 31 | } 32 | 33 | log.Warn().Msg("falling back to patch mode") 34 | 35 | return PatchMode{}.Increment(prefix, suffix, targetVersion) 36 | } 37 | 38 | // String returns a string representation of an instance. 39 | func (autoMode AutoMode) String() string { 40 | return Auto 41 | } 42 | -------------------------------------------------------------------------------- /pkg/modes/gitcommit.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "github.com/restechnica/semverbot/pkg/git" 5 | "github.com/restechnica/semverbot/pkg/semver" 6 | ) 7 | 8 | // GitCommit mode name for GitCommitMode. 9 | const GitCommit = "git-commit" 10 | 11 | // GitCommitMode implementation of the Mode interface. 12 | // It increments the semver level based on the latest git commit messages. 13 | type GitCommitMode struct { 14 | Delimiters string 15 | GitAPI git.API 16 | SemverMap semver.Map 17 | } 18 | 19 | // NewGitCommitMode creates a new GitCommitMode. 20 | // Returns the new GitCommitMode. 21 | func NewGitCommitMode(delimiters string, semverMap semver.Map) GitCommitMode { 22 | return GitCommitMode{Delimiters: delimiters, GitAPI: git.NewCLI(), SemverMap: semverMap} 23 | } 24 | 25 | // Increment increments a given version based on the latest git commit message. 26 | // Returns the incremented version or an error if it failed to detect the mode based on the git commit. 27 | func (mode GitCommitMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 28 | var message string 29 | var detectedMode Mode 30 | 31 | if message, err = mode.GitAPI.GetLatestCommitMessage(); err != nil { 32 | return 33 | } 34 | 35 | if detectedMode, err = DetectModeFromString(message, mode.SemverMap, mode.Delimiters); err != nil { 36 | return 37 | } 38 | 39 | return detectedMode.Increment(prefix, suffix, targetVersion) 40 | } 41 | 42 | // String returns a string representation of an instance. 43 | func (mode GitCommitMode) String() string { 44 | return GitCommit 45 | } 46 | -------------------------------------------------------------------------------- /main.nu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | 3 | const output_path = "bin" 4 | 5 | def build-all [build_options: list] { 6 | print "building all binaries..." 7 | 8 | let architectures = ["amd64", "arm64"] 9 | let operating_systems = ["windows", "linux", "darwin"] 10 | 11 | $architectures | each { |arch| 12 | $operating_systems | each { |os| 13 | $env.GOOS = $os 14 | $env.GOARCH = $arch 15 | 16 | mut binary_path = $"($output_path)/sbot-($os)-($arch)" 17 | 18 | if $os == "windows" { 19 | $binary_path = $"($binary_path).exe" 20 | } 21 | 22 | let options = ["build", "-o", $binary_path] ++ $build_options 23 | 24 | run-external go ...$options 25 | print $"built ($binary_path)" 26 | } 27 | } 28 | } 29 | 30 | def build-local [build_options: list] { 31 | print "building local binary..." 32 | let binary_path = $"($output_path)/sbot" 33 | let options = ["build", "-o", $binary_path] ++ $build_options 34 | run-external go ...$options 35 | print $"built ($binary_path)" 36 | } 37 | 38 | def get-ldflags [version: string] { 39 | const go_import_path = "github.com/restechnica/semverbot" 40 | 41 | let ldflags = [ 42 | $"-X ($go_import_path)/internal/ldflags.Version=($version)" 43 | ] | str join ' ' 44 | 45 | return $ldflags 46 | } 47 | 48 | def "main build" [--all, --version: string = "dev"] { 49 | let ldflags = get-ldflags $version 50 | 51 | let options: list = ["-ldflags", $ldflags] 52 | 53 | if $all { 54 | build-all $options 55 | } else { 56 | build-local $options 57 | } 58 | } 59 | 60 | def main [] {} 61 | -------------------------------------------------------------------------------- /pkg/modes/gitbranch.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/restechnica/semverbot/pkg/git" 7 | "github.com/restechnica/semverbot/pkg/semver" 8 | ) 9 | 10 | // GitBranch mode name for GitBranchMode. 11 | const GitBranch = "git-branch" 12 | 13 | // GitBranchMode implementation of the Mode interface. 14 | // It increments the semver level based on the naming of the source branch of a git merge. 15 | type GitBranchMode struct { 16 | Delimiters string 17 | GitAPI git.API 18 | SemverMap semver.Map 19 | } 20 | 21 | // NewGitBranchMode creates a new GitBranchMode. 22 | // Returns the new GitBranchMode. 23 | func NewGitBranchMode(delimiters string, semverMap semver.Map) GitBranchMode { 24 | return GitBranchMode{Delimiters: delimiters, GitAPI: git.NewCLI(), SemverMap: semverMap} 25 | } 26 | 27 | // Increment increments the semver level based on the naming of the source branch of a git merge. 28 | // Returns the incremented version or an error if the last git commit is not a merge or if no mode was detected 29 | // based on the branch name. 30 | func (mode GitBranchMode) Increment(prefix string, suffix string, targetVersion string) (nextVersion string, err error) { 31 | var branchName string 32 | var matchedMode Mode 33 | 34 | if branchName, err = mode.GitAPI.GetMergedBranchName(); err != nil { 35 | return 36 | } 37 | 38 | var isMergeCommit = branchName != "" 39 | 40 | if !isMergeCommit { 41 | return nextVersion, fmt.Errorf("failed to increment version because the latest git commit is not a merge commit") 42 | } 43 | 44 | if matchedMode, err = DetectModeFromString(branchName, mode.SemverMap, mode.Delimiters); err != nil { 45 | return nextVersion, err 46 | } 47 | 48 | return matchedMode.Increment(prefix, suffix, targetVersion) 49 | } 50 | 51 | // String returns a string representation of an instance. 52 | func (mode GitBranchMode) String() string { 53 | return GitBranch 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/restechnica/semverbot 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.2.12 7 | github.com/blang/semver/v4 v4.0.0 8 | github.com/restechnica/go-cmder v0.1.1 9 | github.com/rs/zerolog v1.31.0 10 | github.com/spf13/cobra v1.8.0 11 | github.com/spf13/viper v1.18.2 12 | github.com/stretchr/testify v1.8.4 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 17 | github.com/fsnotify/fsnotify v1.7.0 // indirect 18 | github.com/hashicorp/hcl v1.0.0 // indirect 19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 20 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 21 | github.com/magiconair/properties v1.8.7 // indirect 22 | github.com/mattn/go-colorable v0.1.13 // indirect 23 | github.com/mattn/go-isatty v0.0.19 // indirect 24 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 25 | github.com/mitchellh/mapstructure v1.5.0 // indirect 26 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 28 | github.com/sagikazarmark/locafero v0.4.0 // indirect 29 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 30 | github.com/sourcegraph/conc v0.3.0 // indirect 31 | github.com/spf13/afero v1.11.0 // indirect 32 | github.com/spf13/cast v1.6.0 // indirect 33 | github.com/spf13/pflag v1.0.5 // indirect 34 | github.com/stretchr/objx v0.5.0 // indirect 35 | github.com/subosito/gotenv v1.6.0 // indirect 36 | go.uber.org/atomic v1.9.0 // indirect 37 | go.uber.org/multierr v1.9.0 // indirect 38 | golang.org/x/crypto v0.16.0 // indirect 39 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 40 | golang.org/x/sys v0.15.0 // indirect 41 | golang.org/x/term v0.15.0 // indirect 42 | golang.org/x/text v0.14.0 // indirect 43 | gopkg.in/ini.v1 v1.67.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /pkg/modes/detect.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/restechnica/semverbot/internal/util" 7 | "github.com/restechnica/semverbot/pkg/semver" 8 | ) 9 | 10 | func DetectModeFromString(str string, semverMap semver.Map, delimiters string) (detected Mode, err error) { 11 | var modes []Mode 12 | 13 | if modes, err = DetectModesFromString(str, semverMap, delimiters); err != nil { 14 | return nil, err 15 | } 16 | 17 | if len(modes) == 0 { 18 | return nil, fmt.Errorf(`failed to detect mode from string '%s' with delimiters '%s'`, str, delimiters) 19 | } 20 | 21 | var priority = map[string]int{ 22 | Patch: 1, 23 | Minor: 2, 24 | Major: 3, 25 | } 26 | 27 | for _, mode := range modes { 28 | if detected == nil { 29 | detected = mode 30 | } 31 | 32 | if priority[mode.String()] > priority[detected.String()] { 33 | detected = mode 34 | } 35 | } 36 | 37 | return detected, err 38 | } 39 | 40 | // DetectModesFromString detects multiple modes based on a string. 41 | // Mode detection is limited to PatchMode, MinorMode, MajorMode. 42 | // The order of a detected modes is relative to their position in the string. 43 | // Returns a slice of the detected modes. 44 | func DetectModesFromString(str string, semverMap semver.Map, delimiters string) (detected []Mode, err error) { 45 | var substrings = util.SplitByDelimiterString(str, delimiters) 46 | 47 | for _, substring := range substrings { 48 | for level, values := range semverMap { 49 | if util.SliceContainsString(values, substring) { 50 | switch level { 51 | case Patch: 52 | detected = append(detected, NewPatchMode()) 53 | case Minor: 54 | detected = append(detected, NewMinorMode()) 55 | case Major: 56 | detected = append(detected, NewMajorMode()) 57 | default: 58 | return nil, fmt.Errorf("failed to detect mode due to unsupported semver level: '%s'", level) 59 | } 60 | } 61 | } 62 | } 63 | 64 | return detected, err 65 | } 66 | -------------------------------------------------------------------------------- /pkg/cli/defaults.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/restechnica/semverbot/internal" 7 | ) 8 | 9 | var ( 10 | // DefaultAdditionalConfigFilePaths additional default relative filepaths to the config file. 11 | DefaultAdditionalConfigFilePaths = []string{".sbot.toml", ".semverbot/config.toml", ".sbot/config.toml"} 12 | 13 | // DefaultConfigFilePath the default relative filepath to the config file. 14 | DefaultConfigFilePath = internal.DefaultConfigFilePath 15 | 16 | // DefaultGitBranchDelimiters the default delimiters used by the git-branch mode. 17 | DefaultGitBranchDelimiters = internal.DefaultGitBranchDelimiters 18 | 19 | // DefaultGitCommitDelimiters the default delimiters used by the git-commit mode. 20 | DefaultGitCommitDelimiters = internal.DefaultGitCommitDelimiters 21 | 22 | // DefaultGitTagsPrefix the default prefix prepended to git tags. 23 | DefaultGitTagsPrefix = internal.DefaultGitTagsPrefix 24 | 25 | // DefaultGitTagsSuffix the default suffix prepended to git tags. 26 | DefaultGitTagsSuffix = internal.DefaultGitTagsSuffix 27 | 28 | // DefaultMode the default mode for incrementing versions. 29 | DefaultMode = internal.DefaultMode 30 | 31 | // DefaultVersion the default version when no other version can be found. 32 | DefaultVersion = internal.DefaultVersion 33 | ) 34 | 35 | func GetDefaultConfig() string { 36 | const template = `mode = "%s" 37 | 38 | [git] 39 | 40 | [git.config] 41 | email = "semverbot@github.com" 42 | name = "semverbot" 43 | 44 | [git.tags] 45 | prefix = "%s" 46 | suffix = "%s" 47 | 48 | [semver] 49 | patch = ["fix", "bug"] 50 | minor = ["feature"] 51 | major = ["release"] 52 | 53 | [modes] 54 | 55 | [modes.git-branch] 56 | delimiters = "%s" 57 | 58 | [modes.git-commit] 59 | delimiters = "%s" 60 | ` 61 | 62 | return fmt.Sprintf( 63 | template, 64 | DefaultMode, 65 | DefaultGitTagsPrefix, 66 | DefaultGitTagsSuffix, 67 | DefaultGitBranchDelimiters, 68 | DefaultGitCommitDelimiters, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/semver/parse_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParse(t *testing.T) { 12 | type Test struct { 13 | Name string 14 | Major string 15 | Minor string 16 | Patch string 17 | Prebuild string 18 | Prefix string 19 | Suffix string 20 | } 21 | 22 | var tests = []Test{ 23 | {Name: "Default", Major: "0", Minor: "0", Patch: "0"}, 24 | {Name: "Patch", Major: "0", Minor: "0", Patch: "1"}, 25 | {Name: "Minor", Major: "0", Minor: "2", Patch: "0"}, 26 | {Name: "Major", Major: "3", Minor: "0", Patch: "0"}, 27 | {Name: "DiscardPrefix", Major: "1", Minor: "0", Patch: "0", Prefix: "v"}, 28 | {Name: "DiscardSuffix", Major: "2", Minor: "0", Patch: "0", Suffix: "a"}, 29 | {Name: "DiscardSuffixAlt", Major: "2", Minor: "0", Patch: "0", Suffix: "-alt"}, 30 | {Name: "KeepPrebuild", Major: "2", Minor: "0", Patch: "0", Suffix: "", Prebuild: "-pre+001"}, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.Name, func(t *testing.T) { 35 | var version = fmt.Sprintf(`%s%s.%s.%s%s%s`, test.Prefix, test.Major, test.Minor, test.Patch, 36 | test.Suffix, test.Prebuild) 37 | 38 | var got, err = Parse(test.Prefix, test.Suffix, version) 39 | 40 | assert.Equal(t, test.Major, fmt.Sprint(got.Major), `want: "%s", got: "%d"`, test.Major, got.Major) 41 | assert.Equal(t, test.Minor, fmt.Sprint(got.Minor), `want: "%s", got: "%s"`, test.Minor, got.Minor) 42 | assert.Equal(t, test.Patch, fmt.Sprint(got.Patch), `want: "%s", got: "%s"`, test.Patch, got.Patch) 43 | 44 | if test.Prefix != "" { 45 | assert.False(t, strings.HasPrefix(got.String(), test.Prefix)) 46 | } 47 | 48 | if test.Suffix != "" { 49 | assert.False(t, strings.HasSuffix(got.String(), test.Suffix)) 50 | } 51 | 52 | if test.Prebuild != "" { 53 | assert.True(t, strings.HasSuffix(got.String(), test.Prebuild)) 54 | } 55 | 56 | assert.NoError(t, err) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/modes/minor_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMinorMode_Increment(t *testing.T) { 10 | type Test struct { 11 | Name string 12 | Prefix string 13 | Suffix string 14 | Version string 15 | Want string 16 | } 17 | 18 | var tests = []Test{ 19 | {Name: "IncrementMinor", Prefix: "v", Version: "0.0.0", Want: "0.1.0"}, 20 | {Name: "DiscardPrefix", Prefix: "v", Version: "v0.1.0", Want: "0.2.0"}, 21 | {Name: "DiscardSuffix", Prefix: "v", Suffix: "a", Version: "0.1.0a", Want: "0.2.0"}, 22 | {Name: "DiscardSuffixAlt", Prefix: "v", Suffix: "-alt", Version: "0.1.0-alt", Want: "0.2.0"}, 23 | {Name: "DiscardPrebuild", Prefix: "v", Version: "0.2.0-pre+001", Want: "0.3.0"}, 24 | {Name: "ResetPatch", Prefix: "v", Version: "3.0.4", Want: "3.1.0"}, 25 | } 26 | 27 | for _, test := range tests { 28 | var mode = NewMinorMode() 29 | var got, err = mode.Increment(test.Prefix, test.Suffix, test.Version) 30 | 31 | assert.NoError(t, err) 32 | assert.IsType(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 33 | } 34 | 35 | t.Run("ReturnErrorOnInvalidVersion", func(t *testing.T) { 36 | var mode = NewMinorMode() 37 | var _, got = mode.Increment("v", "", "invalid") 38 | assert.Error(t, got) 39 | }) 40 | } 41 | 42 | func TestMinorMode_MinorConstant(t *testing.T) { 43 | t.Run("CheckConstant", func(t *testing.T) { 44 | var want = "minor" 45 | var got = Minor 46 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 47 | }) 48 | } 49 | 50 | func TestMinorMode_String(t *testing.T) { 51 | t.Run("ShouldEqualConstant", func(t *testing.T) { 52 | var mode = NewMinorMode() 53 | var got = mode.String() 54 | var want = Minor 55 | 56 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 57 | }) 58 | } 59 | 60 | func TestNewMinorMode(t *testing.T) { 61 | t.Run("ValidateState", func(t *testing.T) { 62 | var mode = NewMinorMode() 63 | assert.NotNil(t, mode) 64 | assert.IsType(t, MinorMode{}, mode) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/modes/patch_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPatchMode_Increment(t *testing.T) { 10 | type Test struct { 11 | Name string 12 | Prefix string 13 | Suffix string 14 | Version string 15 | Want string 16 | } 17 | 18 | var tests = []Test{ 19 | {Name: "IncrementPatch", Prefix: "v", Version: "0.0.0", Want: "0.0.1"}, 20 | {Name: "DiscardPrefix", Prefix: "v", Version: "v0.0.1", Want: "0.0.2"}, 21 | {Name: "DiscardSuffix", Prefix: "v", Suffix: "a", Version: "v0.0.1a", Want: "0.0.2"}, 22 | {Name: "DiscardSuffixAlt", Prefix: "v", Suffix: "-alt", Version: "v0.0.1-alt", Want: "0.0.2"}, 23 | {Name: "DiscardPrebuild", Prefix: "v", Version: "0.0.2-pre+001", Want: "0.0.3"}, 24 | {Name: "NoResets", Prefix: "v", Version: "3.2.0", Want: "3.2.1"}, 25 | } 26 | 27 | for _, test := range tests { 28 | var mode = NewPatchMode() 29 | var got, err = mode.Increment(test.Prefix, test.Suffix, test.Version) 30 | 31 | assert.NoError(t, err) 32 | assert.IsType(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 33 | } 34 | 35 | t.Run("ReturnErrorOnInvalidVersion", func(t *testing.T) { 36 | var mode = NewPatchMode() 37 | var _, got = mode.Increment("v", "", "invalid") 38 | assert.Error(t, got) 39 | }) 40 | } 41 | 42 | func TestPatchMode_PatchConstant(t *testing.T) { 43 | t.Run("CheckConstant", func(t *testing.T) { 44 | var want = "patch" 45 | var got = Patch 46 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 47 | }) 48 | } 49 | 50 | func TestPatchMode_String(t *testing.T) { 51 | t.Run("ShouldEqualConstant", func(t *testing.T) { 52 | var mode = NewPatchMode() 53 | var got = mode.String() 54 | var want = Patch 55 | 56 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 57 | }) 58 | } 59 | 60 | func TestNewPatchMode(t *testing.T) { 61 | t.Run("ValidateState", func(t *testing.T) { 62 | var mode = NewPatchMode() 63 | assert.NotNil(t, mode) 64 | assert.IsType(t, PatchMode{}, mode) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/modes/api_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/restechnica/semverbot/pkg/semver" 9 | ) 10 | 11 | func TestAPI_SelectMode(t *testing.T) { 12 | var semverMap = semver.Map{ 13 | Patch: {"fix", "bug"}, 14 | Minor: {"feature"}, 15 | Major: {"release"}, 16 | } 17 | 18 | var gitBranchDelimiters = "/" 19 | var gitCommitDelimiters = "[]():" 20 | 21 | type Test struct { 22 | Mode string 23 | Name string 24 | Want Mode 25 | } 26 | 27 | var tests = []Test{ 28 | {Name: "SelectPatchMode", Mode: Patch, Want: NewPatchMode()}, 29 | {Name: "SelectPatchModeIfInvalidMode", Mode: "invalid", Want: NewPatchMode()}, 30 | {Name: "SelectMinorMode", Mode: Minor, Want: NewMinorMode()}, 31 | {Name: "SelectMajorMode", Mode: Major, Want: NewMajorMode()}, 32 | {Name: "SelectAutoMode", Mode: Auto, Want: AutoMode{}}, 33 | {Name: "SelectGitBranchMode", Mode: GitBranch, Want: GitBranchMode{}}, 34 | {Name: "SelectGitCommitMode", Mode: GitCommit, Want: GitCommitMode{}}, 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.Name, func(t *testing.T) { 39 | var gitBranchMode = NewGitBranchMode(gitBranchDelimiters, semverMap) 40 | var gitCommitMode = NewGitCommitMode(gitCommitDelimiters, semverMap) 41 | 42 | var modeAPI = NewAPI(gitBranchMode, gitCommitMode) 43 | var got = modeAPI.SelectMode(test.Mode) 44 | 45 | assert.IsType(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 46 | }) 47 | } 48 | } 49 | 50 | func TestNewAPI(t *testing.T) { 51 | t.Run("ValidateState", func(t *testing.T) { 52 | var gitBranchDelimiters = "/" 53 | var gitCommitDelimiters = "[]" 54 | 55 | var semverMap = semver.Map{} 56 | var gitBranchMode = NewGitBranchMode(gitBranchDelimiters, semverMap) 57 | var gitCommitMode = NewGitCommitMode(gitCommitDelimiters, semverMap) 58 | var modeAPI = NewAPI(gitBranchMode, gitCommitMode) 59 | 60 | assert.NotNil(t, modeAPI) 61 | assert.NotNil(t, modeAPI.GitBranchMode) 62 | assert.NotNil(t, modeAPI.GitCommitMode) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/modes/major_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMajorMode_Increment(t *testing.T) { 10 | type Test struct { 11 | Name string 12 | Version string 13 | Suffix string 14 | Want string 15 | } 16 | 17 | var tests = []Test{ 18 | {Name: "IncrementMajor", Version: "0.0.0", Want: "1.0.0"}, 19 | {Name: "DiscardPrefix", Version: "v1.0.0", Want: "2.0.0"}, 20 | {Name: "DiscardSuffix", Version: "1.0.0a", Suffix: "a", Want: "1.0.0"}, 21 | {Name: "DiscardSuffixAlt", Version: "1.0.0-alt", Suffix: "-alt", Want: "1.0.0"}, 22 | {Name: "DiscardPrebuild", Version: "2.0.0-pre+001", Want: "3.0.0"}, 23 | {Name: "ResetMinor", Version: "4.5.0", Want: "5.0.0"}, 24 | {Name: "ResetPatch", Version: "3.0.4", Want: "4.0.0"}, 25 | {Name: "ResetPatchAndMinor", Version: "2.1.5", Want: "3.0.0"}, 26 | } 27 | 28 | for _, test := range tests { 29 | var mode = NewMajorMode() 30 | var got, err = mode.Increment("v", test.Suffix, test.Version) 31 | 32 | assert.NoError(t, err) 33 | assert.IsType(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 34 | } 35 | 36 | t.Run("ReturnErrorOnInvalidVersion", func(t *testing.T) { 37 | var mode = NewMajorMode() 38 | var _, got = mode.Increment("v", "", "invalid") 39 | assert.Error(t, got) 40 | }) 41 | } 42 | 43 | func TestMajorMode_MajorConstant(t *testing.T) { 44 | t.Run("CheckConstant", func(t *testing.T) { 45 | var want = "major" 46 | var got = Major 47 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 48 | }) 49 | } 50 | 51 | func TestMajorMode_String(t *testing.T) { 52 | t.Run("ShouldEqualConstant", func(t *testing.T) { 53 | var mode = NewMajorMode() 54 | var got = mode.String() 55 | var want = Major 56 | 57 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 58 | }) 59 | } 60 | 61 | func TestNewMajorMode(t *testing.T) { 62 | t.Run("ValidateState", func(t *testing.T) { 63 | var mode = NewMajorMode() 64 | assert.NotNil(t, mode) 65 | assert.IsType(t, MajorMode{}, mode) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/semver/find_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFind(t *testing.T) { 10 | type Test struct { 11 | Name string 12 | Prefix string 13 | Suffix string 14 | Versions []string 15 | WantIndex int 16 | } 17 | 18 | var tests = []Test{ 19 | {Name: "FindVersionIfValid", Prefix: "v", Suffix: "", Versions: []string{"v1.0.1", "v0.1.1", "v0.1.0"}, WantIndex: 0}, 20 | {Name: "FindVersionWithCustomPrefixIfValid", Prefix: "test-", Suffix: "", Versions: []string{"test-1.0.1", "test-0.1.1", "test-0.1.0"}, WantIndex: 0}, 21 | {Name: "FindVersionWithCustomSuffixIfValid", Prefix: "v", Suffix: "a", Versions: []string{"v1.0.1a", "v0.1.1a", "v0.1.0a"}, WantIndex: 0}, 22 | {Name: "SkipVersionIfInvalid", Prefix: "v", Suffix: "", Versions: []string{"invalid1", "invalid2", "v0.1.0"}, WantIndex: 2}, 23 | {Name: "FindVersionWhenDifferentOrder", Prefix: "v", Suffix: "", Versions: []string{"v1.3.1", "v0.2.0", "v2.3.0"}, WantIndex: 2}, 24 | {Name: "FindVersionWhenMultiplePrefixes", Prefix: "v", Suffix: "", Versions: []string{"v1.3.1", "v0.2.0", "2.3.0"}, WantIndex: 2}, 25 | {Name: "FindVersionWhenMultipleSuffixes", Prefix: "v", Suffix: "n", Versions: []string{"v1.3.1a", "v0.2.0-alt", "v2.3.0n"}, WantIndex: 2}, 26 | } 27 | 28 | for _, test := range tests { 29 | t.Run(test.Name, func(t *testing.T) { 30 | var versions = test.Versions 31 | var want = versions[test.WantIndex] 32 | var got, err = Find(test.Prefix, test.Suffix, versions) 33 | 34 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 35 | assert.NoError(t, err) 36 | }) 37 | } 38 | 39 | type ErrorTest struct { 40 | Name string 41 | Versions []string 42 | } 43 | 44 | var errorTests = []ErrorTest{ 45 | {Name: "ReturnErrorOnInvalidVersions", Versions: []string{"invalid", "semver", "versions"}}, 46 | {Name: "ReturnErrorOnNoVersions", Versions: []string{}}, 47 | } 48 | 49 | for _, test := range errorTests { 50 | t.Run(test.Name, func(t *testing.T) { 51 | var _, got = Find("v", "a", test.Versions) 52 | assert.Error(t, got) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/predict-version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rs/zerolog/log" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/restechnica/semverbot/pkg/cli" 11 | "github.com/restechnica/semverbot/pkg/core" 12 | ) 13 | 14 | // NewPredictVersionCommand creates a new predict version command. 15 | // Returns the new spf13/cobra command. 16 | func NewPredictVersionCommand() *cobra.Command { 17 | var command = &cobra.Command{ 18 | Use: "version", 19 | PreRunE: PredictVersionCommandPreRunE, 20 | RunE: PredictVersionCommandRunE, 21 | } 22 | 23 | command.Flags().StringVarP(&cli.ModeFlag, "mode", "m", "auto", "sbot mode") 24 | 25 | return command 26 | } 27 | 28 | // PredictVersionCommandPreRunE runs before the command runs. 29 | // Returns an error if it fails. 30 | func PredictVersionCommandPreRunE(cmd *cobra.Command, args []string) (err error) { 31 | return viper.BindPFlag(cli.ModeConfigKey, cmd.Flags().Lookup("mode")) 32 | } 33 | 34 | // PredictVersionCommandRunE runs the command. 35 | // Returns an error if the command fails. 36 | func PredictVersionCommandRunE(cmd *cobra.Command, args []string) (err error) { 37 | log.Debug().Str("command", "v1.predict-version").Msg("starting run...") 38 | 39 | var options = &core.PredictVersionOptions{ 40 | DefaultVersion: cli.DefaultVersion, 41 | GitBranchDelimiters: viper.GetString(cli.ModesGitBranchDelimitersConfigKey), 42 | GitCommitDelimiters: viper.GetString(cli.ModesGitCommitDelimitersConfigKey), 43 | GitTagsPrefix: viper.GetString(cli.GitTagsPrefixConfigKey), 44 | GitTagsSuffix: viper.GetString(cli.GitTagsSuffixConfigKey), 45 | Mode: viper.GetString(cli.ModeConfigKey), 46 | SemverMap: viper.GetStringMapStringSlice(cli.SemverMapConfigKey), 47 | } 48 | 49 | log.Debug(). 50 | Str("default", options.DefaultVersion). 51 | Str("mode", options.Mode). 52 | Msg("options") 53 | 54 | var version string 55 | 56 | if version, err = core.PredictVersion(options); err != nil { 57 | err = cli.NewCommandError(err) 58 | } else { 59 | fmt.Println(version) 60 | } 61 | 62 | return err 63 | } 64 | -------------------------------------------------------------------------------- /pkg/cli/commands/v1/release-version.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | 8 | "github.com/restechnica/semverbot/pkg/cli" 9 | "github.com/restechnica/semverbot/pkg/core" 10 | ) 11 | 12 | // NewReleaseVersionCommand creates a new release version command. 13 | // Returns the new spf13/cobra command. 14 | func NewReleaseVersionCommand() *cobra.Command { 15 | var command = &cobra.Command{ 16 | Use: "version", 17 | PreRunE: ReleaseVersionCommandPreRunE, 18 | RunE: ReleaseVersionCommandRunE, 19 | } 20 | 21 | command.Flags().StringVarP(&cli.ModeFlag, "mode", "m", "", "sbot mode") 22 | 23 | return command 24 | } 25 | 26 | // ReleaseVersionCommandPreRunE runs before the command runs. 27 | // Returns an error if it fails. 28 | func ReleaseVersionCommandPreRunE(cmd *cobra.Command, args []string) (err error) { 29 | return viper.BindPFlag(cli.ModeConfigKey, cmd.Flags().Lookup("mode")) 30 | } 31 | 32 | // ReleaseVersionCommandRunE runs the command. 33 | // Returns an error if the command fails. 34 | func ReleaseVersionCommandRunE(cmd *cobra.Command, args []string) (err error) { 35 | log.Debug().Str("command", "v1.release-version").Msg("starting run...") 36 | 37 | var predictOptions = &core.PredictVersionOptions{ 38 | DefaultVersion: cli.DefaultVersion, 39 | GitBranchDelimiters: viper.GetString(cli.ModesGitBranchDelimitersConfigKey), 40 | GitCommitDelimiters: viper.GetString(cli.ModesGitCommitDelimitersConfigKey), 41 | GitTagsPrefix: viper.GetString(cli.GitTagsPrefixConfigKey), 42 | GitTagsSuffix: viper.GetString(cli.GitTagsSuffixConfigKey), 43 | Mode: viper.GetString(cli.ModeConfigKey), 44 | SemverMap: viper.GetStringMapStringSlice(cli.SemverMapConfigKey), 45 | } 46 | 47 | log.Debug(). 48 | Str("default", predictOptions.DefaultVersion). 49 | Str("mode", predictOptions.Mode). 50 | Str("prefix", predictOptions.GitTagsPrefix). 51 | Str("suffix", predictOptions.GitTagsSuffix). 52 | Msg("options") 53 | 54 | if err = core.ReleaseVersion(predictOptions); err != nil { 55 | err = cli.NewCommandError(err) 56 | } 57 | 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /pkg/semver/trim_test.go: -------------------------------------------------------------------------------- 1 | package semver 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTrim(t *testing.T) { 12 | type Test struct { 13 | Name string 14 | Major string 15 | Minor string 16 | Patch string 17 | Prebuild string 18 | Prefix string 19 | Suffix string 20 | } 21 | 22 | var tests = []Test{ 23 | {Name: "Default", Major: "0", Minor: "0", Patch: "0"}, 24 | {Name: "Patch", Major: "0", Minor: "0", Patch: "1"}, 25 | {Name: "Minor", Major: "0", Minor: "2", Patch: "0"}, 26 | {Name: "Major", Major: "3", Minor: "0", Patch: "0"}, 27 | {Name: "DiscardPrefix", Major: "1", Minor: "0", Patch: "0", Prefix: "v"}, 28 | {Name: "DiscardSuffix", Major: "1", Minor: "0", Patch: "0", Suffix: "a"}, 29 | {Name: "DiscardSuffixAlt", Major: "1", Minor: "0", Patch: "0", Suffix: "-alt"}, 30 | {Name: "DiscardPrebuild", Major: "2", Minor: "0", Patch: "0", Prebuild: "-pre+001"}, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.Name, func(t *testing.T) { 35 | var version = fmt.Sprintf(`%s%s.%s.%s%s%s`, test.Prefix, test.Major, test.Minor, test.Patch, 36 | test.Suffix, test.Prebuild) 37 | 38 | var want = strings.ReplaceAll(version, test.Prefix, "") 39 | want = strings.ReplaceAll(want, test.Suffix, "") 40 | want = strings.ReplaceAll(want, test.Prebuild, "") 41 | 42 | var got, err = Trim(test.Prefix, test.Suffix, version) 43 | 44 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 45 | 46 | if test.Prefix != "" { 47 | assert.False(t, strings.HasPrefix(got, test.Prefix)) 48 | } 49 | 50 | if test.Suffix != "" { 51 | assert.False(t, strings.HasSuffix(got, test.Suffix)) 52 | } 53 | 54 | if test.Prebuild != "" { 55 | assert.False(t, strings.HasSuffix(got, test.Prebuild)) 56 | } 57 | 58 | assert.NoError(t, err) 59 | }) 60 | } 61 | 62 | type ErrorTest struct { 63 | Name string 64 | Version string 65 | } 66 | 67 | var errorTests = []ErrorTest{ 68 | {Name: "ReturnErrorOnInvalidVersion", Version: "invalid"}, 69 | } 70 | 71 | for _, test := range errorTests { 72 | t.Run(test.Name, func(t *testing.T) { 73 | var _, got = Trim("v", "a", test.Version) 74 | assert.Error(t, got) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/modes/auto_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | 10 | "github.com/restechnica/semverbot/internal/mocks" 11 | ) 12 | 13 | func TestAutoMode_AutoConstant(t *testing.T) { 14 | t.Run("CheckConstant", func(t *testing.T) { 15 | var want = "auto" 16 | var got = Auto 17 | 18 | assert.Equal(t, want, got, `want: "%s", got: "%s"`, want, got) 19 | }) 20 | } 21 | 22 | func TestAutoMode_Increment(t *testing.T) { 23 | type Test struct { 24 | Modes []Mode 25 | Name string 26 | Prefix string 27 | Suffix string 28 | Version string 29 | Want string 30 | } 31 | 32 | var mockMode = mocks.NewMockMode() 33 | mockMode.On("Increment", mock.Anything, mock.Anything, mock.Anything).Return("", fmt.Errorf("some-error")) 34 | 35 | var tests = []Test{ 36 | {Name: "IncrementMajor", Prefix: "v", Suffix: "", Modes: []Mode{NewMajorMode()}, Version: "0.0.0", Want: "1.0.0"}, 37 | {Name: "IncrementMinor", Prefix: "v", Suffix: "", Modes: []Mode{NewMinorMode()}, Version: "0.0.0", Want: "0.1.0"}, 38 | {Name: "IncrementPatch", Prefix: "v", Suffix: "", Modes: []Mode{NewPatchMode()}, Version: "0.0.0", Want: "0.0.1"}, 39 | {Name: "DefaultToPatchIfModeSliceEmpty", Prefix: "v", Suffix: "", Modes: []Mode{}, Version: "0.0.0", Want: "0.0.1"}, 40 | {Name: "IncrementWithSecondModeAfterFirstFailed", Prefix: "v", Suffix: "", Modes: []Mode{mockMode, NewMinorMode()}, Version: "0.0.0", Want: "0.1.0"}, 41 | } 42 | 43 | for _, test := range tests { 44 | var mode = NewAutoMode(test.Modes) 45 | var got, err = mode.Increment(test.Prefix, test.Suffix, test.Version) 46 | 47 | assert.NoError(t, err) 48 | assert.IsType(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 49 | } 50 | } 51 | 52 | func TestAutoMode_String(t *testing.T) { 53 | t.Run("ShouldEqualConstant", func(t *testing.T) { 54 | var mode = NewAutoMode([]Mode{}) 55 | var got = mode.String() 56 | var want = Auto 57 | 58 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 59 | }) 60 | } 61 | 62 | func TestNewAutoMode(t *testing.T) { 63 | t.Run("ValidateState", func(t *testing.T) { 64 | var mockMode = mocks.NewMockMode() 65 | var modes = []Mode{mockMode, mockMode, mockMode, mockMode} 66 | var mode = NewAutoMode(modes) 67 | assert.NotNil(t, mode) 68 | assert.NotEmpty(t, mode.Modes) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /internal/fakes/git.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import "fmt" 4 | 5 | // FakeGitAPI a git.API interface fake implementation. 6 | type FakeGitAPI struct { 7 | Config map[string]string 8 | LocalTags []string 9 | PushedTags []string 10 | } 11 | 12 | // NewFakeGitAPI creates a new FakeGitAPI. 13 | // Returns the new FakeGitAPI. 14 | func NewFakeGitAPI() *FakeGitAPI { 15 | return &FakeGitAPI{ 16 | Config: map[string]string{}, 17 | LocalTags: []string{}, 18 | PushedTags: []string{}, 19 | } 20 | } 21 | 22 | // CreateAnnotatedTag creates a fake tag. 23 | func (fake *FakeGitAPI) CreateAnnotatedTag(tag string) (err error) { 24 | fake.LocalTags = append(fake.LocalTags, tag) 25 | return err 26 | } 27 | 28 | // FetchTags does nothing. 29 | func (fake *FakeGitAPI) FetchTags() (output string, err error) { 30 | return output, err 31 | } 32 | 33 | // FetchUnshallow does nothing. 34 | func (fake *FakeGitAPI) FetchUnshallow() (output string, err error) { 35 | return output, err 36 | } 37 | 38 | // GetConfig returns a fake config. 39 | func (fake *FakeGitAPI) GetConfig(key string) (value string, err error) { 40 | var config, exists = fake.Config[key] 41 | 42 | if exists { 43 | return config, nil 44 | } 45 | 46 | return "", fmt.Errorf("config does not exist") 47 | } 48 | 49 | // GetLatestAnnotatedTag returns a fake tag. 50 | func (fake *FakeGitAPI) GetLatestAnnotatedTag() (tag string, err error) { 51 | if len(fake.LocalTags) == 0 { 52 | return tag, fmt.Errorf("no tags found") 53 | } 54 | return fake.LocalTags[len(fake.LocalTags)-1], nil 55 | } 56 | 57 | // GetLatestCommitMessage does nothing. 58 | func (fake *FakeGitAPI) GetLatestCommitMessage() (message string, err error) { 59 | return message, err 60 | } 61 | 62 | // GetMergedBranchName does nothing. 63 | func (fake *FakeGitAPI) GetMergedBranchName() (name string, err error) { 64 | return name, err 65 | } 66 | 67 | // GetTags does nothing 68 | func (fake *FakeGitAPI) GetTags() (tags string, err error) { 69 | return tags, err 70 | } 71 | 72 | // PushTag pushes a fake tag. 73 | func (fake *FakeGitAPI) PushTag(tag string) (err error) { 74 | fake.PushedTags = append(fake.PushedTags, tag) 75 | return err 76 | } 77 | 78 | // SetConfig sets a fake config. 79 | func (fake *FakeGitAPI) SetConfig(key string, value string) (err error) { 80 | fake.Config[key] = value 81 | return err 82 | } 83 | 84 | // SetConfigIfNotSet sets a fake config if it does not exist. 85 | func (fake *FakeGitAPI) SetConfigIfNotSet(key string, value string) (actual string, err error) { 86 | if actual, err = fake.GetConfig(key); err != nil { 87 | err = fake.SetConfig(key, value) 88 | actual = value 89 | } 90 | 91 | return actual, err 92 | } 93 | -------------------------------------------------------------------------------- /internal/mocks/git.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | // MockGitAPI a git.API interface mock implementation. 8 | type MockGitAPI struct { 9 | mock.Mock 10 | } 11 | 12 | // NewMockGitAPI creates a new MockGitAPI. 13 | // Returns the new MockGitAPI. 14 | func NewMockGitAPI() *MockGitAPI { 15 | return &MockGitAPI{} 16 | } 17 | 18 | // CreateAnnotatedTag mocks creating a tag. 19 | // Returns a mocked error. 20 | func (mock *MockGitAPI) CreateAnnotatedTag(tag string) (err error) { 21 | args := mock.Called(tag) 22 | return args.Error(0) 23 | } 24 | 25 | // FetchTags mocks fetching tags. 26 | // Returns a mocked error. 27 | func (mock *MockGitAPI) FetchTags() (output string, err error) { 28 | args := mock.Called() 29 | return args.String(0), args.Error(1) 30 | } 31 | 32 | // FetchUnshallow mocks changing to an unshallow repo. 33 | // Returns a mocked error. 34 | func (mock *MockGitAPI) FetchUnshallow() (output string, err error) { 35 | args := mock.Called() 36 | return args.String(0), args.Error(0) 37 | } 38 | 39 | // GetConfig mocks getting a config. 40 | // Returns a mocked config or a mocked error. 41 | func (mock *MockGitAPI) GetConfig(key string) (value string, err error) { 42 | args := mock.Called(key) 43 | return args.String(0), args.Error(1) 44 | } 45 | 46 | // GetLatestAnnotatedTag mocks getting the latest annotated tag. 47 | // Returns a mocked tag or a mocked error. 48 | func (mock *MockGitAPI) GetLatestAnnotatedTag() (tag string, err error) { 49 | args := mock.Called() 50 | return args.String(0), args.Error(1) 51 | } 52 | 53 | // GetLatestCommitMessage mocks getting the latest commit message. 54 | // Returns a mocked commit message or a mocked error. 55 | func (mock *MockGitAPI) GetLatestCommitMessage() (message string, err error) { 56 | args := mock.Called() 57 | return args.String(0), args.Error(1) 58 | } 59 | 60 | // GetMergedBranchName mocks getting a merged branch name. 61 | // Returns a mocked merged branch name or a mocked error. 62 | func (mock *MockGitAPI) GetMergedBranchName() (name string, err error) { 63 | args := mock.Called() 64 | return args.String(0), args.Error(1) 65 | } 66 | 67 | // GetTags mocks getting all tags. 68 | // Returns a mocked string of tags or a mocked error. 69 | func (mock *MockGitAPI) GetTags() (tags string, err error) { 70 | args := mock.Called() 71 | return args.String(0), args.Error(1) 72 | } 73 | 74 | // PushTag pushes a fake tag. 75 | // Returns a mocked error. 76 | func (mock *MockGitAPI) PushTag(tag string) (err error) { 77 | args := mock.Called(tag) 78 | return args.Error(0) 79 | } 80 | 81 | // SetConfig mocks setting a config. 82 | // Returns a mocked error. 83 | func (mock *MockGitAPI) SetConfig(key string, value string) (err error) { 84 | args := mock.Called(key, value) 85 | return args.Error(0) 86 | } 87 | 88 | // SetConfigIfNotSet mocks setting a config if not set. 89 | // Returns a mocked error. 90 | func (mock *MockGitAPI) SetConfigIfNotSet(key string, value string) (actual string, err error) { 91 | args := mock.Called(key, value) 92 | return args.String(0), args.Error(1) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/git/cli.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | cmder "github.com/restechnica/go-cmder/pkg" 5 | ) 6 | 7 | // CLI a git.API to interact with the git CLI. 8 | type CLI struct { 9 | Commander cmder.Commander 10 | } 11 | 12 | // NewCLI creates a new CLI with a commander to run git commands. 13 | // Returns the new CLI. 14 | func NewCLI() CLI { 15 | return CLI{Commander: cmder.NewExecCommander()} 16 | } 17 | 18 | // CreateAnnotatedTag creates an annotated git tag. 19 | // Returns an error if the command fails. 20 | func (api CLI) CreateAnnotatedTag(tag string) (err error) { 21 | return api.Commander.Run("git", "tag", "-a", tag, "-m", tag) 22 | } 23 | 24 | // FetchTags fetches all tags from the remote origin. 25 | // Returns the output and an error if the command fails. 26 | func (api CLI) FetchTags() (output string, err error) { 27 | return api.Commander.Output("git", "fetch", "--tags", "--verbose") 28 | } 29 | 30 | // FetchUnshallow convert a shallow repository to a complete one. 31 | // Returns an error if the command fails. 32 | func (api CLI) FetchUnshallow() (output string, err error) { 33 | return api.Commander.Output("git", "fetch", "--unshallow") 34 | } 35 | 36 | // GetConfig gets the git config for a specific key. 37 | // Returns the value of the git config as a string and an error if the command failed. 38 | func (api CLI) GetConfig(key string) (value string, err error) { 39 | return api.Commander.Output("git", "config", "--get", key) 40 | } 41 | 42 | // GetLatestAnnotatedTag gets the latest annotated git tag. 43 | // Returns the git tag and an error if the command failed. 44 | func (api CLI) GetLatestAnnotatedTag() (tag string, err error) { 45 | return api.Commander.Output("git", "describe", "--tags") 46 | } 47 | 48 | // GetLatestCommitMessage gets the latest git commit message. 49 | // Returns the git commit message or an error if the command failed. 50 | func (api CLI) GetLatestCommitMessage() (message string, err error) { 51 | return api.Commander.Output("git", "--no-pager", "show", "-s", "--format=%s") 52 | } 53 | 54 | // GetMergedBranchName gets the source branch name if the last commit is a merge. 55 | // Returns the branch name or an error if something went wrong with git. 56 | func (api CLI) GetMergedBranchName() (name string, err error) { 57 | return api.Commander.Output( 58 | "git", 59 | "name-rev", 60 | "--name-only", 61 | "--refs=refs/heads/*", 62 | "--refs=refs/remotes/*", 63 | "HEAD^2", 64 | ) 65 | } 66 | 67 | // GetTags gets all tags, both lightweight and annotated. 68 | // Returns a string of newline separated tags, sorted by version in descending order. 69 | func (api CLI) GetTags() (tags string, err error) { 70 | return api.Commander.Output("git", "tag", "--sort=-version:refname") 71 | } 72 | 73 | // PushTag pushes a tag to the remote origin. 74 | // Returns an error if the command failed. 75 | func (api CLI) PushTag(tag string) (err error) { 76 | return api.Commander.Run("git", "push", "origin", tag) 77 | } 78 | 79 | // SetConfig sets a git config key and value. 80 | // Returns an error if the command failed. 81 | func (api CLI) SetConfig(key string, value string) (err error) { 82 | return api.Commander.Run("git", "config", key, value) 83 | } 84 | 85 | // SetConfigIfNotSet sets a git config key and value if the config does not exist. 86 | // Returns the actual value and an error if the command failed. 87 | func (api CLI) SetConfigIfNotSet(key string, value string) (actual string, err error) { 88 | if actual, err = api.GetConfig(key); err != nil { 89 | err = api.SetConfig(key, value) 90 | actual = value 91 | } 92 | 93 | return actual, err 94 | } 95 | -------------------------------------------------------------------------------- /internal/util/strings_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestContains(t *testing.T) { 10 | type Test struct { 11 | Delimiters string 12 | Name string 13 | TargetString string 14 | Value string 15 | Want bool 16 | } 17 | 18 | var tests = []Test{ 19 | {Name: "EmptyDelimitersReturnsFalse", Delimiters: "", TargetString: "some-value", Value: "value", Want: false}, 20 | {Name: "EmptyTargetStringWithNonEmptyValueReturnsFalse", Delimiters: "-", TargetString: "", Value: "value", Want: false}, 21 | {Name: "EmptyTargetStringWithEmptyValueReturnsFalse", Delimiters: "-", TargetString: "", Value: "", Want: false}, 22 | {Name: "EmptyValueReturnsFalse", Delimiters: "/", TargetString: "some/string", Value: "", Want: false}, 23 | {Name: "IncorrectDelimitersReturnsFalse", Delimiters: "/", TargetString: "some-value", Value: "value", Want: false}, 24 | {Name: "OneDelimiterButWrongLocationReturnsFalse", Delimiters: "/", TargetString: "some/feature [test]", Value: "feature", Want: false}, 25 | {Name: "OneDelimiterButCorrectLocationReturnsTrue", Delimiters: "/", TargetString: "some/feature/ [test]", Value: "feature", Want: true}, 26 | {Name: "MultipleDelimitersReturnsTrue", Delimiters: "/[]", TargetString: "some/feature [test]", Value: "test", Want: true}, 27 | } 28 | 29 | for _, test := range tests { 30 | t.Run(test.Name, func(t *testing.T) { 31 | var got = Contains(test.TargetString, test.Value, test.Delimiters) 32 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 33 | }) 34 | } 35 | } 36 | 37 | func TestSplitByDelimiterString(t *testing.T) { 38 | type Test struct { 39 | Delimiters string 40 | Name string 41 | TargetString string 42 | Want []string 43 | } 44 | 45 | var tests = []Test{ 46 | {Name: "EmptyDelimiters", Delimiters: "", TargetString: "some-string", Want: []string{"some-string"}}, 47 | {Name: "EmptyTargetString", Delimiters: "/", TargetString: "", Want: []string{}}, 48 | {Name: "IncorrectDelimiters", Delimiters: "/", TargetString: "some-string", Want: []string{"some-string"}}, 49 | {Name: "OneDelimiter", Delimiters: "/", TargetString: "some/feature [test]", Want: []string{"some", "feature [test]"}}, 50 | {Name: "MultipleDelimiters", Delimiters: "/[]", TargetString: "some/feature [test]", Want: []string{"some", "feature ", "test"}}, 51 | } 52 | 53 | for _, test := range tests { 54 | t.Run(test.Name, func(t *testing.T) { 55 | var got = SplitByDelimiterString(test.TargetString, test.Delimiters) 56 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 57 | }) 58 | } 59 | } 60 | 61 | func TestSliceContainsString(t *testing.T) { 62 | type Test struct { 63 | Name string 64 | Slice []string 65 | Value string 66 | Want bool 67 | } 68 | 69 | var tests = []Test{ 70 | {Name: "EmptySlice", Slice: []string{}, Value: "test", Want: false}, 71 | {Name: "EmptyValue", Slice: []string{"test"}, Value: "", Want: false}, 72 | {Name: "OneSliceElementWithValue", Slice: []string{"test"}, Value: "test", Want: true}, 73 | {Name: "MultipleSliceElementsWithValue", Slice: []string{"one", "test", "two"}, Value: "test", Want: true}, 74 | {Name: "OneSliceElementWithoutValue", Slice: []string{"without"}, Value: "test", Want: false}, 75 | {Name: "MultipleElementWithoutValue", Slice: []string{"without", "with", "out"}, Value: "test", Want: false}, 76 | } 77 | 78 | for _, test := range tests { 79 | t.Run(test.Name, func(t *testing.T) { 80 | var got = SliceContainsString(test.Slice, test.Value) 81 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/modes/gitcommit_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/restechnica/semverbot/internal/mocks" 10 | "github.com/restechnica/semverbot/pkg/semver" 11 | ) 12 | 13 | func TestGitCommitMode_GitCommitConstant(t *testing.T) { 14 | t.Run("CheckConstant", func(t *testing.T) { 15 | var want = "git-commit" 16 | var got = GitCommit 17 | 18 | assert.Equal(t, want, got, `want: '%s', got: '%s'`, want, got) 19 | }) 20 | } 21 | 22 | func TestGitCommitMode_Increment(t *testing.T) { 23 | var semverMap = semver.Map{ 24 | Patch: {"fix", "bug"}, 25 | Minor: {"feature"}, 26 | Major: {"release"}, 27 | } 28 | 29 | type Test struct { 30 | CommitMessage string 31 | Delimiters string 32 | Name string 33 | Prefix string 34 | Suffix string 35 | SemverMap semver.Map 36 | Version string 37 | Want string 38 | } 39 | 40 | var tests = []Test{ 41 | {Name: "IncrementPatch", CommitMessage: "fix] some-bug", Delimiters: "[]", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.0.0", Want: "0.0.1"}, 42 | {Name: "IncrementPatch", CommitMessage: "[fi] some/bug", Delimiters: "/", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.0.0", Want: "0.0.1"}, 43 | {Name: "IncrementMinor", CommitMessage: "[feature] some-feat", Delimiters: "[]", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.0.1", Want: "0.1.0"}, 44 | {Name: "IncrementMajor", CommitMessage: "[release] some-release", Delimiters: "[]", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.1.0", Want: "1.0.0"}, 45 | } 46 | 47 | for _, test := range tests { 48 | t.Run(test.Name, func(t *testing.T) { 49 | var gitAPI = mocks.NewMockGitAPI() 50 | gitAPI.On("GetLatestCommitMessage").Return(test.CommitMessage, nil) 51 | 52 | var mode = NewGitCommitMode(test.Delimiters, test.SemverMap) 53 | mode.GitAPI = gitAPI 54 | 55 | var got, err = mode.Increment(test.Prefix, test.Suffix, test.Version) 56 | 57 | assert.NoError(t, err) 58 | assert.IsType(t, test.Want, got, `want: '%s, got: '%s'`, test.Want, got) 59 | }) 60 | } 61 | 62 | t.Run("ReturnErrorOnGitAPIError", func(t *testing.T) { 63 | var want = fmt.Errorf("some-error") 64 | 65 | var gitAPI = mocks.NewMockGitAPI() 66 | gitAPI.On("GetLatestCommitMessage").Return("", want) 67 | 68 | var mode = NewGitCommitMode("[]", semverMap) 69 | mode.GitAPI = gitAPI 70 | 71 | var _, got = mode.Increment("v", "", "0.0.0") 72 | 73 | assert.Error(t, got) 74 | assert.Equal(t, want, got, `want: '%s, got: '%s'`, want, got) 75 | }) 76 | 77 | t.Run("ReturnErrorIfNoMatchingMode", func(t *testing.T) { 78 | var gitAPI = mocks.NewMockGitAPI() 79 | gitAPI.On("GetLatestCommitMessage").Return("nomatch/some-feature", nil) 80 | 81 | var mode = NewGitCommitMode("/", semverMap) 82 | mode.GitAPI = gitAPI 83 | 84 | var _, got = mode.Increment("v", "", "0.0.0") 85 | 86 | assert.Error(t, got) 87 | }) 88 | 89 | t.Run("ReturnErrorIfInvalidVersion", func(t *testing.T) { 90 | var gitAPI = mocks.NewMockGitAPI() 91 | gitAPI.On("GetLatestCommitMessage").Return("[feature]some-feature", nil) 92 | 93 | var mode = NewGitCommitMode("[]", semverMap) 94 | mode.GitAPI = gitAPI 95 | 96 | var _, got = mode.Increment("v", "", "invalid") 97 | 98 | assert.Error(t, got) 99 | }) 100 | } 101 | 102 | func TestGitCommitMode_String(t *testing.T) { 103 | t.Run("ShouldEqualConstant", func(t *testing.T) { 104 | var mode = NewGitCommitMode("", semver.Map{}) 105 | var got = mode.String() 106 | var want = GitCommit 107 | 108 | assert.Equal(t, want, got, `want: '%s, got: '%s'`, want, got) 109 | }) 110 | } 111 | 112 | func TestNewGitCommitMode(t *testing.T) { 113 | t.Run("ValidateState", func(t *testing.T) { 114 | var delimiters = "[]" 115 | var semverMap = semver.Map{} 116 | var mode = NewGitCommitMode(delimiters, semverMap) 117 | 118 | assert.NotNil(t, mode) 119 | assert.NotEmpty(t, mode.Delimiters) 120 | assert.NotNil(t, mode.SemverMap) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/versions/api.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/rs/zerolog/log" 7 | 8 | "github.com/restechnica/semverbot/pkg/git" 9 | "github.com/restechnica/semverbot/pkg/modes" 10 | "github.com/restechnica/semverbot/pkg/semver" 11 | ) 12 | 13 | // API an API to work with versions. 14 | type API struct { 15 | Prefix string 16 | Suffix string 17 | GitAPI git.API 18 | } 19 | 20 | // NewAPI creates a new version API. 21 | // Returns the new API. 22 | func NewAPI(prefix string, suffix string) API { 23 | return API{Prefix: prefix, Suffix: suffix, GitAPI: git.NewCLI()} 24 | } 25 | 26 | // GetVersion gets the latest valid semver version from the git tags. 27 | // The tag is trimmed because git adds newlines to the underlying command. 28 | // Returns the current version or an error if the GitAPI failed. 29 | func (api API) GetVersion() (currentVersion string, err error) { 30 | var tags string 31 | 32 | if tags, err = api.GitAPI.GetTags(); err != nil { 33 | return currentVersion, err 34 | } 35 | 36 | // strip all newlines 37 | var versions = strings.Fields(tags) 38 | 39 | if currentVersion, err = semver.Find(api.Prefix, api.Suffix, versions); err != nil { 40 | return currentVersion, err 41 | } 42 | 43 | return semver.Trim(api.Prefix, api.Suffix, currentVersion) 44 | } 45 | 46 | // GetVersionOrDefault gets the current version or a default version if it failed. 47 | // Returns the current version or a default version. 48 | func (api API) GetVersionOrDefault(defaultVersion string) (version string) { 49 | var err error 50 | 51 | log.Info().Msg("getting version...") 52 | 53 | if version, err = api.GetVersion(); err != nil { 54 | log.Debug().Err(err).Msg("") 55 | log.Warn().Msg("falling back to default version") 56 | version = defaultVersion 57 | } 58 | 59 | log.Info().Msg(version) 60 | 61 | return version 62 | } 63 | 64 | // PredictVersion increments a version based on a modes.Mode. 65 | // Returns the next version or an error if the increment failed. 66 | func (api API) PredictVersion(version string, mode modes.Mode) (string, error) { 67 | var err error 68 | 69 | log.Info().Msg("predicting version...") 70 | 71 | version, err = mode.Increment(api.Prefix, api.Suffix, version) 72 | 73 | log.Info().Msg(version) 74 | 75 | return version, err 76 | } 77 | 78 | // ReleaseVersion releases a version by creating an annotated git tag with a prefix. 79 | // Returns an error if the tag creation failed. 80 | func (api API) ReleaseVersion(version string) (err error) { 81 | log.Info().Msg("releasing version...") 82 | var prefixedVersion = AddPrefix(version, api.Prefix) 83 | var prefixedAndSuffixedVersion = AddSuffix(prefixedVersion, api.Suffix) 84 | return api.GitAPI.CreateAnnotatedTag(prefixedAndSuffixedVersion) 85 | } 86 | 87 | // PushVersion pushes a version by pushing a git tag with a prefix. 88 | // Returns an error if pushing the tag failed. 89 | func (api API) PushVersion(version string) (err error) { 90 | log.Info().Msg("pushing version...") 91 | var prefixedVersion = AddPrefix(version, api.Prefix) 92 | var prefixedAndSuffixedVersion = AddSuffix(prefixedVersion, api.Suffix) 93 | return api.GitAPI.PushTag(prefixedAndSuffixedVersion) 94 | } 95 | 96 | // UpdateVersion updates the version by making the git repo unshallow and by fetching all git tags. 97 | // Returns and error if anything went wrong. Errors from making the git repo unshallow are ignored. 98 | func (api API) UpdateVersion() (err error) { 99 | log.Info().Msg("updating version...") 100 | 101 | var output string 102 | 103 | log.Info().Msg("fetching unshallow repository...") 104 | 105 | if output, err = api.GitAPI.FetchUnshallow(); err != nil { 106 | log.Debug().Err(err).Msg("") 107 | log.Warn().Msg("ignoring failed unshallow fetch for now, repository might already be complete") 108 | } else { 109 | log.Debug().Msg(strings.Trim(output, "\n")) 110 | } 111 | 112 | log.Info().Msg("fetching tags...") 113 | 114 | if output, err = api.GitAPI.FetchTags(); err == nil { 115 | log.Debug().Msg(strings.Trim(output, "\n")) 116 | } 117 | 118 | return err 119 | } 120 | -------------------------------------------------------------------------------- /pkg/modes/gitbranch_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/restechnica/semverbot/internal/mocks" 10 | "github.com/restechnica/semverbot/pkg/semver" 11 | ) 12 | 13 | func TestGitBranchMode_GitBranchConstant(t *testing.T) { 14 | t.Run("CheckConstant", func(t *testing.T) { 15 | var want = "git-branch" 16 | var got = GitBranch 17 | 18 | assert.Equal(t, want, got, `want: '%s', got: '%s'`, want, got) 19 | }) 20 | } 21 | 22 | func TestGitBranchMode_Increment(t *testing.T) { 23 | var semverMap = semver.Map{ 24 | Patch: {"fix", "bug"}, 25 | Minor: {"feature"}, 26 | Major: {"release"}, 27 | } 28 | 29 | type Test struct { 30 | BranchName string 31 | Delimiters string 32 | Name string 33 | Prefix string 34 | Suffix string 35 | SemverMap semver.Map 36 | Version string 37 | Want string 38 | } 39 | 40 | var tests = []Test{ 41 | {Name: "IncrementPatch", BranchName: "fix/some-bug", Delimiters: "/", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.0.0", Want: "0.0.1"}, 42 | {Name: "IncrementMinor", BranchName: "feature/some-bug", Delimiters: "/", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.0.1", Want: "0.1.0"}, 43 | {Name: "IncrementMajor", BranchName: "release/some-bug", Delimiters: "/", Prefix: "v", Suffix: "", SemverMap: semverMap, Version: "0.1.0", Want: "1.0.0"}, 44 | } 45 | 46 | for _, test := range tests { 47 | t.Run(test.Name, func(t *testing.T) { 48 | var gitAPI = mocks.NewMockGitAPI() 49 | gitAPI.On("GetMergedBranchName").Return(test.BranchName, nil) 50 | 51 | var mode = NewGitBranchMode(test.Delimiters, test.SemverMap) 52 | mode.GitAPI = gitAPI 53 | 54 | var got, err = mode.Increment(test.Prefix, test.Suffix, test.Version) 55 | 56 | assert.NoError(t, err) 57 | assert.IsType(t, test.Want, got, `want: '%s, got: '%s'`, test.Want, got) 58 | }) 59 | } 60 | 61 | t.Run("ReturnErrorOnGitAPIError", func(t *testing.T) { 62 | var want = fmt.Errorf("some-error") 63 | 64 | var gitAPI = mocks.NewMockGitAPI() 65 | gitAPI.On("GetMergedBranchName").Return("", want) 66 | 67 | var mode = NewGitBranchMode("/", semverMap) 68 | mode.GitAPI = gitAPI 69 | 70 | var _, got = mode.Increment("v", "", "0.0.0") 71 | 72 | assert.Error(t, got) 73 | assert.Equal(t, want, got, `want: '%s, got: '%s'`, want, got) 74 | }) 75 | 76 | t.Run("ReturnErrorIfNoMergeCommit", func(t *testing.T) { 77 | var want = fmt.Errorf("failed to increment version because the latest git commit is not a merge commit") 78 | 79 | var gitAPI = mocks.NewMockGitAPI() 80 | gitAPI.On("GetMergedBranchName").Return("", nil) 81 | 82 | var mode = NewGitBranchMode("/", semverMap) 83 | mode.GitAPI = gitAPI 84 | 85 | var _, got = mode.Increment("v", "", "0.0.0") 86 | 87 | assert.Error(t, got) 88 | assert.Equal(t, want, got, `want: '%s, got: '%s'`, want, got) 89 | }) 90 | 91 | t.Run("ReturnErrorIfNoMatchingMode", func(t *testing.T) { 92 | var gitAPI = mocks.NewMockGitAPI() 93 | gitAPI.On("GetMergedBranchName").Return("feat/some-feature", nil) 94 | 95 | var mode = NewGitBranchMode("/", semverMap) 96 | mode.GitAPI = gitAPI 97 | 98 | var _, got = mode.Increment("v", "", "0.0.0") 99 | 100 | assert.Error(t, got) 101 | }) 102 | 103 | t.Run("ReturnErrorIfInvalidVersion", func(t *testing.T) { 104 | var gitAPI = mocks.NewMockGitAPI() 105 | gitAPI.On("GetMergedBranchName").Return("feature/some-feat", nil) 106 | 107 | var mode = NewGitBranchMode("/", semverMap) 108 | mode.GitAPI = gitAPI 109 | 110 | var _, got = mode.Increment("v", "", "invalid") 111 | 112 | assert.Error(t, got) 113 | }) 114 | } 115 | 116 | func TestGitBranchMode_String(t *testing.T) { 117 | t.Run("ShouldEqualConstant", func(t *testing.T) { 118 | var mode = NewGitBranchMode("", semver.Map{}) 119 | var got = mode.String() 120 | var want = GitBranch 121 | 122 | assert.Equal(t, want, got, `want: '%s, got: '%s'`, want, got) 123 | }) 124 | } 125 | 126 | func TestNewGitBranchMode(t *testing.T) { 127 | t.Run("ValidateState", func(t *testing.T) { 128 | var delimiters = "/" 129 | var semverMap = semver.Map{} 130 | var mode = NewGitBranchMode(delimiters, semverMap) 131 | 132 | assert.NotNil(t, mode) 133 | assert.NotEmpty(t, mode.Delimiters) 134 | assert.NotNil(t, mode.SemverMap) 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /pkg/cli/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/restechnica/semverbot/pkg/cli" 14 | v1 "github.com/restechnica/semverbot/pkg/cli/commands/v1" 15 | "github.com/restechnica/semverbot/pkg/ext/viperx" 16 | "github.com/restechnica/semverbot/pkg/git" 17 | "github.com/restechnica/semverbot/pkg/semver" 18 | ) 19 | 20 | func init() { 21 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}) 22 | } 23 | 24 | // NewRootCommand creates a new root command. 25 | // Returns the new spf13/cobra command. 26 | func NewRootCommand() *cobra.Command { 27 | var command = &cobra.Command{ 28 | Use: "sbot", 29 | PersistentPreRunE: RootCommandPersistentPreRunE, 30 | } 31 | 32 | command.PersistentFlags().StringVarP(&cli.ConfigFlag, "config", "c", cli.DefaultConfigFilePath, "configures which config file to use") 33 | 34 | command.PersistentFlags().BoolVarP(&cli.VerboseFlag, "verbose", "v", false, "increase log level verbosity to Info") 35 | command.PersistentFlags().BoolVarP(&cli.DebugFlag, "debug", "d", false, "increase log level verbosity to Debug") 36 | 37 | command.AddCommand(v1.NewV1Command()) 38 | command.AddCommand(v1.NewGetCommand()) 39 | command.AddCommand(v1.NewInitCommand()) 40 | command.AddCommand(v1.NewPredictCommand()) 41 | command.AddCommand(v1.NewPushCommand()) 42 | command.AddCommand(v1.NewReleaseCommand()) 43 | command.AddCommand(v1.NewUpdateCommand()) 44 | command.AddCommand(v1.NewVersionCommand()) 45 | 46 | return command 47 | } 48 | 49 | // RootCommandPersistentPreRunE runs before the command and any subcommand runs. 50 | // Returns an error if it failed. 51 | func RootCommandPersistentPreRunE(cmd *cobra.Command, args []string) (err error) { 52 | // silence usage output on error because errors at this point are unrelated to CLI usage 53 | cmd.SilenceUsage = true 54 | 55 | ConfigureLogging() 56 | 57 | log.Debug().Str("command", "root").Msg("starting pre-run...") 58 | 59 | log.Debug().Msg("loading default config values...") 60 | 61 | LoadEnvironmentVariablesConfig() 62 | 63 | LoadDefaultConfigValues() 64 | 65 | if err = LoadConfigFile(cmd); err != nil { 66 | return err 67 | } 68 | 69 | if err = LoadFlagsIntoConfig(cmd); err != nil { 70 | return err 71 | } 72 | 73 | log.Debug().Msg("configuring git...") 74 | 75 | if err = SetGitConfigIfConfigured(); err != nil { 76 | return err 77 | } 78 | 79 | // silence errors which at this point are unrelated to CLI (cobra/viper) errors 80 | cmd.SilenceErrors = false 81 | 82 | return err 83 | } 84 | 85 | func ConfigureLogging() { 86 | SetLogLevel() 87 | } 88 | 89 | func SetLogLevel() { 90 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 91 | 92 | if cli.VerboseFlag { 93 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 94 | } 95 | 96 | if cli.DebugFlag { 97 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 98 | } 99 | } 100 | 101 | // LoadEnvironmentVariablesConfig 102 | func LoadEnvironmentVariablesConfig() { 103 | viper.SetEnvPrefix("SBOT") 104 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 105 | } 106 | 107 | // LoadConfigFile loads the SemverBot configuration file. 108 | // If the config flag was used, it will try to load only that path. 109 | // If the config flag was not used, multiple default config file paths will be tried. 110 | // Returns no error if config files are not found, returns an error if it fails otherwise. 111 | func LoadConfigFile(cmd *cobra.Command) (err error) { 112 | configFlag := cmd.Flag("config") 113 | 114 | if configFlag.Changed { 115 | if err = viperx.LoadConfig(cli.ConfigFlag); err != nil { 116 | if errors.As(err, &viper.ConfigFileNotFoundError{}) { 117 | log.Warn().Msgf("config file %s not found", cli.ConfigFlag) 118 | return nil 119 | } 120 | } 121 | 122 | return err 123 | } 124 | 125 | paths := append([]string{cli.DefaultConfigFilePath}, cli.DefaultAdditionalConfigFilePaths...) 126 | 127 | for _, path := range paths { 128 | if err = viperx.LoadConfig(path); err == nil { 129 | return err 130 | } 131 | 132 | if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 133 | return err 134 | } 135 | 136 | err = nil 137 | log.Warn().Msgf("config file %s not found", path) 138 | } 139 | 140 | return err 141 | } 142 | 143 | // LoadDefaultConfigValues loads the default SemverBot config. 144 | func LoadDefaultConfigValues() { 145 | viper.SetDefault(cli.GitTagsPrefixConfigKey, cli.DefaultGitTagsPrefix) 146 | viper.SetDefault(cli.GitTagsSuffixConfigKey, cli.DefaultGitTagsSuffix) 147 | viper.SetDefault(cli.ModeConfigKey, cli.DefaultMode) 148 | viper.SetDefault(cli.ModesGitBranchDelimitersConfigKey, cli.DefaultGitBranchDelimiters) 149 | viper.SetDefault(cli.ModesGitCommitDelimitersConfigKey, cli.DefaultGitCommitDelimiters) 150 | viper.SetDefault(cli.SemverMapConfigKey, semver.Map{}) 151 | } 152 | 153 | // LoadFlagsIntoConfig loads root command flags. 154 | // Returns an error if it fails. 155 | func LoadFlagsIntoConfig(cmd *cobra.Command) (err error) { 156 | //err = viper.BindPFlag("git.tags.prefix", cmd.Flags().Lookup("prefix")) 157 | return err 158 | } 159 | 160 | // SetGitConfigIfConfigured Sets the git config only when the SemverBot config exists and the git config does not exist. 161 | // Returns an error if it fails. 162 | func SetGitConfigIfConfigured() (err error) { 163 | var gitAPI = git.NewCLI() 164 | var value string 165 | 166 | if viper.IsSet(cli.GitConfigEmailConfigKey) { 167 | var email = viper.GetString(cli.GitConfigEmailConfigKey) 168 | value = email 169 | 170 | if value, err = gitAPI.SetConfigIfNotSet("user.email", email); err != nil { 171 | return err 172 | } 173 | } 174 | 175 | log.Debug().Str("user.email", strings.Trim(value, "\n")).Msg("") 176 | 177 | if viper.IsSet(cli.GitConfigNameConfigKey) { 178 | var name = viper.GetString(cli.GitConfigNameConfigKey) 179 | value = name 180 | 181 | if value, err = gitAPI.SetConfigIfNotSet("user.name", name); err != nil { 182 | return err 183 | } 184 | } 185 | 186 | log.Debug().Str("user.name", strings.Trim(value, "\n")).Msg("") 187 | 188 | return err 189 | } 190 | -------------------------------------------------------------------------------- /pkg/git/cli_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | 10 | "github.com/restechnica/semverbot/internal/mocks" 11 | ) 12 | 13 | func TestCLI_CreateAnnotatedTag(t *testing.T) { 14 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 15 | var want = fmt.Errorf("some-error") 16 | 17 | var cmder = mocks.NewMockCommander() 18 | cmder.On("Run", mock.Anything, mock.Anything).Return(want) 19 | 20 | var gitCLI = CLI{Commander: cmder} 21 | var got = gitCLI.CreateAnnotatedTag("0.0.0") 22 | 23 | assert.Error(t, got) 24 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 25 | }) 26 | } 27 | 28 | func TestCLI_FetchTags(t *testing.T) { 29 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 30 | var want = fmt.Errorf("some-error") 31 | 32 | var cmder = mocks.NewMockCommander() 33 | cmder.On("Output", mock.Anything, mock.Anything).Return("", want) 34 | 35 | var gitCLI = CLI{Commander: cmder} 36 | var _, got = gitCLI.FetchTags() 37 | 38 | assert.Error(t, got) 39 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 40 | }) 41 | } 42 | 43 | func TestCLI_FetchUnshallow(t *testing.T) { 44 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 45 | var want = fmt.Errorf("some-error") 46 | 47 | var cmder = mocks.NewMockCommander() 48 | cmder.On("Output", mock.Anything, mock.Anything).Return("", want) 49 | 50 | var gitCLI = CLI{Commander: cmder} 51 | var _, got = gitCLI.FetchUnshallow() 52 | 53 | assert.Error(t, got) 54 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 55 | }) 56 | } 57 | 58 | func TestCLI_GetConfig(t *testing.T) { 59 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 60 | var want = fmt.Errorf("some-error") 61 | 62 | var cmder = mocks.NewMockCommander() 63 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", want) 64 | 65 | var gitCLI = CLI{Commander: cmder} 66 | var _, got = gitCLI.GetConfig("key") 67 | 68 | assert.Error(t, got) 69 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 70 | }) 71 | } 72 | 73 | func TestCLI_GetLatestAnnotatedTag(t *testing.T) { 74 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 75 | var want = fmt.Errorf("some-error") 76 | 77 | var cmder = mocks.NewMockCommander() 78 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", want) 79 | 80 | var gitCLI = CLI{Commander: cmder} 81 | var _, got = gitCLI.GetLatestAnnotatedTag() 82 | 83 | assert.Error(t, got) 84 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 85 | }) 86 | } 87 | 88 | func TestCLI_GetLatestCommitMessage(t *testing.T) { 89 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 90 | var want = fmt.Errorf("some-error") 91 | 92 | var cmder = mocks.NewMockCommander() 93 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", want) 94 | 95 | var gitCLI = CLI{Commander: cmder} 96 | var _, got = gitCLI.GetLatestCommitMessage() 97 | 98 | assert.Error(t, got) 99 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 100 | }) 101 | } 102 | 103 | func TestCLI_GetMergedBranchName(t *testing.T) { 104 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 105 | var want = fmt.Errorf("some-error") 106 | 107 | var cmder = mocks.NewMockCommander() 108 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", want) 109 | 110 | var gitCLI = CLI{Commander: cmder} 111 | var _, got = gitCLI.GetMergedBranchName() 112 | 113 | assert.Error(t, got) 114 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 115 | }) 116 | } 117 | 118 | func TestCLI_GetTags(t *testing.T) { 119 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 120 | var want = fmt.Errorf("some-error") 121 | 122 | var cmder = mocks.NewMockCommander() 123 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", want) 124 | 125 | var gitCLI = CLI{Commander: cmder} 126 | var _, got = gitCLI.GetTags() 127 | 128 | assert.Error(t, got) 129 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 130 | }) 131 | } 132 | 133 | func TestCLI_PushTag(t *testing.T) { 134 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 135 | var want = fmt.Errorf("some-error") 136 | 137 | var cmder = mocks.NewMockCommander() 138 | cmder.On("Run", mock.Anything, mock.Anything).Return(want) 139 | 140 | var gitCLI = CLI{Commander: cmder} 141 | var got = gitCLI.PushTag("tag") 142 | 143 | assert.Error(t, got) 144 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 145 | }) 146 | } 147 | 148 | func TestCLI_SetConfig(t *testing.T) { 149 | t.Run("ReturnErrorOnCommanderError", func(t *testing.T) { 150 | var want = fmt.Errorf("some-error") 151 | 152 | var cmder = mocks.NewMockCommander() 153 | cmder.On("Run", mock.Anything, mock.Anything).Return(want) 154 | 155 | var gitCLI = CLI{Commander: cmder} 156 | var got = gitCLI.SetConfig("key", "value") 157 | 158 | assert.Error(t, got) 159 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 160 | }) 161 | } 162 | 163 | func TestCLI_SetConfigIfNotSet(t *testing.T) { 164 | t.Run("DoNotSetConfigIfConfigExists", func(t *testing.T) { 165 | var want = "initial" 166 | 167 | var cmder = mocks.NewMockCommander() 168 | cmder.On("Output", mock.Anything, mock.Anything).Return(want, nil) 169 | 170 | var gitCLI = CLI{Commander: cmder} 171 | var got, err = gitCLI.SetConfigIfNotSet("key", "value") 172 | 173 | cmder.AssertCalled(t, "Output", mock.Anything, mock.Anything) 174 | cmder.AssertNotCalled(t, "Run", mock.Anything, mock.Anything) 175 | 176 | assert.NoError(t, err) 177 | assert.Equal(t, nil, err, `want: "%s, got: "%s"`, nil, err) 178 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 179 | }) 180 | 181 | t.Run("SetConfigIfConfigDoesNotExist", func(t *testing.T) { 182 | var initial = "initial" 183 | var want = "value" 184 | 185 | var cmder = mocks.NewMockCommander() 186 | cmder.On("Output", mock.Anything, mock.Anything).Return(initial, fmt.Errorf("some-error")) 187 | cmder.On("Run", mock.Anything, mock.Anything).Return(nil) 188 | 189 | var gitCLI = CLI{Commander: cmder} 190 | var got, err = gitCLI.SetConfigIfNotSet("key", want) 191 | 192 | cmder.AssertCalled(t, "Output", mock.Anything, mock.Anything) 193 | cmder.AssertCalled(t, "Run", mock.Anything, mock.Anything) 194 | 195 | assert.NoError(t, err) 196 | assert.Equal(t, nil, err, `want: "%s, got: "%s"`, nil, err) 197 | assert.Equal(t, nil, err, `want: "%s, got: "%s"`, nil, err) 198 | 199 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 200 | }) 201 | 202 | t.Run("ReturnErrorOnSetConfigError", func(t *testing.T) { 203 | var want = fmt.Errorf("some-error") 204 | 205 | var cmder = mocks.NewMockCommander() 206 | cmder.On("Output", mock.Anything, mock.Anything).Return("value", fmt.Errorf("some-error")) 207 | cmder.On("Run", mock.Anything, mock.Anything).Return(fmt.Errorf("some-error")) 208 | 209 | var gitCLI = CLI{Commander: cmder} 210 | var _, got = gitCLI.SetConfigIfNotSet("key", "value") 211 | 212 | assert.Error(t, got) 213 | assert.Equal(t, want, got, `want: "%s, got: "%s"`, want, got) 214 | }) 215 | } 216 | 217 | func TestNewCLI(t *testing.T) { 218 | t.Run("ValidateState", func(t *testing.T) { 219 | var cli = NewCLI() 220 | assert.NotNil(t, cli) 221 | assert.NotNil(t, cli.Commander) 222 | }) 223 | } 224 | -------------------------------------------------------------------------------- /pkg/modes/detect_test.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/restechnica/semverbot/pkg/semver" 10 | ) 11 | 12 | func TestDetectModeFromString(t *testing.T) { 13 | var semverMap = semver.Map{ 14 | Patch: {"fix", "bug", "patch"}, 15 | Minor: {"feature", "feat", "minor"}, 16 | Major: {"release", "major"}, 17 | } 18 | 19 | type Test struct { 20 | String string 21 | Delimiters string 22 | Name string 23 | SemverMap semver.Map 24 | Want Mode 25 | } 26 | 27 | var tests = []Test{ 28 | {Name: "DetectPatchModeWithSlash", String: "fix/some-bug", Delimiters: "/", SemverMap: semverMap, Want: NewPatchMode()}, 29 | {Name: "DetectPatchModeWithSquareBrackets", String: "[bug] some fix", Delimiters: "[]", SemverMap: semverMap, Want: NewPatchMode()}, 30 | {Name: "DetectMinorModeWithSlash", String: "feature/some-bug", Delimiters: "/", SemverMap: semverMap, Want: NewMinorMode()}, 31 | {Name: "DetectMinorModeWithRoundBrackets", String: "feat(subject): some changes", Delimiters: "():", SemverMap: semverMap, Want: NewMinorMode()}, 32 | {Name: "DetectMinorModeWithSquareBrackets", String: "[feature] some changes", Delimiters: "[]", SemverMap: semverMap, Want: NewMinorMode()}, 33 | {Name: "DetectMajorModeWithSlash", String: "release/some-bug", Delimiters: "/", SemverMap: semverMap, Want: NewMajorMode()}, 34 | {Name: "DetectMinorWithMultipleModes", String: "some [fix] and release/feat(subject)", Delimiters: "()[]/", SemverMap: semverMap, Want: NewMinorMode()}, 35 | {Name: "DetectMajorWithMultipleModes0", String: "[fix] some [feature] and [release]", Delimiters: "[]", SemverMap: semverMap, Want: NewMajorMode()}, 36 | {Name: "DetectMajorWithMultipleModes1", String: "[release] some [fix] test [feature]", Delimiters: "[]", SemverMap: semverMap, Want: NewMajorMode()}, 37 | {Name: "DetectMajorWithMultipleModes2", String: "[feature] some [release] test [fix]", Delimiters: "[]", SemverMap: semverMap, Want: NewMajorMode()}, 38 | } 39 | 40 | for _, test := range tests { 41 | t.Run(test.Name, func(t *testing.T) { 42 | var got, err = DetectModeFromString(test.String, test.SemverMap, test.Delimiters) 43 | 44 | assert.NoError(t, err) 45 | assert.IsType(t, test.Want, got, `want: '%s, got: '%s'`, test.Want, got) 46 | }) 47 | } 48 | 49 | type ErrorTest struct { 50 | String string 51 | Delimiters string 52 | Error error 53 | Name string 54 | SemverMap semver.Map 55 | } 56 | 57 | var errorTests = []ErrorTest{ 58 | { 59 | Name: "DetectNothingWithEmptySemverMap", 60 | String: "[feature] some changes", 61 | Delimiters: "[]", 62 | Error: fmt.Errorf(`failed to detect mode from string '[feature] some changes' with delimiters '[]'`), 63 | SemverMap: semver.Map{}, 64 | }, 65 | { 66 | Name: "DetectNothingWithEmptyDelimiters", 67 | String: "[feature] some changes", 68 | Delimiters: "", 69 | Error: fmt.Errorf(`failed to detect mode from string '[feature] some changes' with delimiters ''`), 70 | SemverMap: semverMap, 71 | }, 72 | { 73 | Name: "DetectNothingWithEmptyCommitMessage", 74 | String: "", 75 | Delimiters: "/", 76 | Error: fmt.Errorf(`failed to detect mode from string '' with delimiters '/'`), 77 | SemverMap: semverMap, 78 | }, 79 | { 80 | Name: "DetectNothingWithFaultySemverMap", 81 | String: "[feature] some changes", 82 | Delimiters: "[]", 83 | Error: fmt.Errorf(`failed to detect mode due to unsupported semver level: 'mnr'`), 84 | SemverMap: semver.Map{ 85 | "mnr": {"feature"}, 86 | }, 87 | }, 88 | } 89 | 90 | for _, test := range errorTests { 91 | t.Run(test.Name, func(t *testing.T) { 92 | var _, got = DetectModeFromString(test.String, test.SemverMap, test.Delimiters) 93 | 94 | assert.Error(t, got) 95 | assert.Equal(t, test.Error, got, `want: '%s', got: '%s'`, test.Error, got) 96 | }) 97 | } 98 | } 99 | 100 | func TestDetectModesFromString(t *testing.T) { 101 | var semverMap = semver.Map{ 102 | Patch: {"fix", "bug", "patch"}, 103 | Minor: {"feature", "feat", "minor"}, 104 | Major: {"release", "major"}, 105 | } 106 | 107 | type Test struct { 108 | String string 109 | Delimiters string 110 | Name string 111 | SemverMap semver.Map 112 | Want []Mode 113 | } 114 | 115 | var tests = []Test{ 116 | {Name: "DetectPatchModeWithSlash", String: "fix/some-bug", Delimiters: "/", SemverMap: semverMap, Want: []Mode{NewPatchMode()}}, 117 | {Name: "DetectPatchModeWithSquareBrackets", String: "[bug] some fix", Delimiters: "[]", SemverMap: semverMap, Want: []Mode{NewPatchMode()}}, 118 | {Name: "DetectMinorModeWithSlash", String: "feature/some-bug", Delimiters: "/", SemverMap: semverMap, Want: []Mode{NewMinorMode()}}, 119 | {Name: "DetectMinorModeWithRoundBrackets", String: "feat(subject): some changes", Delimiters: "():", SemverMap: semverMap, Want: []Mode{NewMinorMode()}}, 120 | {Name: "DetectMinorModeWithSquareBrackets", String: "[feature] some changes", Delimiters: "[]", SemverMap: semverMap, Want: []Mode{NewMinorMode()}}, 121 | {Name: "DetectMajorModeWithSlash", String: "release/some-bug", Delimiters: "/", SemverMap: semverMap, Want: []Mode{NewMajorMode()}}, 122 | {Name: "DetectMultipleModes0", String: "[fix] some [feature] and [release]", Delimiters: "[]", SemverMap: semverMap, Want: []Mode{NewPatchMode(), NewMinorMode(), NewMajorMode()}}, 123 | {Name: "DetectMultipleModes1", String: "some [feature] and release/test and fix(subject)", Delimiters: "()[]/", SemverMap: semverMap, Want: []Mode{NewMinorMode()}}, 124 | {Name: "DetectMultipleModes2", String: "some [fix] and release/test and feat(subject)", Delimiters: "()[]/", SemverMap: semverMap, Want: []Mode{NewPatchMode()}}, 125 | {Name: "DetectMultipleModes3", String: "some [fix] and release/feat(subject)", Delimiters: "()[]/", SemverMap: semverMap, Want: []Mode{NewPatchMode(), NewMinorMode()}}, 126 | {Name: "DetectMultipleModesInOrder0", String: "[release] some [fix] test [feature]", Delimiters: "[]", SemverMap: semverMap, Want: []Mode{NewMajorMode(), NewPatchMode(), NewMinorMode()}}, 127 | {Name: "DetectMultipleModesInOrder1", String: "[feature] some [release] test [fix]", Delimiters: "[]", SemverMap: semverMap, Want: []Mode{NewMinorMode(), NewMajorMode(), NewPatchMode()}}, 128 | {Name: "DetectNothingWithEmptySemverMap", String: "feature/some-feature", Delimiters: "/", SemverMap: semver.Map{}, Want: []Mode{}}, 129 | {Name: "DetectNothingWithEmptyDelimiters", String: "feature/some-feature", Delimiters: "", SemverMap: semverMap, Want: []Mode{}}, 130 | {Name: "DetectNothingWithEmptyString", String: "", Delimiters: "/", SemverMap: semverMap, Want: []Mode{}}, 131 | } 132 | 133 | for _, test := range tests { 134 | t.Run(test.Name, func(t *testing.T) { 135 | var got, err = DetectModesFromString(test.String, test.SemverMap, test.Delimiters) 136 | 137 | assert.NoError(t, err) 138 | 139 | assert.Equal(t, len(test.Want), len(got), `want: '%s, got: '%s'`, test.Want, got) 140 | 141 | for i, mode := range got { 142 | assert.IsType(t, test.Want[i], mode, `want: '%s, got: '%s'`, test.Want, got) 143 | } 144 | }) 145 | } 146 | 147 | type ErrorTest struct { 148 | String string 149 | Delimiters string 150 | Error error 151 | Name string 152 | SemverMap semver.Map 153 | } 154 | 155 | var errorTests = []ErrorTest{ 156 | { 157 | Name: "DetectNothingWithFaultySemverMap", 158 | String: "feature/some-feature", 159 | Delimiters: "/", 160 | Error: fmt.Errorf(`failed to detect mode due to unsupported semver level: 'mnr'`), 161 | SemverMap: semver.Map{ 162 | "mnr": {"feature"}, 163 | }, 164 | }, 165 | } 166 | 167 | for _, test := range errorTests { 168 | t.Run(test.Name, func(t *testing.T) { 169 | var _, got = DetectModesFromString(test.String, test.SemverMap, test.Delimiters) 170 | 171 | assert.Error(t, got) 172 | assert.Equal(t, test.Error, got, `want: '%s, got: '%s'`, test.Error, got) 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.2.12 h1:5a07y93zA6SZ09gOa9wLVLznF5zTJMQ+pJ3cZK4IuO8= 2 | github.com/AlecAivazis/survey/v2 v2.2.12/go.mod h1:6d4saEvBsfSHXeN1a5OA5m2+HJ2LuVokllnC77pAIKI= 3 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= 4 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 5 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 6 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 7 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 15 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 16 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 17 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 18 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 19 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 20 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= 21 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 22 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 23 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 24 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 25 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 26 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 27 | github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= 28 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 31 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 32 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 33 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 34 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 35 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 36 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 37 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 38 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 39 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 40 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 41 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 42 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 43 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 44 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 45 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 48 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/restechnica/go-cmder v0.1.1 h1:PuYwEvsJ/dv1lZxpCXmfVsDaPCMmH/hZ9LJjoIZzoho= 50 | github.com/restechnica/go-cmder v0.1.1/go.mod h1:B93P5lufqoYMl8z7PnakXJrkNpVPLPgdaRZGSNhxE5A= 51 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 52 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 53 | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 54 | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 55 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 56 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 57 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 58 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 59 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 60 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 61 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 62 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 63 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 64 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 65 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 66 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 67 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 68 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 69 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 70 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 71 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 74 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 75 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 76 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 77 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 78 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 80 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 81 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 82 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 83 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 84 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 85 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 86 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 87 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 88 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 89 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 90 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 91 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 92 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 93 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 94 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 97 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 103 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 104 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 105 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 106 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 107 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 108 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 109 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 112 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 113 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 114 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 115 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 116 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 117 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | -------------------------------------------------------------------------------- /pkg/versions/api_test.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | "github.com/restechnica/semverbot/internal/fakes" 12 | "github.com/restechnica/semverbot/internal/mocks" 13 | "github.com/restechnica/semverbot/pkg/cli" 14 | "github.com/restechnica/semverbot/pkg/git" 15 | "github.com/restechnica/semverbot/pkg/modes" 16 | ) 17 | 18 | func init() { 19 | zerolog.SetGlobalLevel(zerolog.Disabled) 20 | } 21 | 22 | func TestAPI_GetVersion(t *testing.T) { 23 | type Test struct { 24 | Name string 25 | Prefix string 26 | Suffix string 27 | Version string 28 | } 29 | 30 | var tests = []Test{ 31 | {Name: "ReturnVersion", Prefix: "v", Suffix: "", Version: "0.0.0"}, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.Name, func(t *testing.T) { 36 | var cmder = mocks.NewMockCommander() 37 | cmder.On("Output", mock.Anything, mock.Anything).Return(test.Version, nil) 38 | 39 | var gitAPI = git.CLI{Commander: cmder} 40 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 41 | 42 | var got, err = versionAPI.GetVersion() 43 | 44 | assert.NoError(t, err) 45 | assert.Equal(t, test.Version, got, `want: "%s, got: "%s"`, test.Version, got) 46 | }) 47 | } 48 | 49 | type GitErrorTest struct { 50 | Error error 51 | Name string 52 | Prefix string 53 | Suffix string 54 | } 55 | 56 | var gitErrorTests = []GitErrorTest{ 57 | {Name: "ReturnErrorOnGitError", Prefix: "v", Suffix: "", Error: fmt.Errorf("some-error")}, 58 | } 59 | 60 | for _, test := range gitErrorTests { 61 | t.Run(test.Name, func(t *testing.T) { 62 | var cmder = mocks.NewMockCommander() 63 | cmder.On("Output", mock.Anything, mock.Anything).Return("", test.Error) 64 | 65 | var gitAPI = git.CLI{Commander: cmder} 66 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 67 | 68 | var _, got = versionAPI.GetVersion() 69 | 70 | assert.Error(t, got) 71 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 72 | }) 73 | } 74 | 75 | type SemverErrorTest struct { 76 | Error error 77 | Name string 78 | Prefix string 79 | Suffix string 80 | Versions string 81 | } 82 | 83 | var semverErrorTests = []SemverErrorTest{ 84 | {Name: "ReturnErrorOnInvalidVersions", Versions: "invalid1 invalid2", Error: fmt.Errorf("could not find a valid semver version")}, 85 | {Name: "ReturnErrorOnNoVersions", Versions: "", Error: fmt.Errorf("could not find a valid semver version")}, 86 | } 87 | 88 | for _, test := range semverErrorTests { 89 | t.Run(test.Name, func(t *testing.T) { 90 | var cmder = mocks.NewMockCommander() 91 | cmder.On("Output", mock.Anything, mock.Anything).Return(test.Versions, nil) 92 | 93 | var gitAPI = git.CLI{Commander: cmder} 94 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 95 | 96 | var _, got = versionAPI.GetVersion() 97 | 98 | assert.Error(t, got) 99 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 100 | }) 101 | } 102 | } 103 | 104 | func TestAPI_GetVersionOrDefault(t *testing.T) { 105 | type Test struct { 106 | Name string 107 | Prefix string 108 | Suffix string 109 | Version string 110 | } 111 | 112 | var tests = []Test{ 113 | {Name: "ReturnVersionWithoutError", Prefix: "v", Suffix: "", Version: "0.0.0"}, 114 | } 115 | 116 | for _, test := range tests { 117 | t.Run(test.Name, func(t *testing.T) { 118 | var cmder = mocks.NewMockCommander() 119 | cmder.On("Output", mock.Anything, mock.Anything).Return(test.Version, nil) 120 | 121 | var gitAPI = git.CLI{Commander: cmder} 122 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 123 | 124 | var got, err = versionAPI.GetVersion() 125 | 126 | assert.NoError(t, err) 127 | assert.Equal(t, test.Version, got, `want: "%s, got: "%s"`, test.Version, got) 128 | }) 129 | } 130 | 131 | type ErrorTest struct { 132 | Error error 133 | Prefix string 134 | Suffix string 135 | Name string 136 | } 137 | 138 | var errorTests = []ErrorTest{ 139 | {Name: "ReturnDefaultVersionOnGitApiError", Prefix: "v", Suffix: "", Error: fmt.Errorf("some-error")}, 140 | } 141 | 142 | for _, test := range errorTests { 143 | t.Run(test.Name, func(t *testing.T) { 144 | var cmder = mocks.NewMockCommander() 145 | cmder.On("Output", mock.Anything, mock.Anything).Return("", test.Error) 146 | 147 | var gitAPI = git.CLI{Commander: cmder} 148 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 149 | 150 | var got = versionAPI.GetVersionOrDefault(cli.DefaultVersion) 151 | 152 | assert.Equal(t, cli.DefaultVersion, got, `want: "%s, got: "%s"`, cli.DefaultVersion, got) 153 | }) 154 | } 155 | } 156 | 157 | func TestAPI_PredictVersion(t *testing.T) { 158 | type Test struct { 159 | Mode modes.Mode 160 | Name string 161 | Prefix string 162 | Suffix string 163 | Version string 164 | Want string 165 | } 166 | 167 | var tests = []Test{ 168 | {Name: "ReturnPatchPrediction", Prefix: "v", Suffix: "", Mode: modes.NewPatchMode(), Version: "0.0.0", Want: "0.0.1"}, 169 | {Name: "ReturnMinorPrediction", Prefix: "v", Suffix: "", Mode: modes.NewMinorMode(), Version: "0.0.0", Want: "0.1.0"}, 170 | {Name: "ReturnMajorPrediction", Prefix: "v", Suffix: "", Mode: modes.NewMajorMode(), Version: "0.0.0", Want: "1.0.0"}, 171 | } 172 | 173 | for _, test := range tests { 174 | t.Run(test.Name, func(t *testing.T) { 175 | var cmder = mocks.NewMockCommander() 176 | cmder.On("Output", mock.Anything, mock.Anything).Return(test.Version, nil) 177 | 178 | var gitAPI = git.CLI{Commander: cmder} 179 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 180 | 181 | var got, err = versionAPI.PredictVersion(test.Version, test.Mode) 182 | 183 | assert.NoError(t, err) 184 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 185 | }) 186 | } 187 | 188 | type ErrorTest struct { 189 | Error error 190 | Name string 191 | Prefix string 192 | Suffix string 193 | Version string 194 | } 195 | 196 | var errorTests = []ErrorTest{ 197 | {Name: "ReturnErrorOnModeIncrementError", Prefix: "v", Error: fmt.Errorf("some-error"), Version: "invalid"}, 198 | } 199 | 200 | for _, test := range errorTests { 201 | t.Run(test.Name, func(t *testing.T) { 202 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix} 203 | 204 | var mode = mocks.NewMockMode() 205 | mode.On("Increment", mock.Anything, mock.Anything, mock.Anything).Return(test.Version, test.Error) 206 | 207 | var _, got = versionAPI.PredictVersion("0.0.0", mode) 208 | 209 | assert.Error(t, got) 210 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 211 | }) 212 | } 213 | } 214 | 215 | func TestAPI_PushVersion(t *testing.T) { 216 | type Test struct { 217 | Mode modes.Mode 218 | Name string 219 | Prefix string 220 | Suffix string 221 | Version string 222 | Want string 223 | } 224 | 225 | var tests = []Test{ 226 | {Name: "PushWithPrefix", Mode: modes.NewPatchMode(), Prefix: "v", Version: "0.0.1", Want: "v0.0.1"}, 227 | {Name: "PushWithoutPrefix", Mode: modes.NewPatchMode(), Prefix: "", Version: "0.0.1", Want: "0.0.1"}, 228 | {Name: "PushWithSuffix", Mode: modes.NewPatchMode(), Prefix: "v", Suffix: "a", Version: "0.0.1", Want: "v0.0.1a"}, 229 | {Name: "PushWithSuffixAlt", Mode: modes.NewPatchMode(), Prefix: "v", Suffix: "-alt", Version: "0.0.1", Want: "v0.0.1-alt"}, 230 | {Name: "PushWithoutSuffix", Mode: modes.NewPatchMode(), Prefix: "", Suffix: "", Version: "0.0.1", Want: "0.0.1"}, 231 | } 232 | 233 | for _, test := range tests { 234 | t.Run(test.Name, func(t *testing.T) { 235 | var gitAPI = fakes.NewFakeGitAPI() 236 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 237 | 238 | var err = versionAPI.PushVersion(test.Version) 239 | 240 | var pushedTags = versionAPI.GitAPI.(*fakes.FakeGitAPI).PushedTags 241 | var got = pushedTags[len(pushedTags)-1] 242 | 243 | assert.NoError(t, err) 244 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 245 | }) 246 | } 247 | 248 | type ErrorTest struct { 249 | Error error 250 | Name string 251 | Version string 252 | } 253 | 254 | var errorTests = []ErrorTest{ 255 | {Name: "ReturnErrorOnGitApiError", Error: fmt.Errorf("some-error"), Version: "invalid"}, 256 | } 257 | 258 | for _, test := range errorTests { 259 | t.Run(test.Name, func(t *testing.T) { 260 | var cmder = mocks.NewMockCommander() 261 | cmder.On("Run", mock.Anything, mock.Anything).Return(test.Error) 262 | 263 | var gitAPI = git.CLI{Commander: cmder} 264 | var versionAPI = API{Prefix: "v", Suffix: "", GitAPI: gitAPI} 265 | 266 | var got = versionAPI.PushVersion("0.0.1") 267 | 268 | assert.Error(t, got) 269 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 270 | }) 271 | } 272 | } 273 | 274 | func TestAPI_ReleaseVersion(t *testing.T) { 275 | type Test struct { 276 | Mode modes.Mode 277 | Name string 278 | Prefix string 279 | Suffix string 280 | Version string 281 | Want string 282 | } 283 | 284 | var tests = []Test{ 285 | {Name: "ReleaseWithPrefix", Mode: modes.NewPatchMode(), Prefix: "v", Version: "0.0.1", Want: "v0.0.1"}, 286 | {Name: "ReleaseWithoutPrefix", Mode: modes.NewPatchMode(), Prefix: "", Version: "0.0.1", Want: "0.0.1"}, 287 | {Name: "ReleaseWithSuffix", Mode: modes.NewPatchMode(), Prefix: "v", Suffix: "a", Version: "0.0.1", Want: "v0.0.1a"}, 288 | {Name: "ReleaseWithSuffixAlt", Mode: modes.NewPatchMode(), Prefix: "v", Suffix: "-alt", Version: "0.0.1", Want: "v0.0.1-alt"}, 289 | {Name: "ReleaseWithoutSuffix", Mode: modes.NewPatchMode(), Prefix: "", Suffix: "", Version: "0.0.1", Want: "0.0.1"}, 290 | } 291 | 292 | for _, test := range tests { 293 | t.Run(test.Name, func(t *testing.T) { 294 | var gitAPI = fakes.NewFakeGitAPI() 295 | var versionAPI = API{Prefix: test.Prefix, Suffix: test.Suffix, GitAPI: gitAPI} 296 | 297 | var err = versionAPI.ReleaseVersion(test.Version) 298 | 299 | var localTags = versionAPI.GitAPI.(*fakes.FakeGitAPI).LocalTags 300 | var got = localTags[len(localTags)-1] 301 | 302 | assert.NoError(t, err) 303 | assert.Equal(t, test.Want, got, `want: "%s, got: "%s"`, test.Want, got) 304 | }) 305 | } 306 | 307 | type ErrorTest struct { 308 | Error error 309 | Name string 310 | Version string 311 | } 312 | 313 | var errorTests = []ErrorTest{ 314 | {Name: "ReturnErrorOnGitApiError", Error: fmt.Errorf("some-error"), Version: "invalid"}, 315 | } 316 | 317 | for _, test := range errorTests { 318 | t.Run(test.Name, func(t *testing.T) { 319 | var cmder = mocks.NewMockCommander() 320 | cmder.On("Run", mock.Anything, mock.Anything).Return(test.Error) 321 | 322 | var gitAPI = git.CLI{Commander: cmder} 323 | var versionAPI = API{Prefix: "v", Suffix: "", GitAPI: gitAPI} 324 | 325 | var got = versionAPI.ReleaseVersion("0.0.1") 326 | 327 | assert.Error(t, got) 328 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 329 | }) 330 | } 331 | } 332 | 333 | func TestAPI_UpdateVersion(t *testing.T) { 334 | t.Run("HappyPath", func(t *testing.T) { 335 | var gitAPI = fakes.NewFakeGitAPI() 336 | var versionAPI = API{GitAPI: gitAPI} 337 | 338 | var err = versionAPI.UpdateVersion() 339 | 340 | assert.NoError(t, err) 341 | }) 342 | 343 | type ErrorTest struct { 344 | Error error 345 | Name string 346 | Version string 347 | } 348 | 349 | var errorTests = []ErrorTest{ 350 | {Name: "ReturnErrorOnGitApiError", Error: fmt.Errorf("some-error"), Version: "invalid"}, 351 | } 352 | 353 | for _, test := range errorTests { 354 | t.Run(test.Name, func(t *testing.T) { 355 | var cmder = mocks.NewMockCommander() 356 | cmder.On("Output", mock.Anything, mock.Anything).Return("", test.Error) 357 | 358 | var gitAPI = git.CLI{Commander: cmder} 359 | var versionAPI = API{GitAPI: gitAPI} 360 | 361 | var got = versionAPI.UpdateVersion() 362 | 363 | assert.Error(t, got) 364 | assert.Equal(t, test.Error, got, `want: "%s, got: "%s"`, test.Error, got) 365 | }) 366 | } 367 | } 368 | 369 | func TestNewAPI(t *testing.T) { 370 | t.Run("ValidateState", func(t *testing.T) { 371 | var api = NewAPI("v", "") 372 | assert.NotNil(t, api.GitAPI) 373 | }) 374 | } 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SemverBot 2 | 3 | [![github.com release badge](https://img.shields.io/github/release/restechnica/semverbot.svg)](https://github.com/restechnica/semverbot/) 4 | [![github.com workflow badge](https://github.com/restechnica/semverbot/workflows/main/badge.svg)](https://github.com/restechnica/semverbot/actions?query=workflow%3Amain) 5 | [![go.pkg.dev badge](https://pkg.go.dev/badge/github.com/restechnica/semverbot)](https://pkg.go.dev/github.com/restechnica/semverbot) 6 | [![goreportcard.com badge](https://goreportcard.com/badge/github.com/restechnica/semverbot)](https://goreportcard.com/report/github.com/restechnica/semverbot) 7 | [![img.shields.io MPL2 license badge](https://img.shields.io/github/license/restechnica/semverbot)](./LICENSE) 8 | 9 | A CLI which automates semver versioning. 10 | 11 | ## Table of Contents 12 | 13 | * [Requirements](#requirements) 14 | * [How to install](#how-to-install) 15 | * [Github](#github) 16 | * [Homebrew](#homebrew) 17 | * [Usage](#usage) 18 | * [Modes](#modes) 19 | * [How to configure](#how-to-configure) 20 | * [Configuration properties](#configuration-properties) 21 | * [Examples](#examples) 22 | * [Why SemverBot?](#why-semverbot) 23 | 24 | ## Requirements 25 | 26 | `sbot` requires a `git` installation. 27 | 28 | ## How to install 29 | 30 | `sbot` can be retrieved from GitHub or a Homebrew tap. Run `sbot -h` to validate the installation. 31 | The tool is available for Windows, Linux and macOS. 32 | 33 | ### github 34 | 35 | `sbot` is available through github. The following example works for a GitHub Workflow, other CI/CD tooling will require a different path setup. 36 | 37 | ```shell 38 | SEMVERBOT_VERSION=1.0.0 39 | mkdir bin 40 | echo "$(pwd)/bin" >> $GITHUB_PATH 41 | curl -o bin/sbot -L https://github.com/restechnica/semverbot/releases/download/v$SEMVERBOT_VERSION/sbot-linux-amd64 42 | chmod +x bin/sbot 43 | ``` 44 | 45 | ### homebrew 46 | 47 | `sbot` is available through the public tap [github.com/restechnica/homebrew-tap](https://github.com/restechnica/homebrew-tap) 48 | 49 | ```shell 50 | brew tap restechnica/tap git@github.com:restechnica/homebrew-tap.git 51 | brew install restechnica/tap/semverbot 52 | ``` 53 | 54 | ### golang 55 | 56 | `sbot` is written in golang, which means you can use `go install`. Make sure the installation folder, which depends on your golang setup, is in your system PATH. 57 | 58 | ```shell 59 | go install github.com/restechnica/semverbot/cmd/sbot@v1.0.0 60 | ``` 61 | 62 | ## Usage 63 | 64 | Each command has a `-h, --help` flag available. Support for `-v, --verbose` and `-d, --debug` has been added as well. 65 | 66 | ### `sbot get version` 67 | 68 | Gets the current version, which is the latest `git` semver tag without any prefix. Non-semver tags are ignored. 69 | 70 | ### `sbot init` 71 | 72 | Generates a configuration with defaults, see [configuration defaults](#defaults). 73 | 74 | ### `sbot predict version [-m, --mode] ` 75 | 76 | Gets the next version, without any prefix. Uses a mode to detect which semver level it should increment. Defaults to mode `auto`. 77 | See [Modes](#modes) for more documentation on the supported modes. 78 | 79 | ### `sbot push version` 80 | 81 | Pushes the latest `git` tag to the remote repository. Equivalent to `git push origin {prefix}{version}`. 82 | 83 | ### `sbot release version [-m, --mode] ` 84 | 85 | Creates a new version, which is a `git` annotated tag. Uses a mode to detect which semver level it should increment. 86 | Defaults to mode `auto`. See [Modes](#modes) for more documentation on the supported modes. 87 | 88 | ### `sbot update version` 89 | 90 | Fetches all tags with `git` to make sure the git repo has the latest tags available. 91 | Equivalent to running `git fetch --unshallow` and `git fetch --tags`. 92 | This command is very useful in pipelines where shallow clones are often the default to save time and space. 93 | 94 | ## Modes 95 | 96 | ### auto (default) 97 | 98 | Attempts a series of modes in the following order: 99 | 1. `git-branch` 100 | 1. `git-commit` - only if `git-branch` failed to detect a semver level to increment 101 | 1. `patch` - only if `git-commit` failed to detect a semver level to increment 102 | 103 | ### git-branch 104 | 105 | Detects which semver level to increment based on the **name** of the `git` branch from where a merge commit originated from. 106 | This only works when the old branch has not been deleted yet. 107 | 108 | The branch name is matched against the ['semver' configuration](#defaults). 109 | 110 | ### git-commit 111 | 112 | Detects which semver level to increment based on the **message** of the latest `git` commit. 113 | 114 | The commit message is matched against the ['semver' configuration](#defaults). 115 | 116 | ### major 117 | 118 | Increments the `major` level. 119 | 120 | ### minor 121 | 122 | Increments the `minor` level. 123 | 124 | ### patch 125 | 126 | Increments the `patch` level. 127 | 128 | ## How to configure 129 | 130 | `sbot` supports a configuration file. It looks in the current working directory by default. 131 | 132 | Supported default paths: 133 | - `.semverbot.toml` 134 | - `.sbot.toml` 135 | - `.semverbot/config.toml` 136 | - `.sbot/config.toml` 137 | 138 | `.json` and `.yaml` formats are not officially supported, but might work. Using `.toml` is highly recommended. 139 | 140 | ### Defaults 141 | 142 | `sbot init` generates the following configuration: 143 | 144 | ```toml 145 | mode = "auto" 146 | 147 | [git] 148 | 149 | [git.config] 150 | email = "semverbot@github.com" 151 | name = "semverbot" 152 | 153 | [git.tags] 154 | prefix = "v" 155 | suffix = "" 156 | 157 | [semver] 158 | patch = ["fix", "bug"] 159 | minor = ["feature"] 160 | major = ["release"] 161 | 162 | [modes] 163 | 164 | [modes.git-branch] 165 | delimiters = "/" 166 | 167 | [modes.git-commit] 168 | delimiters = "[]/" 169 | ``` 170 | 171 | ## Configuration properties 172 | 173 | ### mode 174 | 175 | `sbot` supports multiple modes to detect which semver level it should increment. Each mode works with different criteria. 176 | A `mode` flag enables you to switch modes on the fly. 177 | 178 | See [Modes](#modes) for documentation about the supported modes. 179 | 180 | Defaults to `auto`. 181 | 182 | ### git 183 | 184 | `sbot` works with `git` under the hood, which needs to be set up properly. These config options make sure `git` is set up properly for your environment before running an `sbot` command. 185 | 186 | ### git.email 187 | 188 | `git` requires `user.email` to be set. If not set, `sbot` will set `user.email` to the value of this property. Rest assured, `sbot` will not override an existing `user.email` value. 189 | 190 | Without this config `sbot` might show unexpected behaviour. 191 | 192 | ### git.name 193 | 194 | `git` requires `user.name` to be set. If not set, `sbot` will set `user.name` to the value of this property. Rest assured, `sbot` will not override an existing `user.name` value. 195 | 196 | Without this config `sbot` might show unexpected behaviour. 197 | 198 | ### git.tags.prefix 199 | 200 | Different platforms and environments work with different (or without) version prefixes. This option enables you to set whatever prefix you would like to work with. 201 | The `"v"` prefix, e.g. `v1.0.1` is used by default due to its popularity, e.g. some Golang tools completely depend on it. 202 | 203 | Note: `sbot` will always display the version without the prefix. 204 | 205 | ### git.tags.suffix 206 | 207 | In case you need a version suffix, this option enables you to set whatever you would like to work with. 208 | By default, no suffix is used. 209 | 210 | Note: `sbot` will always display the version without the suffix. 211 | 212 | ### semver 213 | 214 | This is where you configure what you think a semver level should be mapped to. 215 | 216 | A mapping of semver levels and words, which are matched against git information. 217 | Whenever a match happens, `sbot` will increment the corresponding level. 218 | 219 | See [Modes](#modes) for documentation about the supported modes. 220 | 221 | ### modes 222 | 223 | `sbot` works with different modes, which might require configuration. 224 | 225 | ### modes.git-branch.delimiters 226 | 227 | A string of delimiters which are used to split a git branch name. 228 | The matching words for each semver level in the semver map are matched against each of the resulting strings from the split. 229 | 230 | e.g. delimiters `"/"` will split `feature/some-feature` into `["feature", "some-feature"]`, 231 | and the `feature` and `some-feature` strings will be matched against semver map values. 232 | 233 | Defaults to `"/"` due to its popular use in git branch names. 234 | 235 | ### modes.git-commit.delimiters 236 | 237 | A string of delimiters which are used to split a git commit message. 238 | 239 | e.g. delimiters `"[]"` will split `[feature] some-feature` into `["feature", " some-feature"]`, 240 | and the `feature` and ` some-feature` strings will be matched against semver map values. 241 | 242 | Defaults to `"[]/"` due to their popular use in git commit messages. The "/" character is often used in pull request 243 | commit messages on GitHub, GitLab and Bitbucket. If somehow the branch name recognition 244 | fails, the merge commit message is used as backup. 245 | 246 | ## Using Environment Variables 247 | 248 | You can use environment variables to override configuration properties. The environment variable name is the uppercase 249 | version of the configuration property name, prefixed with `SBOT_`. For example, to override the `git.tags.suffix` 250 | property, you can set the `SBOT_GIT_TAGS_SUFFIX` environment variable. 251 | 252 | ```shell 253 | export SBOT_GIT_TAGS_SUFFIX="-beta" 254 | sbot release version 255 | ``` 256 | 257 | ## Examples 258 | 259 | ### Local 260 | 261 | Make sure `sbot` is installed. 262 | 263 | ```shell 264 | sbot init 265 | sbot release version 266 | sbot push version 267 | ``` 268 | 269 | These commands are basically all you need to work with `sbot` locally. 270 | 271 | ### GitHub Workflow 272 | 273 | #### Shell 274 | 275 | ```shell 276 | # installation 277 | SEMVERBOT_VERSION=1.0.0 278 | mkdir bin 279 | echo "$(pwd)/bin" >> $GITHUB_PATH 280 | curl -o bin/sbot -L https://github.com/restechnica/semverbot/releases/download/v$SEMVERBOT_VERSION/sbot-linux-amd64 281 | chmod +x bin/sbot 282 | 283 | # preparation 284 | sbot update version 285 | current_version="$(sbot get version)" 286 | release_version="$(sbot predict version)" 287 | echo "CURRENT_VERSION=${current_version}" >> $GITHUB_ENV 288 | echo "RELEASE_VERSION=${release_version}" >> $GITHUB_ENV 289 | echo "current version: ${current_version}" 290 | echo "next version: ${release_version}" 291 | 292 | # usage 293 | sbot release version 294 | sbot push version 295 | ``` 296 | 297 | #### Yaml 298 | 299 | ```yaml 300 | name: main 301 | 302 | on: 303 | push: 304 | branches: [ main ] 305 | 306 | env: 307 | SEMVERBOT_VERSION: "1.0.0" 308 | 309 | jobs: 310 | build: 311 | name: pipeline 312 | runs-on: ubuntu-latest 313 | steps: 314 | - uses: actions/checkout@v2 315 | 316 | - name: set up path 317 | run: | 318 | mkdir bin 319 | echo "$(pwd)/bin" >> $GITHUB_PATH 320 | 321 | - name: install semverbot 322 | run: | 323 | curl -o bin/sbot -L https://github.com/restechnica/semverbot/releases/download/v$SEMVERBOT_VERSION/sbot-linux-amd64 324 | chmod +x bin/sbot 325 | 326 | - name: update version 327 | run: | 328 | sbot update version 329 | current_version="$(sbot get version)" 330 | release_version="$(sbot predict version)" 331 | 332 | echo "CURRENT_VERSION=${current_version}" >> $GITHUB_ENV 333 | echo "RELEASE_VERSION=${release_version}" >> $GITHUB_ENV 334 | 335 | echo "current version: ${current_version}" 336 | echo "next version: ${release_version}" 337 | 338 | # ... build / publish ... 339 | 340 | - name: release version 341 | run: | 342 | sbot release version 343 | sbot push version 344 | ``` 345 | 346 | ### Development workflow 347 | 348 | A typical development workflow when working with `sbot`: 349 | 350 | `[create branch 'feature/my-feature' from main/master]` > `[make changes]` > `[push changes]` > `[create pull request]` > `[approve pull request]` > `[merge pull request]` > `[trigger pipeline]` > 351 | `[calculate next version based on branch name]` > `[build application]` > `[publish artifact]` > `[semverbot release & push version]` 352 | 353 | ## Why SemverBot? 354 | 355 | There are several reasons why you should consider using `sbot` for your semver versioning. 356 | 357 | `sbot` is originally made for large scale IT departments which maintain hundreds, if not thousands, of code repositories. 358 | Manual releases for each of those components and their subcomponents cost a considerable amount of developer time. 359 | 360 | 1. Standardize how your releases are tagged 361 | 2. Automate the releasing process for potentially thousands of code repositories 362 | 363 | ### Automation and pipelines 364 | 365 | `sbot` automates the process of tagging releases for you. 366 | * `sbot` uses `git` under the hood, which is today's widely adopted version control system 367 | * `sbot` does **not** use a file to keep track of the version 368 | * no pipeline loops 369 | * no need to maintain the version in two places, e.g., both a package.json file and git tags 370 | * `sbot` is ready to be used in pipelines out of the box 371 | 372 | Note: it is still possible to use `sbot` and file-based versioning tools side-by-side 373 | 374 | ### Convenience 375 | 376 | * `sbot` is designed to be used by both developers and pipelines 377 | * `sbot` is platform independent 378 | * support for Windows, Linux and macOS 379 | * no dependency on 'complex' `npm`, `pip` or other package management installations 380 | * `sbot` heavily simplifies incrementing semver levels based on git information 381 | * today's `git` projects already hold a lot of useful semver information, e.g., branch names like `feature/xxx` or commit messages like `[fix] xxx` 382 | * no need to create and maintain custom code for semver level detection 383 | 384 | ### Configurability 385 | 386 | * `sbot` supports a well-documented configuration file 387 | * intuitively customize how patch, minor and major levels are detected 388 | * `sbot` supports several flags to override parts of the configuration file on the fly 389 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------