Skip to content

Commit

Permalink
feat: Add CLI option to override package version (#10)
Browse files Browse the repository at this point in the history
* feat: Allow providing version overrides to check action (#6)

* feat: Add version override option to CLI (#6)

* docs: Document --override CLI option in README (#6)
  • Loading branch information
bcyran authored Mar 9, 2023
1 parent fd9ccd9 commit e91e165
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 46 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,18 @@ cd /home/user/workspace/aur/package1 && bumper
```

### Options
| CLI option | Default | Description |
| ------------ | --------- | ------------- |
| `--bump`/`-b` | `true` | Bump outdated packages. If disabled, `bumper` will only check for updates. |
| `--make`/`-m` | `true` | Build the package after bumping and before commiting. |
| `--commit`/`-c` | `true` | Commit made changes. Disabling commit disables push as well. |
| `--push`/`-p` | `false` | Push commited changes. |
| `--config` | `$XDG_CONFIG_HOME/bumper/config.yaml`, `$HOME/.config/bumper/config.yaml` | Configuration file path. See [configuration section](#configuration). |
| `--depth`/`-d` | `1` | Depth of directory tree recursion when looking for packages. By default checks given directory and its children. |
| `--completion` | - | Generate and print shell completion script. Available: bash, zsh, fish. |
| `--version`/`-v` | - | Print version and exit. |
| `--help`/`-h` | - | Print help and exit. |
| CLI option | Default | Description |
| ---------- | ------- | ----------- |
| `--bump`/`-b` | `true` | Bump outdated packages. If disabled, `bumper` will only check for updates. |
| `--make`/`-m` | `true` | Build the package after bumping and before commiting. |
| `--commit`/`-c` | `true` | Commit made changes. Disabling commit disables push as well. |
| `--push`/`-p` | `false` | Push commited changes. |
| `--config` | `$XDG_CONFIG_HOME/bumper/config.yaml`, `$HOME/.config/bumper/config.yaml` | Configuration file path. See [configuration section](#configuration). |
| `--depth`/`-d` | `1` | Depth of directory tree recursion when looking for packages. By default checks given directory and its children. |
| `--override`/`-o` | - | Override version for specified packages, e.g.: `-o mypackage=1.2.3`. This skips upstream check completely. Can be used multiple times for multiple overrides. |
| `--completion` | - | Generate and print shell completion script. Available: bash, zsh, fish. |
| `--version`/`-v` | - | Print version and exit. |
| `--help`/`-h` | - | Print help and exit. |

### Configuration
APIs used to retrieve the upstream versions can have some limitations for unauthorized access.
Expand Down
40 changes: 29 additions & 11 deletions bumper/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"go.uber.org/config"
)

var ErrCheckAction = errors.New("check action error")

const sourceSeparator = "::"

type checkActionResult struct {
Expand Down Expand Up @@ -49,18 +51,34 @@ func NewCheckAction(versionProviderFactory versionProviderFactory, checkConfig c
func (action *CheckAction) Execute(pkg *pack.Package) ActionResult {
actionResult := &checkActionResult{}

if pkg.IsVCS {
actionResult.currentVersion = pkg.Pkgver
actionResult.Status = ActionSkippedStatus
return actionResult
}
var upstreamVersion upstream.Version

var pkgVersionOverride string
action.checkConfig.Get("versionOverrides").Get(pkg.Pkgbase).Populate(&pkgVersionOverride) // nolint:errcheck

if pkgVersionOverride != "" {
var isValid bool
upstreamVersion, isValid = upstream.ParseVersion(pkgVersionOverride)
if !isValid {
actionResult.Status = ActionFailedStatus
actionResult.Error = fmt.Errorf("%w: version override '%s' is not a valid version", ErrCheckAction, pkgVersionOverride)
return actionResult
}
} else {
if pkg.IsVCS {
actionResult.currentVersion = pkg.Pkgver
actionResult.Status = ActionSkippedStatus
return actionResult
}

upstreamUrls := getPackageUrls(pkg)
upstreamVersion, err := action.tryGetUpstreamVersion(upstreamUrls)
if err != nil {
actionResult.Status = ActionFailedStatus
actionResult.Error = err
return actionResult
upstreamUrls := getPackageUrls(pkg)
var err error
upstreamVersion, err = action.tryGetUpstreamVersion(upstreamUrls)
if err != nil {
actionResult.Status = ActionFailedStatus
actionResult.Error = err
return actionResult
}
}

cmpResult := pack.VersionCmp(upstreamVersion, pkg.Pkgver)
Expand Down
69 changes: 64 additions & 5 deletions bumper/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ import (
)

var (
checkConfigProvider, _ = config.NewYAML(config.Source(strings.NewReader("{empty: {}, check: {providers: {version: 2.0.0}}}")))
emptyCheckConfig = checkConfigProvider.Get("empty")
checkConfigWithVersion = checkConfigProvider.Get("check")
versionOverride = "4.2.0"
invalidVersionOverride = "whatever"

fakeVersionCheckConfigProvider, _ = config.NewYAML(config.Source(strings.NewReader("{empty: {}, check: {providers: {fakeVersionProvider: 2.0.0}}}")))
emptyCheckConfig = fakeVersionCheckConfigProvider.Get("empty")
fakeVersionCheckConfig = fakeVersionCheckConfigProvider.Get("check")

versionOverrideCheckConfigProvider, _ = config.NewYAML(config.Source(strings.NewReader(fmt.Sprintf("{check: {versionOverrides: {foopkg: %s}}}", versionOverride))))
versionOverrideCheckConfig = versionOverrideCheckConfigProvider.Get("check")

invalidOverrideCheckConfigProvider, _ = config.NewYAML(config.Source(strings.NewReader(fmt.Sprintf("{check: {versionOverrides: {foopkg: %s}}}", invalidVersionOverride))))
invalidVersionOverrideCheckConfig = invalidOverrideCheckConfigProvider.Get("check")
)

type fakeVersionProvider struct {
Expand All @@ -35,9 +44,9 @@ func (provider *fakeVersionProvider) Equal(other interface{}) bool {

func TestCheckAction_Success(t *testing.T) {
verProvFactory := func(url string, providersConfig config.Value) upstream.VersionProvider {
return &fakeVersionProvider{version: providersConfig.Get("version").String()}
return &fakeVersionProvider{version: providersConfig.Get("fakeVersionProvider").String()}
}
action := NewCheckAction(verProvFactory, checkConfigWithVersion)
action := NewCheckAction(verProvFactory, fakeVersionCheckConfig)
pkg := pack.Package{
Srcinfo: &pack.Srcinfo{
URL: "foo",
Expand All @@ -57,6 +66,32 @@ func TestCheckAction_Success(t *testing.T) {
assert.True(t, pkg.IsOutdated)
}

func TestCheckAction_SuccessVersionOverride(t *testing.T) {
verProvFactory := func(url string, providersConfig config.Value) upstream.VersionProvider {
t.Error("provider should not be called when version override provided")
return nil
}
action := NewCheckAction(verProvFactory, versionOverrideCheckConfig)
pkg := pack.Package{
Srcinfo: &pack.Srcinfo{
Pkgbase: "foopkg",
URL: "foo",
FullVersion: &pack.FullVersion{
Pkgver: pack.Version("1.0.0"),
},
},
}

result := action.Execute(&pkg)

// result assertions
assert.Equal(t, ActionSuccessStatus, result.GetStatus())
assert.Equal(t, fmt.Sprintf("1.0.0 → %s", versionOverride), result.String())
// package assertions
assert.Equal(t, upstream.Version(versionOverride), pkg.UpstreamVersion)
assert.True(t, pkg.IsOutdated)
}

func TestCheckAction_Skip(t *testing.T) {
verProvFactory := func(url string, providersConfig config.Value) upstream.VersionProvider { return nil }
action := NewCheckAction(verProvFactory, emptyCheckConfig)
Expand Down Expand Up @@ -127,6 +162,30 @@ func TestCheckAction_FailChecksMultipleURLs(t *testing.T) {
assert.ErrorContains(t, result.GetError(), expectedErr)
}

func TestCheckAction_FailInvalidVersionOverride(t *testing.T) {
verProvFactory := func(url string, providersConfig config.Value) upstream.VersionProvider {
t.Error("provider should not be called when version override provided")
return nil
}
action := NewCheckAction(verProvFactory, invalidVersionOverrideCheckConfig)
pkg := pack.Package{
Srcinfo: &pack.Srcinfo{
Pkgbase: "foopkg",
URL: "foo",
FullVersion: &pack.FullVersion{
Pkgver: pack.Version("1.0.0"),
},
},
}

result := action.Execute(&pkg)

// result assertions
assert.Equal(t, ActionFailedStatus, result.GetStatus())
assert.Equal(t, "?", result.String())
assert.ErrorContains(t, result.GetError(), fmt.Sprintf("version override '%s' is not a valid version", invalidVersionOverride))
}

func TestCheckActionResult_String(t *testing.T) {
cases := map[checkActionResult]string{
{
Expand Down
7 changes: 5 additions & 2 deletions bumper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var (

// ReadConfig reads config at the given path, or at the default location
// if the path is empty.
func ReadConfig(requestedPath string) (config.Provider, error) {
func ReadConfig(requestedPath string, overrides ...config.YAMLOption) (config.Provider, error) {
var configPath string

if requestedPath != "" {
Expand All @@ -42,7 +42,10 @@ func ReadConfig(requestedPath string) (config.Provider, error) {
configPath = defaultPath
}

return config.NewYAML(config.File(configPath))
configSources := []config.YAMLOption{config.File(configPath)}
configSources = append(configSources, overrides...)

return config.NewYAML(configSources...)
}

func getConfigPath() (string, error) {
Expand Down
7 changes: 5 additions & 2 deletions bumper/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ import (
)

func TestReadConfig_PathOk(t *testing.T) {
override := config.Static(map[string]string{"override": "yes!"})

bumperConfigDirPath := filepath.Join(t.TempDir(), "some/non-standard/dir")
err := os.MkdirAll(bumperConfigDirPath, 0o755)
require.Nil(t, err)
configPath := filepath.Join(bumperConfigDirPath, "config.yaml")
err = os.WriteFile(configPath, []byte("providers: {test_key: test_value}"), 0o644)
err = os.WriteFile(configPath, []byte("{providers: {test_key: test_value}, override: no}"), 0o644)
require.Nil(t, err)

actualConfig, err := ReadConfig(configPath)
actualConfig, err := ReadConfig(configPath, override)

assert.Nil(t, err)
assert.NotNil(t, actualConfig)
assert.Equal(t, "test_value", actualConfig.Get("providers.test_key").String())
assert.Equal(t, "yes!", actualConfig.Get("override").String())
}

func TestReadConfig_PathNoConfig(t *testing.T) {
Expand Down
17 changes: 13 additions & 4 deletions cmd/bumper/bumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ var (
commit: true,
push: false,
}
collectDepth = 1
configPath = ""
completion = ""
collectDepth = 1
configPath = ""
completion = ""
versionOverrides = []string{}
)

var bumperCmd = &cobra.Command{
Expand All @@ -53,6 +54,7 @@ Packages are searched recursively starting in the given dir (current working
directory by default if no dir is given). Default recursion depth is 1 which
enables you to run bumper in a dir containing multiple package dirs.`,
Example: ` bumper find and bump packages in $PWD
bumper --override my-package=1.2.3 override my-package version to 1.2.3
bumper --bump=false find packages, check updates in $PWD
bumper ~/workspace/aur find and bump packages in given dir
bumper ~/workspace/aur/my-package bump single package`,
Expand Down Expand Up @@ -80,7 +82,13 @@ enables you to run bumper in a dir containing multiple package dirs.`,
os.Exit(1)
}

bumperConfig, err := bumper.ReadConfig(configPath)
bumperCLIConfig, err := configFromVersionOverrides(versionOverrides)
if err != nil {
fmt.Printf("Fatal error, invalid CLI option: %v.\n", err)
os.Exit(1)
}

bumperConfig, err := bumper.ReadConfig(configPath, bumperCLIConfig)
if err != nil {
fmt.Printf("Fatal error, invalid config: %v.\n", err)
os.Exit(1)
Expand All @@ -102,6 +110,7 @@ func init() {
bumperCmd.Flags().IntVarP(&collectDepth, "depth", "d", 1, "depth of dir recursion in search for packages")
bumperCmd.Flags().StringVarP(&configPath, "config", "", "", "path to configuration file")
bumperCmd.Flags().StringVarP(&completion, "completion", "", "", "generate completion for shell: bash, zsh, fish")
bumperCmd.Flags().StringArrayVarP(&versionOverrides, "override", "o", []string{}, "override upstream version, format: package=version")
bumperCmd.RegisterFlagCompletionFunc("completion", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { //nolint:errcheck
return []string{"bash", "zsh", "fish"}, cobra.ShellCompDirectiveDefault
})
Expand Down
46 changes: 46 additions & 0 deletions cmd/bumper/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package bumper

import (
"errors"
"fmt"
"strings"

"go.uber.org/config"
)

const overrideSeparator = "="

var ErrInvalidOverride = errors.New("invalid version override")

func configFromVersionOverrides(versionOverrides []string) (config.YAMLOption, error) {
overridesMap, err := parseVersionOverrides(versionOverrides)
if err != nil {
return nil, err
}

var overrideValue interface{}
if len(overridesMap) != 0 {
overrideValue = overridesMap
} else {
overrideValue = nil
}

checkConfig := map[string]map[string]interface{}{
"check": {"versionOverrides": overrideValue},
}
return config.Static(checkConfig), nil
}

func parseVersionOverrides(versionOverrides []string) (map[string]string, error) {
overridesMap := map[string]string{}

for _, overrideString := range versionOverrides {
pkgname, override, separatorFound := strings.Cut(overrideString, overrideSeparator)
if !separatorFound {
return nil, fmt.Errorf("%w: '%s'", ErrInvalidOverride, overrideString)
}
overridesMap[pkgname] = override
}

return overridesMap, nil
}
28 changes: 28 additions & 0 deletions cmd/bumper/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package bumper

import (
"testing"

"github.com/stretchr/testify/assert"
"go.uber.org/config"
)

func TestConfigFromVersionOverrides_Success(t *testing.T) {
overrideString := []string{"foopkg=1.2.3", "barpkg=6.6.6"}

source, err := configFromVersionOverrides(overrideString)
assert.Nil(t, err)

actualConfig, err := config.NewYAML(source)
assert.Nil(t, err)
assert.Equal(t, "1.2.3", actualConfig.Get("check.versionOverrides.foopkg").String())
assert.Equal(t, "6.6.6", actualConfig.Get("check.versionOverrides.barpkg").String())
}

func TestConfigFromVersionOverrides_Fail(t *testing.T) {
overrideString := []string{"invalidstring"}

_, err := configFromVersionOverrides(overrideString)
assert.ErrorIs(t, err, ErrInvalidOverride)
assert.ErrorContains(t, err, "'invalidstring'")
}
6 changes: 3 additions & 3 deletions upstream/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ func (gitHub *gitHubProvider) latestReleaseVersion() (Version, error) {
if release.Draft || release.Prerelease {
continue
}
if version, isValid := parseVersion(release.TagName); isValid {
if version, isValid := ParseVersion(release.TagName); isValid {
return version, nil
}
if version, isValid := parseVersion(release.Name); isValid {
if version, isValid := ParseVersion(release.Name); isValid {
return version, nil
}
}
Expand All @@ -107,7 +107,7 @@ func (gitHub *gitHubProvider) latestTagVersion() (Version, error) {
}

for _, tag := range latestTags {
if version, isValid := parseVersion(tag.Name); isValid {
if version, isValid := ParseVersion(tag.Name); isValid {
return version, nil
}
}
Expand Down
6 changes: 3 additions & 3 deletions upstream/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ func (gitLab *gitLabProvider) latestReleaseVersion() (Version, error) {
if release.Upcoming {
continue
}
if version, isValid := parseVersion(release.TagName); isValid {
if version, isValid := ParseVersion(release.TagName); isValid {
return version, nil
}
if version, isValid := parseVersion(release.Name); isValid {
if version, isValid := ParseVersion(release.Name); isValid {
return version, nil
}
}
Expand All @@ -127,7 +127,7 @@ func (gitLab *gitLabProvider) latestTagVersion() (Version, error) {
}

for _, tag := range latestTags {
if version, isValid := parseVersion(tag.Name); isValid {
if version, isValid := ParseVersion(tag.Name); isValid {
return version, nil
}
}
Expand Down
Loading

0 comments on commit e91e165

Please sign in to comment.