diff --git a/README.md b/README.md index 0c88e13..7d45d70 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bumper/check.go b/bumper/check.go index bb0f52e..5d80dc7 100644 --- a/bumper/check.go +++ b/bumper/check.go @@ -10,6 +10,8 @@ import ( "go.uber.org/config" ) +var ErrCheckAction = errors.New("check action error") + const sourceSeparator = "::" type checkActionResult struct { @@ -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) diff --git a/bumper/check_test.go b/bumper/check_test.go index a1e1668..3e8db66 100644 --- a/bumper/check_test.go +++ b/bumper/check_test.go @@ -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 { @@ -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", @@ -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) @@ -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{ { diff --git a/bumper/config.go b/bumper/config.go index 9b62dfd..2a28a89 100644 --- a/bumper/config.go +++ b/bumper/config.go @@ -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 != "" { @@ -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) { diff --git a/bumper/config_test.go b/bumper/config_test.go index cc45a90..af2db57 100644 --- a/bumper/config_test.go +++ b/bumper/config_test.go @@ -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) { diff --git a/cmd/bumper/bumper.go b/cmd/bumper/bumper.go index 3ad45b5..15e7383 100644 --- a/cmd/bumper/bumper.go +++ b/cmd/bumper/bumper.go @@ -28,9 +28,10 @@ var ( commit: true, push: false, } - collectDepth = 1 - configPath = "" - completion = "" + collectDepth = 1 + configPath = "" + completion = "" + versionOverrides = []string{} ) var bumperCmd = &cobra.Command{ @@ -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`, @@ -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) @@ -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 }) diff --git a/cmd/bumper/options.go b/cmd/bumper/options.go new file mode 100644 index 0000000..77cb618 --- /dev/null +++ b/cmd/bumper/options.go @@ -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 +} diff --git a/cmd/bumper/options_test.go b/cmd/bumper/options_test.go new file mode 100644 index 0000000..ea2be3b --- /dev/null +++ b/cmd/bumper/options_test.go @@ -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'") +} diff --git a/upstream/github.go b/upstream/github.go index d2f1de3..0ca2af1 100644 --- a/upstream/github.go +++ b/upstream/github.go @@ -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 } } @@ -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 } } diff --git a/upstream/gitlab.go b/upstream/gitlab.go index 48f93f2..1307ca1 100644 --- a/upstream/gitlab.go +++ b/upstream/gitlab.go @@ -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 } } @@ -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 } } diff --git a/upstream/pypi.go b/upstream/pypi.go index a7f3d70..412a9a1 100644 --- a/upstream/pypi.go +++ b/upstream/pypi.go @@ -43,7 +43,7 @@ func (pypi *pypiProvider) LatestVersion() (Version, error) { if err := httpGetJSON(pypi.packageInfoURL(), &packageInfo, nil); err != nil { return "", err } - if version, isValid := parseVersion(packageInfo.Info.Version); isValid { + if version, isValid := ParseVersion(packageInfo.Info.Version); isValid { return version, nil } return "", ErrVersionNotFound diff --git a/upstream/version.go b/upstream/version.go index a3ff0b4..4ee217c 100644 --- a/upstream/version.go +++ b/upstream/version.go @@ -16,9 +16,9 @@ func (v Version) GetVersionStr() string { var versionRegexp = regexp.MustCompile(`^[\w.]+$`) -// parseVersion makes Version from a string. +// ParseVersion makes Version from a string. // If the string cannot be interpreted as a Version, returns nil. -func parseVersion(rawVersion string) (Version, bool) { +func ParseVersion(rawVersion string) (Version, bool) { if !containsDigit(rawVersion) { return Version(""), false } diff --git a/upstream/version_test.go b/upstream/version_test.go index 184b016..7720177 100644 --- a/upstream/version_test.go +++ b/upstream/version_test.go @@ -15,7 +15,7 @@ func TestParseVersion_Valid(t *testing.T) { } for rawVersion, expectedResult := range cases { - result, valid := parseVersion(rawVersion) + result, valid := ParseVersion(rawVersion) assert.True(t, valid) assert.Equal(t, expectedResult, result) @@ -32,7 +32,7 @@ func TestParseVersion_Invalid(t *testing.T) { } for _, rawVersion := range cases { - _, valid := parseVersion(rawVersion) + _, valid := ParseVersion(rawVersion) assert.False(t, valid) }