Skip to content

Commit ec60e5b

Browse files
Copilotgoloroden
andauthored
feat: Add customizable commit prefixes for version bumping (#159)
* Initial plan * Implement core custom prefix functionality for CLI and conventionalcommits package Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Complete implementation with precedence handling, comprehensive tests, and documentation Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Refactor to instance-based TypeClassifier, eliminating global state Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Remove unnecessary comments that explain 'what' instead of 'why' Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Address PR review feedback: remove global state, fix documentation, clean up code Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Remove empty init.go file and fix double newline in type.go Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> * Remove accidentally committed binary file Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: goloroden <906327+goloroden@users.noreply.github.com>
1 parent 5fbc32a commit ec60e5b

File tree

11 files changed

+285
-35
lines changed

11 files changed

+285
-35
lines changed

README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ jobs:
7676
uses: thenativeweb/get-next-version@main
7777
with:
7878
prefix: 'v' # optional, defaults to ''
79+
# Optional: customize commit prefixes
80+
# fix_prefixes: 'fix,deps,perf'
81+
# feature_prefixes: 'feat,enhance'
82+
# chore_prefixes: 'chore,docs,style'
7983
- name: Show the next version
8084
run: |
8185
echo ${{ steps.get_next_version.outputs.version }}
@@ -98,4 +102,48 @@ Some examples for commit messages are shown below:
98102
- `feat: Add support for Node.js 18`
99103
- `feat!: Change API from v1 to v2`
100104

101-
Please note that `!` indicates breaking changes, and will always result in a new major version, independent of the type of change.
105+
## Customizing commit prefixes
106+
107+
By default, `get-next-version` uses the following commit prefixes:
108+
109+
- Feature prefixes (minor version bump): `feat`
110+
- Fix prefixes (patch version bump): `fix`
111+
- Chore prefixes (no version bump): `build`, `chore`, `ci`, `docs`, `style`, `refactor`, `perf`, `test`
112+
113+
You can customize these prefixes to match your project's conventions using CLI flags or GitHub Action inputs.
114+
115+
### Using CLI
116+
117+
Use comma-separated values for custom prefixes:
118+
119+
```sh
120+
# Add 'deps' and 'perf' as fix prefixes (patch version bump)
121+
get-next-version --fix-prefixes "fix,deps,perf"
122+
123+
# Add 'enhance' as a feature prefix (minor version bump)
124+
get-next-version --feature-prefixes "feat,enhance"
125+
126+
# Customize chore prefixes (no version bump)
127+
get-next-version --chore-prefixes "chore,docs,style"
128+
129+
# Combine multiple custom prefixes
130+
get-next-version --fix-prefixes "fix,deps" --feature-prefixes "feat,enhance"
131+
```
132+
133+
### Using GitHub Action
134+
135+
When using the GitHub Action, specify custom prefixes as inputs:
136+
137+
```yaml
138+
- name: Get next version
139+
id: get_next_version
140+
uses: thenativeweb/get-next-version@main
141+
with:
142+
fix_prefixes: 'fix,deps,perf'
143+
feature_prefixes: 'feat,enhance'
144+
chore_prefixes: 'chore,docs,style'
145+
```
146+
147+
When you specify custom prefixes, they completely replace the defaults for that category. If you want to keep the defaults and add new ones, include them explicitly in your custom list.
148+
149+
Note that `!` indicates breaking changes, and will always result in a new major version, independent of the type of change.

action.yml.tpl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ inputs:
77
description: 'Sets the version prefix'
88
required: false
99
default: ''
10+
feature_prefixes:
11+
description: 'Sets custom feature prefixes (comma-separated)'
12+
required: false
13+
default: ''
14+
fix_prefixes:
15+
description: 'Sets custom fix prefixes (comma-separated)'
16+
required: false
17+
default: ''
18+
chore_prefixes:
19+
description: 'Sets custom chore prefixes (comma-separated)'
20+
required: false
21+
default: ''
1022
outputs:
1123
version:
1224
description: 'Next version'

action/entrypoint.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ set -e
55
/action/get-next-version \
66
--repository /github/workspace \
77
--target github-action \
8-
--prefix "$INPUT_PREFIX"
8+
--prefix "$INPUT_PREFIX" \
9+
--feature-prefixes "$INPUT_FEATURE_PREFIXES" \
10+
--fix-prefixes "$INPUT_FIX_PREFIXES" \
11+
--chore-prefixes "$INPUT_CHORE_PREFIXES"

cli/root.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package cli
22

33
import (
4+
"strings"
5+
46
"github.com/Masterminds/semver"
57
gogit "github.com/go-git/go-git/v5"
68
"github.com/rs/zerolog/log"
79
"github.com/spf13/cobra"
10+
"github.com/thenativeweb/get-next-version/conventionalcommits"
811
"github.com/thenativeweb/get-next-version/git"
912
"github.com/thenativeweb/get-next-version/target"
1013
"github.com/thenativeweb/get-next-version/util"
@@ -13,15 +16,21 @@ import (
1316
)
1417

1518
var (
16-
rootRepositoryFlag string
17-
rootTargetFlag string
18-
rootPrefixFlag string
19+
rootRepositoryFlag string
20+
rootTargetFlag string
21+
rootPrefixFlag string
22+
rootFeaturePrefixesFlag string
23+
rootFixPrefixesFlag string
24+
rootChorePrefixesFlag string
1925
)
2026

2127
func init() {
2228
RootCommand.Flags().StringVarP(&rootRepositoryFlag, "repository", "r", ".", "sets the path to the repository")
2329
RootCommand.Flags().StringVarP(&rootTargetFlag, "target", "t", "version", "sets the output target")
2430
RootCommand.Flags().StringVarP(&rootPrefixFlag, "prefix", "p", "", "sets the version prefix")
31+
RootCommand.Flags().StringVar(&rootFeaturePrefixesFlag, "feature-prefixes", "", "sets custom feature prefixes (comma-separated)")
32+
RootCommand.Flags().StringVar(&rootFixPrefixesFlag, "fix-prefixes", "", "sets custom fix prefixes (comma-separated)")
33+
RootCommand.Flags().StringVar(&rootChorePrefixesFlag, "chore-prefixes", "", "sets custom chore prefixes (comma-separated)")
2534
}
2635

2736
var RootCommand = &cobra.Command{
@@ -43,14 +52,16 @@ var RootCommand = &cobra.Command{
4352
log.Fatal().Msg("invalid target")
4453
}
4554

55+
classifier := createTypeClassifier()
56+
4657
repository, err := gogit.PlainOpen(rootRepositoryFlag)
4758
if err != nil {
4859
log.Fatal().Msg(err.Error())
4960
}
5061

5162
var nextVersion semver.Version
5263
var hasNextVersion bool
53-
result, err := git.GetConventionalCommitTypesSinceLastRelease(repository)
64+
result, err := git.GetConventionalCommitTypesSinceLastRelease(repository, classifier)
5465
if err != nil {
5566
log.Fatal().Msg(err.Error())
5667
} else {
@@ -63,3 +74,36 @@ var RootCommand = &cobra.Command{
6374
}
6475
},
6576
}
77+
78+
func createTypeClassifier() *conventionalcommits.TypeClassifier {
79+
var choreTypes, fixTypes, featureTypes []string
80+
81+
if rootChorePrefixesFlag != "" {
82+
choreTypes = parseCommaSeparatedPrefixes(rootChorePrefixesFlag)
83+
}
84+
85+
if rootFixPrefixesFlag != "" {
86+
fixTypes = parseCommaSeparatedPrefixes(rootFixPrefixesFlag)
87+
}
88+
89+
if rootFeaturePrefixesFlag != "" {
90+
featureTypes = parseCommaSeparatedPrefixes(rootFeaturePrefixesFlag)
91+
}
92+
93+
return conventionalcommits.NewTypeClassifierWithCustomPrefixes(choreTypes, fixTypes, featureTypes)
94+
}
95+
96+
func parseCommaSeparatedPrefixes(input string) []string {
97+
if input == "" {
98+
return nil
99+
}
100+
101+
var result []string
102+
for _, prefix := range strings.Split(input, ",") {
103+
trimmed := strings.TrimSpace(prefix)
104+
if trimmed != "" {
105+
result = append(result, trimmed)
106+
}
107+
}
108+
return result
109+
}

conventionalcommits/commit_message.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ import (
1010
)
1111

1212
var (
13-
bodyRegex *regexp.Regexp
1413
breakingFooterTokens = []string{"BREAKING CHANGE", "BREAKING-CHANGE"}
1514
footerTokenSeparators = []string{": ", " #"}
1615
)
1716

18-
func initCommitMessage() {
17+
func createBodyRegex(classifier *TypeClassifier) *regexp.Regexp {
1918
typesRegexString := ""
20-
for _, prefix := range allTypes {
19+
for _, prefix := range classifier.GetAllTypes() {
2120
typesRegexString += prefix + "|"
2221
}
2322
typesRegexString = strings.TrimSuffix(typesRegexString, "|")
@@ -26,7 +25,7 @@ func initCommitMessage() {
2625
typesRegexString,
2726
)
2827

29-
bodyRegex = regexp.MustCompile(conventionalCommitBodyRegexString)
28+
return regexp.MustCompile(conventionalCommitBodyRegexString)
3029
}
3130

3231
func splitCommitMessage(message string) (body string, footers []string) {
@@ -57,7 +56,7 @@ func splitCommitMessage(message string) (body string, footers []string) {
5756
return body, footers
5857
}
5958

60-
func CommitMessageToType(message string) (Type, error) {
59+
func CommitMessageToTypeWithClassifier(message string, classifier *TypeClassifier) (Type, error) {
6160
body, footers := splitCommitMessage(message)
6261

6362
var breakingFooterPrefixes []string
@@ -72,6 +71,7 @@ func CommitMessageToType(message string) (Type, error) {
7271
}
7372
}
7473

74+
bodyRegex := createBodyRegex(classifier)
7575
parsedMessageBody := bodyRegex.FindStringSubmatch(body)
7676
if parsedMessageBody == nil {
7777
return Chore, errors.New("invalid message body for conventional commit message")
@@ -85,5 +85,5 @@ func CommitMessageToType(message string) (Type, error) {
8585

8686
typeIndex := util.MustFind(bodyRegex.SubexpNames(), "type")
8787

88-
return StringToType(parsedMessageBody[typeIndex])
88+
return classifier.StringToType(parsedMessageBody[typeIndex])
8989
}

conventionalcommits/commit_message_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
)
99

1010
func TestCommitMessageToType(t *testing.T) {
11+
classifier := conventionalcommits.NewTypeClassifier()
12+
1113
tests := []struct {
1214
message string
1315
doExpectError bool
@@ -32,7 +34,7 @@ func TestCommitMessageToType(t *testing.T) {
3234
}
3335

3436
for _, test := range tests {
35-
commitType, err := conventionalcommits.CommitMessageToType(test.message)
37+
commitType, err := conventionalcommits.CommitMessageToTypeWithClassifier(test.message, classifier)
3638

3739
if test.doExpectError {
3840
assert.Error(t, err)

conventionalcommits/init.go

Lines changed: 0 additions & 6 deletions
This file was deleted.

conventionalcommits/type.go

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,70 @@ const (
1616
)
1717

1818
var (
19-
choreTypes = []string{"build", "chore", "ci", "docs", "style", "refactor", "perf", "test"}
20-
fixTypes = []string{"fix"}
21-
featureTypes = []string{"feat"}
22-
allTypes []string
19+
defaultChoreTypes = []string{"build", "chore", "ci", "docs", "style", "refactor", "perf", "test"}
20+
defaultFixTypes = []string{"fix"}
21+
defaultFeatureTypes = []string{"feat"}
2322
)
2423

25-
func initType() {
26-
for _, types := range [][]string{choreTypes, fixTypes, featureTypes} {
24+
type TypeClassifier struct {
25+
choreTypes []string
26+
fixTypes []string
27+
featureTypes []string
28+
}
29+
30+
func NewTypeClassifier() *TypeClassifier {
31+
return &TypeClassifier{
32+
choreTypes: append([]string{}, defaultChoreTypes...),
33+
fixTypes: append([]string{}, defaultFixTypes...),
34+
featureTypes: append([]string{}, defaultFeatureTypes...),
35+
}
36+
}
37+
38+
func NewTypeClassifierWithCustomPrefixes(customChoreTypes, customFixTypes, customFeatureTypes []string) *TypeClassifier {
39+
tc := &TypeClassifier{}
40+
41+
if len(customChoreTypes) > 0 {
42+
tc.choreTypes = customChoreTypes
43+
} else {
44+
tc.choreTypes = append([]string{}, defaultChoreTypes...)
45+
}
46+
47+
if len(customFixTypes) > 0 {
48+
tc.fixTypes = customFixTypes
49+
} else {
50+
tc.fixTypes = append([]string{}, defaultFixTypes...)
51+
}
52+
53+
if len(customFeatureTypes) > 0 {
54+
tc.featureTypes = customFeatureTypes
55+
} else {
56+
tc.featureTypes = append([]string{}, defaultFeatureTypes...)
57+
}
58+
59+
return tc
60+
}
61+
62+
func (tc *TypeClassifier) GetAllTypes() []string {
63+
var allTypes []string
64+
for _, types := range [][]string{tc.choreTypes, tc.fixTypes, tc.featureTypes} {
2765
allTypes = append(allTypes, types...)
2866
}
67+
return allTypes
2968
}
3069

31-
func StringToType(s string) (Type, error) {
32-
if slices.Contains(choreTypes, strings.ToLower(s)) {
33-
return Chore, nil
70+
func (tc *TypeClassifier) StringToType(s string) (Type, error) {
71+
lowerS := strings.ToLower(s)
72+
73+
if slices.Contains(tc.featureTypes, lowerS) {
74+
return Feature, nil
3475
}
3576

36-
if slices.Contains(fixTypes, strings.ToLower(s)) {
77+
if slices.Contains(tc.fixTypes, lowerS) {
3778
return Fix, nil
3879
}
3980

40-
if slices.Contains(featureTypes, strings.ToLower(s)) {
41-
return Feature, nil
81+
if slices.Contains(tc.choreTypes, lowerS) {
82+
return Chore, nil
4283
}
4384

4485
return Chore, errors.New("invalid string for conventional commit type")

0 commit comments

Comments
 (0)