Skip to content

feat(parser): support regex matching on JSON arrays with scalar elements - #1163

Merged
orhun merged 11 commits into
orhun:mainfrom
ognis1205:feat/support-json-array-field-matcher
Jun 14, 2025
Merged

orhun merged 11 commits into
orhun:mainfrom
ognis1205:feat/support-json-array-field-matcher

Conversation

@ognis1205

@ognis1205 ognis1205 commented Jun 4, 2025 •

Copy link
Copy Markdown
Contributor

Description

This PR extends the commit parser to support regex matching on JSON arrays, where each element is a scalar value (e.g., strings, numbers, booleans). When a commit field (like remote.pr_labels) is a JSON array, all entries are evaluated according to the specified match_mode (any or all). Fixes #1157

Motivation and Context

Previously, only scalar JSON values (e.g., strings, numbers) were matched against regex patterns. This change extends support to JSON arrays, where each element is a scalar value. All values in the array are checked based on the configured match mode (any/all).

How Has This Been Tested?

  1. Clone the mta-solutions/fsharp-data-validation GitHub Repo.
  2. Create a cliff.toml file in the root of the repo with the following contents:
[remote.github]
owner = "mta-solutions"
repo = "fsharp-data-validation"

[changelog]
output = "CHANGELOG.md"

header = """
# Changelog

"""

body = """
{% if version %}\
    ## [{{ version | trim_start_matches(pat="v") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/tree/{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}\
    {% if commits | length > 0 %}\
        \n## [unreleased]
    {% endif %}\
{% endif %}\

{% for group, commits in commits | unique(attribute="id") | filter(attribute="merge_commit", value=true) | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for commit in commits %}
        - {{ commit.message | split(pat="\n") | nth(n=2) | trim_end }} \
        ([PR #{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }})) \
        ([{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }}))\
    {% endfor %}
{% endfor %}\n
"""

footer = """
\n\n*This changelog was automatically generated by [git-cliff](https://github.com/orhun/git-cliff)*
"""

trim = true

[git]
conventional_commits = false
filter_unconventional = false
split_commits = false
commit_parsers = [
  { field = "remote.pr_labels", pattern = "duplicate|invalid|wontfix|skip changelog", skip = true },
  { field = "remote.pr_labels", pattern = "breaking change", group = "<!-- 0 -->🏗️ Breaking Changes" },
  { field = "remote.pr_labels", pattern = "feature|deprecation", group = "<!-- 1 -->🚀 Features" },
  { field = "remote.pr_labels", pattern = "enhancement|refactor", group = "<!-- 1 -->🛠️ Enhancements" },
  { field = "remote.pr_labels", pattern = "bug|regression", group = "<!-- 2 -->🐛 Bug Fixes" },
  { field = "remote.pr_labels", pattern = "security", group = "<!-- 3 -->🔐 Security" },
  { field = "remote.pr_labels", pattern = "documentation", group = "<!-- 4 -->📝 Documentation" },
  { field = "remote.pr_labels", pattern = ".*", group = "<!-- 5 -->🌀 Miscellaneous" },
]
filter_commits = true
sort_commits = "newest"
  1. Run this command from a console:
path/to/my/local/git-cliff/repository/target/debug/git-cliff -v 
  1. View the CHANGELOG.md:
# Changelog

## [2.2.0](https://github.com/mta-solutions/fsharp-data-validation/tree/v2.2.0) (2024-12-13)

### <!-- 1 -->🛠️ Enhancements

- Add additional VCtx functions ([PR #24](https://github.com/mta-solutions/fsharp-data-validation/pull/24)) ([RJSonnenberg](https://github.com/RJSonnenberg))

## [2.1.0](https://github.com/mta-solutions/fsharp-data-validation/tree/v2.1.0) (2024-12-10)

### <!-- 1 -->🛠️ Enhancements

- Remove internal scoping and add Async extension methods for validation contexts ([PR #22](https://github.com/mta-solutions/fsharp-data-validation/pull/22)) ([RJSonnenberg](https://github.com/RJSonnenberg))

### <!-- 4 -->📝 Documentation

- Prep v2.1.0 Release ([PR #23](https://github.com/mta-solutions/fsharp-data-validation/pull/23)) ([RJSonnenberg](https://github.com/RJSonnenberg))

## [2.0.0](https://github.com/mta-solutions/fsharp-data-validation/tree/v2.0.0) (2024-10-08)

### <!-- 1 -->🛠️ Enhancements

- Use FSharpPlus ([PR #20](https://github.com/mta-solutions/fsharp-data-validation/pull/20)) ([devinlyons](https://github.com/devinlyons))

### <!-- 4 -->📝 Documentation

- Prep for Release v2.0.0 ([PR #21](https://github.com/mta-solutions/fsharp-data-validation/pull/21)) ([devinlyons](https://github.com/devinlyons))

## [1.0.1](https://github.com/mta-solutions/fsharp-data-validation/tree/v1.0.1) (2023-11-17)

### <!-- 1 -->🛠️ Enhancements

- Convert to NuGet ([PR #18](https://github.com/mta-solutions/fsharp-data-validation/pull/18)) ([devinlyons](https://github.com/devinlyons))
- Add Giraffe model validation library and examples ([PR #17](https://github.com/mta-solutions/fsharp-data-validation/pull/17)) ([RJSonnenberg](https://github.com/RJSonnenberg))

## [1.0.0](https://github.com/mta-solutions/fsharp-data-validation/tree/v1.0.0) (2022-03-14)

### <!-- 1 -->🛠️ Enhancements

- Add utility functions to help with using NonEmptyLists [MBI-53] ([PR #14](https://github.com/mta-solutions/fsharp-data-validation/pull/14)) ([mleone11](https://github.com/mleone11))



*This changelog was automatically generated by [git-cliff](https://github.com/orhun/git-cliff)*

Screenshots / Logs (if applicable)

N/A

Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (no code change)
  • Refactor (refactoring production code)
  • Other

Checklist:

  • My code follows the code style of this project.
  • I have updated the documentation accordingly.
  • I have formatted the code with rustfmt.
  • I checked the lints with clippy.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
@ognis1205
ognis1205 requested a review from orhun as a code owner June 4, 2025 00:54
@ognis1205

Copy link
Copy Markdown
Contributor Author

cc: @RJSonnenberg

@codecov-commenter

codecov-commenter commented Jun 4, 2025 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 94.44444% with 1 line in your changes missing coverage. Please review.

Project coverage is 42.30%. Comparing base (9970402) to head (c0335b6).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
git-cliff-core/src/commit.rs 94.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1163      +/-   ##
==========================================
+ Coverage   41.95%   42.30%   +0.35%     
==========================================
  Files          21       21              
  Lines        1981     1991      +10     
==========================================
+ Hits          831      842      +11     
+ Misses       1150     1149       -1     
Flag Coverage Δ
unit-tests 42.30% <94.45%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ognis1205 added 4 commits June 4, 2025 11:31
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Comment thread git-cliff-core/src/commit.rs Outdated
@ognis1205

ognis1205 commented Jun 6, 2025 •

Copy link
Copy Markdown
Contributor Author

Hey @RJSonnenberg (cc: @orhun ) ,

I generated the CHANGELOG.md on my local working branch build using the repository you provided (for details, please refer to the “How Has This Been Tested?” section of the PR). Grouping by remote.pr_labels is working, but there's one difference in behavior compared to the existing setup.

{ field = "remote.pr_labels", pattern = ".*", group = "<!-- 5 -->🌀 Miscellaneous" },

Previously, I believe this served as the fallback group setting. However, with the default MatchMode::Any, this does not match when remote.pr_labels is empty. Instead, using:

{ message = ".*", group = "<!-- 6 --> 🌀 Miscellaneous" },

continues to work as a fallback as before.

Note: The original rule ends up matching an empty JSON array ([]) as a string, which is probably not the intended behavior. It might be worth adding a comment or warning about this to avoid confusion.

Any thoughts on this?

…-json-array-field-matcher

Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
@RJSonnenberg

Copy link
Copy Markdown

Hey @RJSonnenberg (cc: @orhun ) ,

I generated the CHANGELOG.md on my local working branch build using the repository you provided (for details, please refer to the “How Has This Been Tested?” section of the PR). Grouping by remote.pr_labels is working, but there's one difference in behavior compared to the existing setup.

{ field = "remote.pr_labels", pattern = ".*", group = "<!-- 5 -->🌀 Miscellaneous" },

Previously, I believe this served as the fallback group setting. However, with the default MatchMode::Any, this does not match when remote.pr_labels is empty. Instead, using:

{ message = ".*", group = "<!-- 6 --> 🌀 Miscellaneous" },

continues to work as a fallback as before.

Note: The original rule ends up matching an empty JSON array ([]) as a string, which is probably not the intended behavior. It might be worth adding a comment or warning about this to avoid confusion.

Any thoughts on this?

I think that is a fine compromise. Like you said, the original only worked as a fallback, so the new fallback group setting looks fine to me. It might throw people off, but as long as it is documented well, we should be good.

@ognis1205

Copy link
Copy Markdown
Contributor Author

@RJSonnenberg

Thanks for your feedback!

Comment thread git-cliff-core/src/config.rs Outdated
Comment on lines +377 to +379
/// Mode to match regex against fields: any or all.
#[serde(default)]
pub match_mode: MatchMode,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading the changes, it's not clear to me why this was added to fix this problem. Can you share some details?

Also, we should document this if it is a necessary change :)

@ognis1205 ognis1205 Jun 7, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the question!

To clarify with a concrete case: imagine we want to match PRs where at least one of the labels matches a pattern like "enhancement|refactor". If a commit has remote.pr_labels = ["feature", "refactor"], then:

  • With match_mode = "any", this is a match (since "refactor" matches).
  • With match_mode = "all", it's not a match (since "feature" doesn't match).

Detailed Example

commit_parsers = [
 { field = "remote.pr_labels", pattern = "enhancement|refactor", group = "<!-- 1 -->🛠️ Enhancements" },
]

Given:

remote.pr_labels = ["feature", "refactor"]

✅ MatchMode::Any

  • "feature" → ❌ (does not match)
  • "refactor" → ✅ (matches)

→ Since at least one label matches, the whole condition is satisfied.

❌ MatchMode::All

  • "feature" → ❌
  • "refactor" → ✅

→ Since not all labels match, the match fails.

Explanation

Previously, CommitParser implicitly treated the remote.pr_labels field as if MatchMode::Any were applied, except when the field was empty.
With this internal changes where the field is now handled as a Vec, I introduced match_mode to make the aggregation behavior of regex matching across vector elements explicit at the API level — i.e., whether the matches should be interpreted as an AND or an OR.
This change ensures that the matching semantics are both predictable and configurable moving forward.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I see... I still think that we can avoid this new field though. Maybe loop through each value in Value::Array and add them to the regex_checks array, wouldn't that work?

If we are going to go forward with it then we should document that this is useful for array types and also figure out if there are any other edge cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I think the behavior you're describing is actually quite similar to what I implemented with MatchMode::Any. It maintains backward compatibility while enabling more flexible matching for array values.

I'll try rewriting the implementation with this in mind and see if it simplifies the design. Thanks for the input!

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, let me know about your progress :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I’ve implemented the changes as you suggested.
One thing to note—as I mentioned in this comment—is that the following fallback:

{ field = "remote.pr_labels", pattern = ".*", group = "<!-- 5 -->🌀 Miscellaneous" },

no longer works as expected. Instead, you’ll need to use:

{ message = ".*", group = "<!-- 6 --> 🌀 Miscellaneous" },

The core logic update is complete, and the only remaining task is to add fixtures. I plan to address that as discussed in this thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orhun (cc: @RJSonnenberg)

Also, as an initial fixture test, I’ve added one based on the commit history from this repository:

https://github.com/mta-solutions/fsharp-data-validation

That said, if you think it’s better for maintainability to have a dedicated test case, as we discussed in the discussion thread, I can also add a commit history that uses PR labels explicitly for testing purposes. Let me know what you think :)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

We can update/expand the fixtures after that discussion is resolved :)

Thanks for your work on this!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure — we’ll continue the discussion there and figure out the best approach.
Thanks again for your support!

@orhun

orhun commented Jun 7, 2025

Copy link
Copy Markdown
Owner

Hey guys! I just had some time catch up with #1157 and this PR - thanks for investigating and possible fix!

I'd like to take a step back and see a minimal example what's broken for understanding this fix better. @ognis1205 can you add a new fixture test? Preferably for grouping pr_labels which is know allegedly broken. I also think that adding that fixture will also help us to catch these regressions better in the future.

@ognis1205

This comment was marked as resolved.

@orhun

orhun commented Jun 10, 2025

Copy link
Copy Markdown
Owner

For the fixtures, you can create a dummy repository on GitHub and simply use that, like we do here or here

@ognis1205

Copy link
Copy Markdown
Contributor Author

@orhun

For the fixtures, you can create a dummy repository on GitHub and simply use that, like we do here or here

Thanks! I will add a fixture test that uses a dummy GitHub repository as suggested.

Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com>
@ognis1205
ognis1205 requested review from RJSonnenberg and orhun June 13, 2025 07:15

@orhun orhun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@orhun
orhun merged commit dc458ea into orhun:main Jun 14, 2025
@ognis1205

Copy link
Copy Markdown
Contributor Author

@orhun

I really appreciate your patience and thoroughness in the reviews. Thank you!

@ognis1205
ognis1205 deleted the feat/support-json-array-field-matcher branch June 14, 2025 16:54
@orhun

orhun commented Jun 16, 2025

Copy link
Copy Markdown
Owner

Thanks for implementing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Git-cliff does not parse changes after breaking change (tag sha-4c3c946)

4 participants