Repository: gembaadvantage/uplift
Pull request #298 (2023-01-11 16:09:13)

feat: add ability to filter on a prerelease when calculating the next semantic version
When enabled, uplift will filter the repository tags that match the suffix
for example, v0.1.0-beta will be bumped to v0.1.1-beta (otherwise it would bump to v0.1.0)

Solves #297 

ga-paul-t: > When enabled, uplift will filter the repository tags that match the suffix for example, v0.1.0-beta will be bumped to v0.1.1-beta (otherwise it would bump to v0.1.0)
> 
> Solves #297

This behaviour that you are experiencing is due to the way semver versioning works when patching a prerelease version. `uplift` uses [github.com/Masterminds/semver](https://github.com/Masterminds/semver), and the logic in question can be found here: https://github.com/Masterminds/semver/blob/master/version.go#L307

The `--ignore-existing-prerelease` flag was introduced as a way to bump semver but disregard prerelease information i.e. to circumvent this behaviour. So in your above example if you used both the `--prerelease` and `--ignore-existing-prerelease` flags you would get that bump from `v0.1.0-beta` to `v0.1.1-beta` for a patch increment
canidam: > The `--ignore-existing-prerelease` flag was introduced as a way to bump semver but disregard prerelease information i.e. to circumvent this behaviour. So in your above example if you used both the `--prerelease` and `--ignore-existing-prerelease` flags you would get that bump from `v0.1.0-beta` to `v0.1.1-beta` for a patch increment

I didn't understand that from the flag description: "ignore any existing prerelease when calculating next semantic version"
I wanted to do the opposite: take into account existing prerelease tags when calculating next semver.

But looks like it does exactly that functionality, so no need for another flag 👍 
I'll do some tests, and remove this code and push an update. Maybe can you think of a better description for this flag?
canidam: This does change the existing behavior when I use both `--prerelease X --ignore-existing-prerelease`.
If I have a single tag `v0.1.2-alpha.1`, and I use `--prerelease beta.1 --ignore-existing-prerelease` I'll get `v0.1.0-beta.1` instead of `v0.1.3-beta.1` which is the behavior today.

In my scenario where I use this as a module label, for every new module I create the first tag will always be `v0.1.0-module` (which is the wanted behavior). I think we still need another flag to keep the current behavior.
ga-paul-t: > This does change the existing behavior when I use both `--prerelease X --ignore-existing-prerelease`. If I have a single tag `v0.1.2-alpha.1`, and I use `--prerelease beta.1 --ignore-existing-prerelease` I'll get `v0.1.0-beta.1` instead of `v0.1.3-beta.1` which is the behavior today.
> 
> In my scenario where I use this as a module label, for every new module I create the first tag will always be `v0.1.0-module` (which is the wanted behavior).
> 
> But I think we still need another flag to keep the current behavior as it is today. WDYT?

I agree that another flag is needed. Something that explicitly states the intended behaviour to filter on the prerelease label.

Here are a couple of ideas:

1. We introduce a new flag that has to be used in combination with the `--prerelease` flag:
```sh
uplift release --prerelease module --filter-on-prerelease --ignore-existing-prerelease
```
2. We introduce a new flag that replaces the `--prerelease` flag:
```sh
uplift release --prerelease-with-filter module --ignore-existing-prerelease
```

I prefer option one. I know it introduces an additional flag, but I see these flags as feature toggles. And filtering based on a prerelease label is niche for mono-repo support. I feel it will marry well when I start working on #263

Thoughts?
canidam: Option one seems better to me, since it's explicitly says what it does. Having two flags `--prerelease` and `--prerelease-with-filter` sound a bit confusing. I'll push an update with an implementation
canidam: @ga-paul-t I've pushed the change, can you review it please?
ga-paul-t: Hey. I'm sorry for not getting back to you sooner. This PR has sent me down a few rabbit holes on the best possible approach to handle Mono-Repos. I have been looking across other tools and frameworks, understanding different approaches to this problem. I am in two minds now.

A more explicit mono-repo feature might be a better way of doing this. I have also considered the entire prerelease workflow within `uplift`. I added the `--prerelease` flag a while ago to provide the minimum feature set in this area, but I am unsure if this is the best approach.

This conversation with you has been valuable. I have yet to rule out approving this. I need a couple of days to do some further design around this.
canidam: Mono-repos are tricky with semver. Maybe a `--mono-repo` flag that specifies additional requirements.

When working with a mono-repo, you must specify the artifact name.
In the changelog, you would want to include any changes that relates to this artifact in addition to any changes to `lib`, `shared` or `common`.

What you have in mind? How other tools handle versioning of mono-repos?

Patch:
diff --git a/cmd/uplift/bump.go b/cmd/uplift/bump.go
index 3cf936e..18a1fa9 100644
--- a/cmd/uplift/bump.go
+++ b/cmd/uplift/bump.go
@@ -155,6 +155,7 @@ func setupBumpContext(opts bumpOptions, out io.Writer) (*context.Context, error)
 		}
 	}
 	ctx.IgnoreExistingPrerelease = opts.IgnoreExistingPrerelease
+	ctx.FilterOnPrerelease = opts.FilterOnPrerelease
 
 	// Handle git config. Command line flag takes precedences
 	ctx.IgnoreDetached = opts.IgnoreDetached
diff --git a/cmd/uplift/release.go b/cmd/uplift/release.go
index 694fc35..e36499d 100644
--- a/cmd/uplift/release.go
+++ b/cmd/uplift/release.go
@@ -213,6 +213,7 @@ func setupReleaseContext(opts releaseOptions, out io.Writer) (*context.Context,
 		}
 	}
 	ctx.IgnoreExistingPrerelease = opts.IgnoreExistingPrerelease
+	ctx.FilterOnPrerelease = opts.FilterOnPrerelease
 
 	// Handle git config. Command line flag takes precedences
 	ctx.IgnoreDetached = opts.IgnoreDetached
diff --git a/cmd/uplift/root.go b/cmd/uplift/root.go
index c548f99..a205356 100644
--- a/cmd/uplift/root.go
+++ b/cmd/uplift/root.go
@@ -38,6 +38,7 @@ type globalOptions struct {
 	NoStage                  bool
 	Silent                   bool
 	IgnoreExistingPrerelease bool
+	FilterOnPrerelease       bool
 	IgnoreDetached           bool
 	IgnoreShallow            bool
 	ConfigDir                string
@@ -81,6 +82,7 @@ func newRootCmd(out io.Writer) *rootCommand {
 	pf.BoolVar(&rootCmd.Opts.IgnoreDetached, "ignore-detached", false, "ignore reported git detached HEAD error")
 	pf.BoolVar(&rootCmd.Opts.IgnoreShallow, "ignore-shallow", false, "ignore reported git shallow clone error")
 	pf.BoolVar(&rootCmd.Opts.IgnoreExistingPrerelease, "ignore-existing-prerelease", false, "ignore any existing prerelease when calculating next semantic version")
+	pf.BoolVar(&rootCmd.Opts.FilterOnPrerelease, "filter-on-prerelease", false, "filter tags that doesn't match the prerelease (requires --ignore-existing-prerelease)")
 
 	cmd.AddCommand(newVersionCmd(out),
 		newCompletionCmd(out),
diff --git a/cmd/uplift/tag.go b/cmd/uplift/tag.go
index b5ecff4..7180852 100644
--- a/cmd/uplift/tag.go
+++ b/cmd/uplift/tag.go
@@ -142,7 +142,7 @@ func newTagCmd(gopts *globalOptions, out io.Writer) *tagCommand {
 			// If only the current tag is to be printed, skip running a pipeline
 			// and just retrieve and print the latest tag
 			if tagCmd.Opts.PrintCurrentTag && !tagCmd.Opts.PrintNextTag {
-				tag := git.LatestTag()
+				tag := git.LatestTag(tagCmd.Opts.Prerelease)
 				fmt.Fprintf(out, tag.Ref)
 				return nil
 			}
@@ -209,6 +209,7 @@ func setupTagContext(opts tagOptions, out io.Writer) (*context.Context, error) {
 		}
 	}
 	ctx.IgnoreExistingPrerelease = opts.IgnoreExistingPrerelease
+	ctx.FilterOnPrerelease = opts.FilterOnPrerelease
 
 	// Handle git config. Command line flag takes precedences
 	ctx.IgnoreDetached = opts.IgnoreDetached
diff --git a/internal/context/context.go b/internal/context/context.go
index c91f094..cef28ad 100644
--- a/internal/context/context.go
+++ b/internal/context/context.go
@@ -43,6 +43,7 @@ type Context struct {
 	Prerelease               string
 	Metadata                 string
 	IgnoreExistingPrerelease bool
+	FilterOnPrerelease       bool
 	NoPrefix                 bool
 	NoVersionChanged         bool
 	CommitDetails            git.CommitDetails
diff --git a/internal/git/utils.go b/internal/git/utils.go
index 8a8371d..68b0413 100644
--- a/internal/git/utils.go
+++ b/internal/git/utils.go
@@ -255,13 +255,23 @@ func retrieveTags(sort []string) []TagEntry {
 }
 
 // LatestTag retrieves the latest tag within the repository
-func LatestTag() TagEntry {
+func LatestTag(suffix string) TagEntry {
 	tags := retrieveTags([]string{"--sort=-creatordate", "--sort=-v:refname"})
 	if len(tags) == 0 {
 		return TagEntry{}
 	}
+	if suffix == "" {
+		return tags[0]
+	}
+	// in case we want to consider suffix, return the first match
+	// as the slice is already sorted
+	for _, t := range tags {
+		if strings.HasSuffix(t.Ref, suffix) {
+			return t
+		}
+	}
 
-	return tags[0]
+	return TagEntry{}
 }
 
 // DescribeTag retrieves details about a specific tag
diff --git a/internal/task/changelog/changelog.go b/internal/task/changelog/changelog.go
index 553b2ae..6fd8837 100644
--- a/internal/task/changelog/changelog.go
+++ b/internal/task/changelog/changelog.go
@@ -76,9 +76,6 @@ type release struct {
 	Changes []git.LogEntry
 }
 
-// Define functions for filtering commits
-type commitFilter func([]git.LogEntry, *regexp.Regexp) []git.LogEntry
-
 // Task that generates a changelog for the current repository
 type Task struct{}
 
@@ -175,7 +172,7 @@ func changelogRelease(ctx *context.Context) ([]release, error) {
 
 	if len(ctx.Changelog.Include) > 0 {
 		log.Info("cherry-picking commits based on include list")
-		ents, err = filterCommits(ents, ctx.Changelog.Include, includeCommits)
+		ents, err = includeCommits(ents, ctx.Changelog.Include)
 		if err != nil {
 			return []release{}, err
 		}
@@ -183,7 +180,7 @@ func changelogRelease(ctx *context.Context) ([]release, error) {
 
 	if len(ctx.Changelog.Exclude) > 0 {
 		log.Info("removing commits based on exclude list")
-		ents, err = filterCommits(ents, ctx.Changelog.Exclude, excludeCommits)
+		ents, err = excludeCommits(ents, ctx.Changelog.Exclude)
 		if err != nil {
 			return []release{}, err
 		}
@@ -247,7 +244,7 @@ func changelogReleases(ctx *context.Context) ([]release, error) {
 
 		if len(ctx.Changelog.Include) > 0 {
 			log.Info("cherry-picking commits based on include list")
-			ents, err = filterCommits(ents, ctx.Changelog.Include, includeCommits)
+			ents, err = includeCommits(ents, ctx.Changelog.Include)
 			if err != nil {
 				return []release{}, err
 			}
@@ -255,7 +252,7 @@ func changelogReleases(ctx *context.Context) ([]release, error) {
 
 		if len(ctx.Changelog.Exclude) > 0 {
 			log.Info("removing commits based on exclude list")
-			ents, err = filterCommits(ents, ctx.Changelog.Exclude, excludeCommits)
+			ents, err = excludeCommits(ents, ctx.Changelog.Exclude)
 			if err != nil {
 				return []release{}, err
 			}
@@ -364,35 +361,44 @@ func reverse(ents []git.LogEntry) {
 	}
 }
 
-func filterCommits(commits []git.LogEntry, regexes []string, filter commitFilter) ([]git.LogEntry, error) {
-	filteredCommits := commits
+func includeCommits(commits []git.LogEntry, regexes []string) ([]git.LogEntry, error) {
+	filtered := []git.LogEntry{}
 	for _, regex := range regexes {
-		rgx, err := regexp.Compile(regex)
+		includeRgx, err := regexp.Compile(regex)
 		if err != nil {
-			return filteredCommits, err
+			return filtered, err
+		}
+
+		// Iterate over the entire list of log entries for each regex and
+		// append any match to the filtered list
+		for _, commit := range commits {
+			if includeRgx.MatchString(commit.Message) {
+				filtered = append(filtered, commit)
+			}
 		}
-		filteredCommits = filter(filteredCommits, rgx)
 	}
 
-	return filteredCommits, nil
+	return filtered, nil
 }
 
-func includeCommits(commits []git.LogEntry, rgx *regexp.Regexp) []git.LogEntry {
-	filteredCommits := []git.LogEntry{}
-	for _, commit := range commits {
-		if rgx.MatchString(commit.Message) {
-			filteredCommits = append(filteredCommits, commit)
+func excludeCommits(commits []git.LogEntry, regexes []string) ([]git.LogEntry, error) {
+	filtered := commits
+	for _, regex := range regexes {
+		excludeRgx, err := regexp.Compile(regex)
+		if err != nil {
+			return filtered, err
 		}
-	}
-	return filteredCommits
-}
 
-func excludeCommits(commits []git.LogEntry, rgx *regexp.Regexp) []git.LogEntry {
-	filteredCommits := []git.LogEntry{}
-	for _, commit := range commits {
-		if !rgx.MatchString(commit.Message) {
-			filteredCommits = append(filteredCommits, commit)
+		// Repeat over the filtered list for every exclude, compressing the list
+		// of log entries on each iteration
+		filterPass := []git.LogEntry{}
+		for _, commit := range filtered {
+			if !excludeRgx.MatchString(commit.Message) {
+				filterPass = append(filterPass, commit)
+			}
 		}
+		filtered = filterPass
 	}
-	return filteredCommits
+
+	return filtered, nil
 }
diff --git a/internal/task/nextsemver/nextsemver.go b/internal/task/nextsemver/nextsemver.go
index e151e2c..e1ee5f3 100644
--- a/internal/task/nextsemver/nextsemver.go
+++ b/internal/task/nextsemver/nextsemver.go
@@ -23,6 +23,7 @@ SOFTWARE.
 package nextsemver
 
 import (
+	"fmt"
 	"strings"
 
 	semv "github.com/Masterminds/semver"
@@ -49,7 +50,12 @@ func (t Task) Skip(ctx *context.Context) bool {
 
 // Run the task
 func (t Task) Run(ctx *context.Context) error {
-	tag := git.LatestTag()
+	var tagSuffix string
+	if ctx.IgnoreExistingPrerelease && ctx.FilterOnPrerelease {
+		tagSuffix = buildTagSuffix(ctx)
+	}
+	tag := git.LatestTag(tagSuffix)
+
 	if tag.Ref == "" {
 		log.Debug("repository not tagged with version")
 	} else {
@@ -126,3 +132,14 @@ func (t Task) Run(ctx *context.Context) error {
 	log.WithField("version", ctx.NextVersion.Raw).Info("identified next semantic version")
 	return nil
 }
+
+func buildTagSuffix(ctx *context.Context) string {
+	var suffix string
+	if ctx.Prerelease != "" {
+		suffix = fmt.Sprintf("-%s", ctx.Prerelease)
+		if ctx.Metadata != "" {
+			suffix = fmt.Sprintf("%s+%s", suffix, ctx.Metadata)
+		}
+	}
+	return suffix
+}


Test patch:
diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml
index 327ac41..5a183d3 100644
--- a/.github/workflows/docker-test.yml
+++ b/.github/workflows/docker-test.yml
@@ -61,7 +61,7 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Lint Dockerfile
-        uses: hadolint/hadolint-action@v3.0.0
+        uses: hadolint/hadolint-action@v3.1.0
         with:
           dockerfile: Dockerfile
 
diff --git a/cmd/uplift/release_test.go b/cmd/uplift/release_test.go
index 9360c6a..6a17af1 100644
--- a/cmd/uplift/release_test.go
+++ b/cmd/uplift/release_test.go
@@ -132,7 +132,8 @@ func TestRelease_SkipChangelog(t *testing.T) {
 	err := relCmd.Cmd.Execute()
 	require.NoError(t, err)
 
-	tag := git.LatestTag()
+	suffix := ""
+	tag := git.LatestTag(suffix)
 	assert.Equal(t, "1.0.1", tag.Ref)
 
 	assert.False(t, changelogExists(t))
@@ -151,7 +152,8 @@ func TestRelease_SkipBumps(t *testing.T) {
 	err := relCmd.Cmd.Execute()
 	require.NoError(t, err)
 
-	tag := git.LatestTag()
+	suffix := ""
+	tag := git.LatestTag(suffix)
 	assert.Equal(t, "1.0.1", tag.Ref)
 
 	actual, err := os.ReadFile("test.txt")
diff --git a/internal/git/utils_test.go b/internal/git/utils_test.go
index 288290a..a293dec 100644
--- a/internal/git/utils_test.go
+++ b/internal/git/utils_test.go
@@ -396,7 +396,8 @@ func TestLatestTag(t *testing.T) {
 	v1, v2 := "v1.0.0", "v2.0.0"
 	TimeBasedTagSeries(t, []string{v1, v2})
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, v2, tag.Ref)
 }
 
@@ -419,14 +420,16 @@ func TestLatestTag_LargeHistory(t *testing.T) {
 		"v2",
 	})
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, "2.0.0", tag.Ref)
 }
 
 func TestLatestTag_NoTagsExist(t *testing.T) {
 	MkTmpDir(t)
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, "", tag.Ref)
 }
 
@@ -438,7 +441,8 @@ func TestLatestTag_NoSemanticTags(t *testing.T) {
 	v2 := "latest"
 	EmptyCommitAndTag(t, v2, "more work")
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, "", tag.Ref)
 }
 
@@ -448,7 +452,8 @@ func TestLatestTag_MixedTagConventions(t *testing.T) {
 	v1, v2, v3 := "v1.0.0", "2.0.0", "v3.0.0"
 	TimeBasedTagSeries(t, []string{v1, v2, v3})
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, "v3.0.0", tag.Ref)
 }
 
@@ -457,7 +462,8 @@ func TestLatestTag_CommitWithMixedTags(t *testing.T) {
 
 	EmptyCommitAndTags(t, "commit", "v1", "prod", "1.0.0")
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 	assert.Equal(t, "1.0.0", tag.Ref)
 }
 
@@ -467,7 +473,8 @@ func TestLatestTag_WithPrerelease(t *testing.T) {
 	t1, t2, t3, t4, t5 := "0.1.0-alpha+0001", "0.1.0-beta+0001", "0.1.0-beta+0002", "0.1.0", "0.1.1-beta+0001"
 	TimeBasedTagSeries(t, []string{t1, t2, t3, t4, t5})
 
-	tag := LatestTag()
+	suffix := ""
+	tag := LatestTag(suffix)
 
 	assert.Equal(t, "0.1.1-beta+0001", tag.Ref)
 }
diff --git a/internal/task/changelog/changelog_test.go b/internal/task/changelog/changelog_test.go
index 2dcd667..ac4ab28 100644
--- a/internal/task/changelog/changelog_test.go
+++ b/internal/task/changelog/changelog_test.go
@@ -590,17 +590,17 @@ func TestRun_IdentifiedSCM(t *testing.T) {
 	assert.Contains(t, buf.String(), expected)
 }
 
-func TestRun_WithIncludes(t *testing.T) {
+func TestRun_WithMultipleIncludes(t *testing.T) {
 	git.InitRepo(t)
 	git.Tag("1.0.0")
-	git.EmptyCommitsAndTag(t, "1.1.0", "ci: tweak", "fix(scope1): a fix", "feat(scope1): a feature", "fix(scope2): another fix")
+	git.EmptyCommitsAndTag(t, "1.1.0", "ci: tweak", "fix(scope1): a fix", "feat(scope1): a feature", "fix(common): another fix")
 
 	var buf bytes.Buffer
 	ctx := &context.Context{
 		Out: &buf,
 		Changelog: context.Changelog{
 			DiffOnly: true,
-			Include:  []string{`^.*\(scope1\)`},
+			Include:  []string{`^.*\(scope1\)`, `^.*\(common\)`},
 		},
 		CurrentVersion: semver.Version{
 			Raw: "1.0.0",
@@ -619,8 +619,8 @@ func TestRun_WithIncludes(t *testing.T) {
 	actual := buf.String()
 	assert.Contains(t, actual, "fix(scope1): a fix")
 	assert.Contains(t, actual, "feat(scope1): a feature")
+	assert.Contains(t, actual, "fix(common): another fix")
 	assert.NotContains(t, actual, "ci: tweak")
-	assert.NotContains(t, actual, "fix(scope2): another fix")
 }
 
 func TestRun_AllWithIncludes(t *testing.T) {
@@ -655,14 +655,14 @@ func TestRun_AllWithIncludes(t *testing.T) {
 func TestRun_CombinedIncludeAndExclude(t *testing.T) {
 	git.InitRepo(t)
 	git.Tag("1.0.0")
-	git.EmptyCommitsAndTag(t, "1.1.0", "ci: tweak", "fix(scope1): a fix", "feat(scope1): a feature", "fix(scope2): another fix")
+	git.EmptyCommitsAndTag(t, "1.1.0", "ci: tweak", "fix(scope1): a fix", "feat(scope1): a feature", "feat(scope2): another feature")
 
 	var buf bytes.Buffer
 	ctx := &context.Context{
 		Out: &buf,
 		Changelog: context.Changelog{
 			DiffOnly: true,
-			Include:  []string{`^.*\(scope1\)`},
+			Include:  []string{`^.*\(scope1\)`, `^.*\(scope2\)`},
 			Exclude:  []string{`^fix`},
 		},
 		CurrentVersion: semver.Version{
@@ -681,7 +681,7 @@ func TestRun_CombinedIncludeAndExclude(t *testing.T) {
 
 	actual := buf.String()
 	assert.Contains(t, actual, "feat(scope1): a feature")
+	assert.Contains(t, actual, "feat(scope2): another feature")
 	assert.NotContains(t, actual, "ci: tweak")
 	assert.NotContains(t, actual, "fix(scope1): a fix")
-	assert.NotContains(t, actual, "fix(scope2): another fix")
 }
diff --git a/internal/task/gittag/gittag_test.go b/internal/task/gittag/gittag_test.go
index 5e5c16c..334419a 100644
--- a/internal/task/gittag/gittag_test.go
+++ b/internal/task/gittag/gittag_test.go
@@ -60,7 +60,8 @@ func TestRun(t *testing.T) {
 	err := Task{}.Run(ctx)
 	require.NoError(t, err)
 
-	ltag := git.LatestTag()
+	suffix := ""
+	ltag := git.LatestTag(suffix)
 	assert.Equal(t, tag, ltag.Ref)
 }
 
@@ -79,7 +80,8 @@ func TestRun_DryRunMode(t *testing.T) {
 	err := Task{}.Run(ctx)
 	require.NoError(t, err)
 
-	ltag := git.LatestTag()
+	suffix := ""
+	ltag := git.LatestTag(suffix)
 	assert.Empty(t, ltag.Ref)
 }
 
@@ -100,7 +102,8 @@ func TestRun_NoVersionChange(t *testing.T) {
 	err := Task{}.Run(ctx)
 	require.NoError(t, err)
 
-	ltag := git.LatestTag()
+	suffix := ""
+	ltag := git.LatestTag(suffix)
 	assert.Equal(t, tag, ltag.Ref)
 }
 
diff --git a/internal/task/nextsemver/nextsemver_test.go b/internal/task/nextsemver/nextsemver_test.go
index 420be56..3c8996d 100644
--- a/internal/task/nextsemver/nextsemver_test.go
+++ b/internal/task/nextsemver/nextsemver_test.go
@@ -100,21 +100,6 @@ BREAKING CHANGE: no backwards compatibility support`,
 	}
 }
 
-func TestRun_PatchIgnorePrerelease(t *testing.T) {
-	git.InitRepo(t)
-	git.Tag("0.1.0-beta.1")
-	git.EmptyCommit(t, "fix: this is a bug fix")
-
-	ctx := &context.Context{
-		Prerelease:               "beta.1",
-		IgnoreExistingPrerelease: true,
-	}
-	err := Task{}.Run(ctx)
-
-	require.NoError(t, err)
-	assert.Equal(t, "0.1.1-beta.1", ctx.NextVersion.Raw)
-}
-
 func TestRun_ExistingVersionNoPrefix(t *testing.T) {
 	git.InitRepo(t)
 	git.Tag("v1.0.0")
@@ -140,6 +125,73 @@ func TestRun_NoSemanticBump(t *testing.T) {
 	require.True(t, ctx.NoVersionChanged)
 }
 
+func TestRun_IgnorePrerelease(t *testing.T) {
+	tests := []struct {
+		name       string
+		commit     string
+		curVer     string
+		prerelease string
+		metadata   string
+		expected   string
+
+		ignorePrerelease bool
+		filterPrerelease bool
+	}{
+		{
+			name:             "PatchIncrement",
+			commit:           "fix: this is a bug fix",
+			curVer:           "0.1.0-beta.1",
+			prerelease:       "beta.1",
+			ignorePrerelease: true,
+			expected:         "0.1.1-beta.1",
+		},
+		{
+			name:             "MinorIncrementNewPrerelease",
+			commit:           "feat: a new beta",
+			curVer:           "0.1.0-beta.1",
+			prerelease:       "beta.2",
+			ignorePrerelease: true,
+			expected:         "0.2.0-beta.2",
+		},
+		{
+			name:             "PatchIncrementFilterPrerelease",
+			commit:           "fix: a new fix",
+			curVer:           "0.1.0-moduleA.1",
+			prerelease:       "moduleA.1",
+			ignorePrerelease: true,
+			filterPrerelease: true,
+			expected:         "0.1.1-moduleA.1",
+		},
+		{
+			name:             "NewReleaseWithNewModule",
+			commit:           "feat: a new module",
+			curVer:           "v1.0.0-moduleA",
+			prerelease:       "moduleB",
+			ignorePrerelease: true,
+			filterPrerelease: true,
+			expected:         "v0.1.0-moduleB",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			git.InitRepo(t)
+			git.Tag(tt.curVer)
+			git.EmptyCommit(t, tt.commit)
+
+			ctx := &context.Context{
+				Prerelease:               tt.prerelease,
+				Metadata:                 tt.metadata,
+				IgnoreExistingPrerelease: tt.ignorePrerelease,
+				FilterOnPrerelease:       tt.filterPrerelease,
+			}
+			err := Task{}.Run(ctx)
+
+			require.NoError(t, err)
+			require.Equal(t, tt.expected, ctx.NextVersion.Raw)
+		})
+	}
+}
+
 func TestRun_FirstVersion(t *testing.T) {
 	tests := []struct {
 		name       string