Problem
FerrFlow currently only recognizes strict conventional commits (feat:, fix:, refactor:, etc.) when determining version bumps. The regex patterns are hardcoded in conventional_commits.rs and don't account for alternative commit conventions that teams might use, such as:
- Branch-style prefixes:
Feat/, Fix/, Refactor/ (capitalized, with slash)
- Capitalized conventional:
Feat:, Fix:
- Other separators or custom patterns
Teams migrating to FerrFlow or working in mixed environments can't use it without rewriting their entire commit history or enforcing a strict format upfront.
Proposed solution
Add a commit_formats (or similar) configuration option in ferrflow.json that lets users define which patterns map to which bump types.
Glob patterns
Each bump level accepts glob patterns to match against the full commit message. This keeps the syntax familiar (FerrFlow already uses globs for branch matching):
{
"commit_formats": {
"minor": ["feat:*", "feat(*):*", "Feat:*", "Feat(*):*", "Feat/*", "feature:*", "feature(*):*"],
"patch": ["fix:*", "fix(*):*", "Fix:*", "Fix(*):*", "Fix/*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*", "Refactor:*", "Refactor(*):*", "Refactor/*"],
"major": ["BREAKING CHANGE*", "*!:*"],
"case_sensitive": true
}
}
With case_sensitive: true (the default), patterns match exactly as written. The default config lists both feat:* and Feat:* explicitly to cover common variants.
This matches commits like:
feat: add login → minor
feat(auth): add login → minor
Feat: add login → minor
Feat/add-login → minor
feature: add login → minor
Fix: resolve crash → patch
Fix/resolve-crash → patch
fix(db): connection leak → patch
refactor: split module → patch
Refactor/cleanup → patch
feat!: remove old API → major
Each value can be:
- A string — single pattern:
"minor": "feat:*"
- A list — multiple patterns:
"minor": ["feat:*", "Feat/*"]
"all" — catch-all, matches any commit: "minor": "all"
Grouping and all option
Use "all" to treat every commit as a bump trigger — useful for projects that don't follow conventional commits at all:
{
"commit_formats": {
"minor": "all"
}
}
More targeted grouping with a catch-all baseline:
{
"commit_formats": {
"patch": "all",
"minor": ["feat:*", "feat(*):*", "Feat/*"],
"major": ["BREAKING CHANGE*", "*!:*"]
}
}
Here, "patch": "all" is the baseline — every commit triggers at least a patch bump, but commits matching minor or major patterns get bumped higher. Priority: major > minor > patch > none.
Case sensitivity
case_sensitive defaults to true — patterns match exactly as written. Set to false for case insensitive matching, which simplifies the config since you don't need to list each casing variant:
{
"commit_formats": {
"minor": ["feat:*", "feat(*):*", "feat/*", "feature:*", "feature(*):*"],
"patch": ["fix:*", "fix(*):*", "fix/*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*", "refactor/*"],
"major": ["BREAKING CHANGE*", "*!:*"],
"case_sensitive": false
}
}
With false, feat:* matches feat:, Feat:, FEAT:, etc.
Default behavior
The default config is permissive — it includes capitalized variants and slash separators out of the box. This is a breaking change: commits like Feat/something or Fix: something that were previously ignored will now trigger version bumps.
Users who want strict conventional commits only can trim the patterns:
{
"commit_formats": {
"minor": ["feat:*", "feat(*):*"],
"patch": ["fix:*", "fix(*):*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*"],
"major": ["BREAKING CHANGE*", "*!:*"]
}
}
Implementation notes
- The regex patterns in
conventional_commits.rs (feat_re(), patch_re(), breaking_re()) would be replaced by glob matching from config
- Reuse
glob_match crate already used for branch matching in prerelease.rs
- For case insensitive matching (
case_sensitive: false): lowercase both the pattern and commit message before glob matching
- The
determine_bump() function takes config as input, tries each level in priority order (major → minor → patch), returns first match
"all" skips pattern matching and always matches that level
- This is a breaking change — must use
feat!: or feat(commits)!: for the commit introducing it
- Schema update needed in
ferrflow.json and Application/packages/site/public/schema/ferrflow.json
- Changelog generation (
changelog.rs) also relies on commit type parsing — needs to stay in sync
Acceptance criteria
Problem
FerrFlow currently only recognizes strict conventional commits (
feat:,fix:,refactor:, etc.) when determining version bumps. The regex patterns are hardcoded inconventional_commits.rsand don't account for alternative commit conventions that teams might use, such as:Feat/,Fix/,Refactor/(capitalized, with slash)Feat:,Fix:Teams migrating to FerrFlow or working in mixed environments can't use it without rewriting their entire commit history or enforcing a strict format upfront.
Proposed solution
Add a
commit_formats(or similar) configuration option inferrflow.jsonthat lets users define which patterns map to which bump types.Glob patterns
Each bump level accepts glob patterns to match against the full commit message. This keeps the syntax familiar (FerrFlow already uses globs for branch matching):
{ "commit_formats": { "minor": ["feat:*", "feat(*):*", "Feat:*", "Feat(*):*", "Feat/*", "feature:*", "feature(*):*"], "patch": ["fix:*", "fix(*):*", "Fix:*", "Fix(*):*", "Fix/*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*", "Refactor:*", "Refactor(*):*", "Refactor/*"], "major": ["BREAKING CHANGE*", "*!:*"], "case_sensitive": true } }With
case_sensitive: true(the default), patterns match exactly as written. The default config lists bothfeat:*andFeat:*explicitly to cover common variants.This matches commits like:
feat: add login→ minorfeat(auth): add login→ minorFeat: add login→ minorFeat/add-login→ minorfeature: add login→ minorFix: resolve crash→ patchFix/resolve-crash→ patchfix(db): connection leak→ patchrefactor: split module→ patchRefactor/cleanup→ patchfeat!: remove old API→ majorEach value can be:
"minor": "feat:*""minor": ["feat:*", "Feat/*"]"all"— catch-all, matches any commit:"minor": "all"Grouping and
alloptionUse
"all"to treat every commit as a bump trigger — useful for projects that don't follow conventional commits at all:{ "commit_formats": { "minor": "all" } }More targeted grouping with a catch-all baseline:
{ "commit_formats": { "patch": "all", "minor": ["feat:*", "feat(*):*", "Feat/*"], "major": ["BREAKING CHANGE*", "*!:*"] } }Here,
"patch": "all"is the baseline — every commit triggers at least a patch bump, but commits matchingminorormajorpatterns get bumped higher. Priority: major > minor > patch > none.Case sensitivity
case_sensitivedefaults totrue— patterns match exactly as written. Set tofalsefor case insensitive matching, which simplifies the config since you don't need to list each casing variant:{ "commit_formats": { "minor": ["feat:*", "feat(*):*", "feat/*", "feature:*", "feature(*):*"], "patch": ["fix:*", "fix(*):*", "fix/*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*", "refactor/*"], "major": ["BREAKING CHANGE*", "*!:*"], "case_sensitive": false } }With
false,feat:*matchesfeat:,Feat:,FEAT:, etc.Default behavior
The default config is permissive — it includes capitalized variants and slash separators out of the box. This is a breaking change: commits like
Feat/somethingorFix: somethingthat were previously ignored will now trigger version bumps.Users who want strict conventional commits only can trim the patterns:
{ "commit_formats": { "minor": ["feat:*", "feat(*):*"], "patch": ["fix:*", "fix(*):*", "perf:*", "perf(*):*", "refactor:*", "refactor(*):*"], "major": ["BREAKING CHANGE*", "*!:*"] } }Implementation notes
conventional_commits.rs(feat_re(),patch_re(),breaking_re()) would be replaced by glob matching from configglob_matchcrate already used for branch matching inprerelease.rscase_sensitive: false): lowercase both the pattern and commit message before glob matchingdetermine_bump()function takes config as input, tries each level in priority order (major → minor → patch), returns first match"all"skips pattern matching and always matches that levelfeat!:orfeat(commits)!:for the commit introducing itferrflow.jsonandApplication/packages/site/public/schema/ferrflow.jsonchangelog.rs) also relies on commit type parsing — needs to stay in syncAcceptance criteria
case_sensitiveoption, defaults totrue"all"glob_matchcrate (consistent with branch matching)"all"as a catch-all for any bump level"all"mode