Skip to content

Conversation

@nemesifier
Copy link
Member

  • Avoided overriding the main entrypoint, rely on cz_openwisp plugin
  • Added openwisp-commit shortcut command + usage docs, this command allows starting the guided commit procedure, facilitates amending, and provides a shortcut to check commit messages.
  • Modified existing openwisp-qa-check script to automatically use the new shortcut
  • The changes above allow using the new commitizen rules without the need of adding a new config file in every openwisp repository
  • Fixed guided commit rules:
    • Don't complain if issue is not referenced at all (is allowed)
    • If issue is referenced, ensure it matches title and body

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Related to #110.

- Avoided overriding the main entrypoint, rely on cz_openwisp plugin
- Added openwisp-commit shortcut command + usage docs, this command
  allows starting the guided commit procedure, facilitates amending,
  and provides a shortcut to check commit messages.
- Modified existing openwisp-qa-check script to automatically use the
  new shortcut
- The changes above allow using the new commitizen rules without the
  need of adding a new config file in every openwisp repository
- Fixed guided commit rules:
  - Don't complain if issue is not referenced at all (is allowed)
  - If issue is referenced, ensure it matches title and body

Related to #110
@nemesifier nemesifier self-assigned this Feb 10, 2026
@nemesifier nemesifier added the bug label Feb 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Walkthrough

Adds a new Bash wrapper script openwisp-commit that runs Commitizen flows and validation, updates developer docs to reference openwisp-commit instead of direct Commitizen commands, replaces the QA hook invocation with openwisp-commit --check, extends OpenWispCommitizen with title/body issue-extraction and validation (capitalization, symmetric issue references, auto-appending "Related to #" when needed), changes the commitizen entry point to cz_openwisp in setup.py, and expands tests to cover many valid and invalid commit-message scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Wrapper as openwisp-commit
    participant CZ as cz (Commitizen CLI)
    participant Validator as OpenWispCommitizen
    participant Git as Git

    User->>Wrapper: run openwisp-commit [--check | --amend | --rev-range]
    alt --check
        Wrapper->>CZ: cz -n cz_openwisp check --rev-range <range>
        CZ->>Validator: validate(commit(s))
        Validator-->>CZ: success / error
        CZ-->>Wrapper: exit code
    else --amend
        Wrapper->>Git: git reset --soft HEAD~1
        Wrapper->>CZ: cz -n cz_openwisp commit
        CZ->>Validator: prompt & format message
        Validator-->>CZ: formatted message (may append "Related to `#N`")
        CZ->>Git: create commit
        Wrapper->>CZ: cz -n cz_openwisp check --rev-range <range>
        CZ->>Validator: validate(new commit)
        Validator-->>CZ: result
    else commit
        Wrapper->>CZ: cz -n cz_openwisp commit
        CZ->>Validator: prompt & format message
        Validator-->>CZ: formatted message
        CZ->>Git: create commit
        Wrapper->>CZ: cz -n cz_openwisp check --rev-range <range>
        CZ->>Validator: validate(commit)
    end
    Wrapper-->>User: exit status & messages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: fine-tuning commitizen rules as referenced in issue #110.
Description check ✅ Passed The description covers key changes, includes all checklist items marked complete, and references the related issue #110.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-commitizen

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
openwisp_utils/releaser/tests/test_commitizen_rules.py (2)

140-144: Misleading test name/docstring: says "must be at end" but asserts the opposite.

The docstring reads "issue reference must be at end of title" yet the test validates that a mid-title #1 is accepted (code == 0). Consider rewording to clarify the intent:

✏️ Suggested wording
-def test_issue_in_the_middle():
-    """Valid: issue reference must be at end of title."""
+def test_issue_in_the_middle():
+    """Valid: issue reference in the middle of the title is also accepted."""

94-111: Static analysis: unused out/err variables in failure-only assertions.

Ruff flags out and err as unused (RUF059) in test_empty_commit_message, test_invalid_prefix_format, and test_title_not_capitalized. Since these tests only check code != 0, the unpacked values are never referenced.

✏️ Prefix with underscores to satisfy linter
 def test_empty_commit_message():
     """Invalid: empty commit message."""
-    code, out, err = run_cz_check("")
+    code, _out, _err = run_cz_check("")
     assert code != 0


 def test_invalid_prefix_format():
     """Invalid: missing square brackets around prefix."""
     message = "qa Good commit message `#1`\n\n" "Body\n\n" "Fixes `#1`"
-    code, out, err = run_cz_check(message)
+    code, _out, _err = run_cz_check(message)
     assert code != 0


 def test_title_not_capitalized():
     """Invalid: title doesn't start with capital letter."""
     message = "[qa] bad commit message `#1`\n\n" "Body\n\n" "Fixes `#1`"
-    code, out, err = run_cz_check(message)
+    code, _out, _err = run_cz_check(message)
     assert code != 0
📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 264ae08 and 00f571e.

📒 Files selected for processing (2)
  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.
📚 Learning: 2026-02-10T20:38:21.134Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.

Applied to files:

  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
📚 Learning: 2026-02-06T20:46:32.980Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.

Applied to files:

  • openwisp_utils/releaser/commitizen.py
🧬 Code graph analysis (1)
openwisp_utils/releaser/tests/test_commitizen_rules.py (1)
openwisp_utils/releaser/commitizen.py (1)
  • message (83-99)
🪛 Ruff (0.15.0)
openwisp_utils/releaser/tests/test_commitizen_rules.py

[error] 6-6: Starting a process with a partial executable path

(S607)


[warning] 96-96: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 96-96: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 103-103: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 103-103: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 110-110: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 110-110: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[error] 150-150: Starting a process with a partial executable path

(S607)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (8)
openwisp_utils/releaser/commitizen.py (5)

88-99: LGTM — auto-append logic is correct after the prior fix.

The body_issues extraction now correctly post-processes matches through _TITLE_ISSUE_EXTRACT_RE, so the set comparison with title_issues works as intended.


101-123: LGTM — clean helper extraction methods.

Both _extract_title_issues and _extract_body_issues correctly separate title from body and reuse the same regex pipeline, keeping the logic DRY with the message() method.


140-198: LGTM — clear symmetry enforcement with actionable error messages.

The three-branch structure (title-only, body-only, mismatch) gives users precise guidance on what to fix.


48-54: Nit: _validate_title validates the raw user input (no prefix), while validate_commit_message validates the full commit line — the capitalization check is consistent.

Both paths correctly check the first letter after the prefix, just at different stages of the workflow. Good.


226-252: The pattern parameter is unused by design; schema_pattern() is still used by commitizen's framework.

The pattern parameter passed to validate_commit_message() is part of the BaseCommitizen interface contract and isn't used within the method because custom validation logic replaces it. However, schema_pattern() itself is not dead code—commitizen's framework calls it separately via the cz check command for pre-validation before invoking validate_commit_message(). Both methods work together: schema_pattern() provides the regex pattern used by the CLI, while validate_commit_message() handles deeper custom validation (symmetry checks, etc.).

The regex structure is also correct: the three alternatives separated by | work with the \Z anchor as intended, and tests confirm multiline messages (both with and without issue references) validate correctly.

No changes needed.

openwisp_utils/releaser/tests/test_commitizen_rules.py (3)

14-18: LGTM — good baseline valid-commit tests.


64-84: LGTM — good coverage of multi-issue scenarios (separate lines and same line).

These tests exercise both Fixes #123\nFixes #124 and `Fixes `#123` `#124 formats, confirming the _BODY_ISSUE_RE captures multiple issues correctly in both styles.


185-206: LGTM — good error-message assertions and auto-append coverage.

The asymmetric-issue error test checks for meaningful keywords in the output, and the auto-appended "Related to" test confirms that the message() output format passes validation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added enhancement releaser Related to the OpenWISP Release Tool labels Feb 10, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp-qa-check (1)

149-157: ⚠️ Potential issue | 🟠 Major

--message flag is now silently ignored, breaking existing workflows.

The runcheckcommit function delegates to openwisp-commit --check, which always validates HEAD^! and does not accept arbitrary commit message input. However, openwisp-qa-check still accepts and documents a --message flag (line 29 in help text, line 229-231 in argument parsing), and still sets the COMMIT_MESSAGE variable (line 188 and 231). This variable is never used, and the flag has no effect.

Users relying on --message to validate arbitrary commit messages (e.g., in CI pipelines that test specific commit messages) will see no validation occur. The documentation at docs/developer/qa-checks.rst also shows example usage with --message.

Either forward the message capability to openwisp-commit (if supported), or remove the dead --message handling, update the help text (line 29), and update the documentation.

🤖 Fix all issues with AI agents
In `@openwisp_utils/releaser/tests/test_commitizen_rules.py`:
- Around line 28-32: The test test_valid_commit_without_issue_single_line_body
duplicates test_valid_commit_without_issue: update
test_valid_commit_without_issue_single_line_body to use a single-line body
message (e.g., set message to "[chores] Good commit message\n\nSome
explanation." -> change to "[chores] Good commit message\n\nSome explanation."
actually should be a one-line body like "[chores] Good commit
message\n\nExplanation." or simply "[chores] Good commit message\n\nSome
explanation" to reflect a single-line body) or delete the duplicate test; ensure
you modify the message variable inside
test_valid_commit_without_issue_single_line_body (the input to run_cz_check) to
a true single-line body or remove the test to avoid redundancy, keeping
test_valid_commit_without_issue and run_cz_check intact.

In `@openwisp-commit`:
- Line 37: The printf call currently uses the literal format string "%%s" which
prints "%s" instead of substituting the argument; update the printf invocation
to use "%s" (single percent) so the "$1" parameter is consumed (i.e., change
printf "%%s\n" "$1" to printf "%s\n" "$1"). Ensure the surrounding code still
passes the correct argument variable to printf.
- Around line 20-44: The --rev-range case currently shifts and assigns REV_RANGE
without validating the next token; update the --rev-range branch (the case
handling that sets REV_RANGE) to first check that a following argument exists
and is not another option (i.e., $1 is non-empty and does not begin with '-')
before assigning; if the value is missing, print an error and call show_help and
exit with non-zero status so REV_RANGE is never silently set to empty.
- Around line 46-56: CZ_CHECK is hardcoded to use HEAD^! instead of the parsed
REV_RANGE and the command variables are defined before argument parsing, so
--rev-range has no effect; move the definitions of CZ_CMD, CZ_COMMIT, CZ_CHECK
and the run_commit_and_check function to after the argument-parsing loop (after
REV_RANGE and CHECK_MODE are set) and change CZ_CHECK to use "$REV_RANGE" (e.g.,
CZ_CHECK="$CZ_CMD check --rev-range \"$REV_RANGE\"") so both explicit checks and
run_commit_and_check pick up the overridden value.
🧹 Nitpick comments (4)
openwisp_utils/releaser/commitizen.py (2)

215-239: The schema_pattern regex cannot enforce issue symmetry — relies on validate_commit_message.

The with_issue_pattern uses named groups title_issues and body_issues, but regex named groups don't enforce equality between captures. This means the pattern will match even when the title and body reference different issue numbers. The actual symmetry enforcement happens in validate_commit_message (lines 158-191), so this works correctly in practice, but the named groups in the schema pattern could be misleading to future readers.

Consider adding a brief comment noting that the pattern is intentionally permissive and that validate_commit_message handles the stricter symmetry check.


5-5: Remove unused regex _TITLE_ISSUE_RE.

_TITLE_ISSUE_RE on line 5 is defined but never referenced in the codebase. The actual regexes used are _TITLE_ISSUE_EXTRACT_RE (line 6) and _BODY_ISSUE_RE (lines 7-10).

♻️ Remove unused regex
-_TITLE_ISSUE_RE = re.compile(r" #[^#\s]+$")
 _TITLE_ISSUE_EXTRACT_RE = re.compile(r" #(\d+)")
openwisp_utils/releaser/tests/test_commitizen_rules.py (2)

83-100: Optional: prefix unused out/err with underscores to satisfy linter.

Ruff RUF059 flags unused out and err variables on lines 85, 92, and 99. These tests only check the return code without inspecting output.

♻️ Example fix for one test
 def test_empty_commit_message():
     """Invalid: empty commit message."""
-    code, out, err = run_cz_check("")
+    code, _out, _err = run_cz_check("")
     assert code != 0

Apply the same pattern to test_invalid_prefix_format and test_title_not_capitalized.


129-136: Ambiguous test intent: comments contradict the test name.

The test is named test_issue_not_at_end_of_title (suggesting invalid behavior) but the comments on lines 131 and 135 say "This is allowed now" and assert code == 0. The test name gives the impression this should fail. Consider renaming to clarify the expected behavior, e.g., test_issue_in_middle_of_title_is_valid.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9d85c3 and 8f9a38b.

📒 Files selected for processing (6)
  • docs/developer/qa-checks.rst
  • openwisp-commit
  • openwisp-qa-check
  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
  • setup.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.
📚 Learning: 2026-02-06T20:46:32.980Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.

Applied to files:

  • openwisp-qa-check
  • openwisp-commit
  • docs/developer/qa-checks.rst
  • setup.py
🧬 Code graph analysis (1)
openwisp_utils/releaser/tests/test_commitizen_rules.py (1)
openwisp_utils/releaser/commitizen.py (1)
  • message (84-98)
🪛 Ruff (0.15.0)
openwisp_utils/releaser/tests/test_commitizen_rules.py

[error] 6-6: Starting a process with a partial executable path

(S607)


[warning] 85-85: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 85-85: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 92-92: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 92-92: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 99-99: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 99-99: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[error] 142-142: Starting a process with a partial executable path

(S607)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.2.0
🔇 Additional comments (5)
setup.py (1)

23-37: LGTM!

The plugin rename from cz_conventional_commits to cz_openwisp avoids overriding the default commitizen plugin, and the rationale is well-documented in the comment. Adding openwisp-commit to the scripts list properly exposes the new wrapper.

docs/developer/qa-checks.rst (1)

130-175: LGTM!

Documentation is clear, well-structured, and accurately reflects the new openwisp-commit workflow — including the --amend, --check flags, auto-append behavior for issue references, and advanced direct cz usage.

openwisp_utils/releaser/commitizen.py (2)

84-98: Verify body formatting when auto-appending "Related to".

In message(), when missing_issues is non-empty, a \n is appended first, then \nRelated to #<issue> for each issue. If the user's body already ends without a trailing newline, the result is:

<body>\n\nRelated to `#123`

That looks correct (blank line separator). But if the body already ends with a newline, you'd get:

<body>\n\n\nRelated to `#123`

This is mitigated by .strip() on line 98 which trims trailing whitespace from the final output. However, body.strip() would also strip leading whitespace from the body if it had any. This is minor since answers["how"].strip() on line 88 already strips the raw input.


157-191: Issue symmetry validation logic is sound.

The three-way branch (title-only, body-only, mismatch) with clear error messages is well structured. One edge case to note: _TITLE_ISSUE_EXTRACT_RE matches #<digits> anywhere in the title (not just at the end), so a commit like [fix] Handle #123 edge case would extract 123 from the title. This appears intentional per the test on line 129 of the test file.

openwisp_utils/releaser/tests/test_commitizen_rules.py (1)

14-18: Good test coverage for symmetric issue references.

The suite covers the key scenarios: issues in both title and body (with various keywords), no issues at all, multiple issues, and auto-appended "Related to". The asymmetric and mismatch error cases are also well-covered.

Also applies to: 42-50

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

openwisp-commit Outdated
exit 0
;;
*)
printf "Unknown argument: %%s\n" "$1"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: %%s in printf produces literal %s instead of substituting the argument.

In bash printf, %% is the escape sequence for a literal % character. So %%s outputs the string %s and $1 is never consumed. This should be %s.

🐛 Proposed fix
-      printf "Unknown argument: %%s\n" "$1"
+      printf "Unknown argument: %s\n" "$1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
printf "Unknown argument: %%s\n" "$1"
printf "Unknown argument: %s\n" "$1"
🤖 Prompt for AI Agents
In `@openwisp-commit` at line 37, The printf call currently uses the literal
format string "%%s" which prints "%s" instead of substituting the argument;
update the printf invocation to use "%s" (single percent) so the "$1" parameter
is consumed (i.e., change printf "%%s\n" "$1" to printf "%s\n" "$1"). Ensure the
surrounding code still passes the correct argument variable to printf.

@nemesifier nemesifier moved this from In progress to Needs review in OpenWISP Contributor's Board Feb 10, 2026
@github-project-automation github-project-automation bot moved this from To do (general) to In progress in OpenWISP Contributor's Board Feb 10, 2026
@coveralls
Copy link

coveralls commented Feb 10, 2026

Coverage Status

coverage: 97.249% (+0.01%) from 97.238%
when pulling 00f571e on fix-commitizen
into d9d85c3 on master.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
openwisp_utils/releaser/tests/test_commitizen_rules.py (1)

129-136: Contradictory comments in test.

Lines 131–132 say "This is allowed now since we extract all issues from title" and then "But let's test that the behavior is correct"—the hedging comments read like notes left during development. Consider simplifying:

Suggested cleanup
 def test_issue_not_at_end_of_title():
-    """Invalid: issue reference must be at end of title."""
-    # This is allowed now since we extract all issues from title
-    # But let's test that the behavior is correct
+    """Valid: issue in the middle of title is still detected symmetrically."""
     message = "[qa] Good `#1` commit message\n\n" "Body\n\n" "Fixes `#1`"
     code, out, err = run_cz_check(message)
-    # This should work since `#1` is in both title and body
     assert code == 0, f"Issue in middle of title should still work: {out + err}"
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9a38b and 9a32bfc.

📒 Files selected for processing (3)
  • openwisp-commit
  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp-commit
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.
🧬 Code graph analysis (1)
openwisp_utils/releaser/tests/test_commitizen_rules.py (1)
openwisp_utils/releaser/commitizen.py (1)
  • message (83-97)
🪛 Ruff (0.15.0)
openwisp_utils/releaser/tests/test_commitizen_rules.py

[error] 6-6: Starting a process with a partial executable path

(S607)


[warning] 85-85: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 85-85: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 92-92: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 92-92: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 99-99: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 99-99: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[error] 142-142: Starting a process with a partial executable path

(S607)

🔇 Additional comments (8)
openwisp_utils/releaser/commitizen.py (4)

83-97: Auto-append logic in message() looks correct.

The set-difference approach for missing_issues and the sorted() call ensure deterministic ordering of auto-appended Related to lines. The final .strip() on the body properly cleans up whitespace.


99-115: Helper methods _extract_title_issues / _extract_body_issues are clean and correct.

They properly split on \n, isolate the title vs. body, and delegate to the compiled regexes. Minor note: lines[0] is safe after the if not lines guard, and the len(lines) < 2 check for body is appropriate.


117-201: Validation logic is well-structured and produces clear error messages.

The three-way branch at lines 161–190 (title-only issues, body-only issues, mismatched issues) gives actionable feedback to the user. The merge-commit early-return and prefix check are solid.


231-244: no_issue_pattern doesn't consume the body, so the schema regex may never match multi-line commits without issues.

The no_issue_pattern uses [^\n]*$ (end of first line) with a negative lookahead, but never consumes the body text. Combined with the outer \Z anchor, the full regex won't match a commit like "[chores] Title\n\nBody text." because the cursor stops at the end of the first line. This is currently masked because validate_commit_message is the actual validation path.

If schema_pattern is purely informational / a fallback behind validate_commit_message, this is fine—but adding a comment would clarify that the regex is intentionally incomplete.

Potential fix to also consume the body
         no_issue_pattern = (
-            r"\[[a-z0-9!/:-]+\] [A-Z][^\n]*"
-            r"$(?!\n\n.*(?:Close|Closes|Closed|Fix|Fixes|Fixed"
-            r"|Resolve|Resolves|Resolved|Related to) #\d+)"
+            r"\[[a-z0-9!/:-]+\] [A-Z][^\n]*"
+            r"(?:\n\n(?:(?!(?:Close|Closes|Closed|Fix|Fixes|Fixed"
+            r"|Resolve|Resolves|Resolved|Related to) #\d+).)*)?$"
         )

[raise_minor_issue, request_verification]

commitizen validate_commit_message vs schema_pattern priority
openwisp_utils/releaser/tests/test_commitizen_rules.py (4)

4-11: Test helper run_cz_check is clean and functional.

Using ["cz", ...] (partial path) triggers Ruff S607, but this is a test utility where cz is expected to be installed in the test environment's PATH. Low risk and acceptable for test code.


14-80: Good coverage of valid commit scenarios.

The tests cover key valid patterns: issue symmetry with Fixes/Closes/Related to, multiple issues, double prefixes, merge commits, and no-issue commits. The assertion messages include captured output, which aids debugging.


83-126: Invalid commit tests are thorough with good error-message assertions.

Checking that error output contains contextual terms like "title", "body", and "mismatch" ensures the user-facing messages stay helpful across refactors. Nice pattern.


189-210: Auto-append tests validate the message()validate_commit_message() round-trip.

These tests confirm that the commit messages produced by message() (with auto-appended Related to lines) pass validation, including the multi-issue variant. This is an important integration-level check.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 5 to 9
_TITLE_ISSUE_EXTRACT_RE = re.compile(r" #(\d+)")
_BODY_ISSUE_RE = re.compile(
r"(?:Close|Closes|Closed|Fix|Fixes|Fixed|Resolve|Resolves|Resolved|Related to) #(\d+)",
re.IGNORECASE,
)
Copy link

@coderabbitai coderabbitai bot Feb 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

_TITLE_ISSUE_EXTRACT_RE matches #<number> anywhere in the title, risking false positives.

The pattern #(\d+) will match any space-preceded #<number> token in the title, including text that isn't an issue reference (e.g., [docs] Updated Python #3.12 instructions would extract 3 as an issue number). Consider anchoring to the end of the title or requiring specific placement if this isn't desired.

Additionally, _BODY_ISSUE_RE only captures one issue per keyword occurrence. A line like Fixes #123 #124 would only capture `123`. Each issue number needs its own keyword (e.g., `Fixes `#123`\nFixes `#124). If that is the intended convention, a brief comment documenting it would help.

Copy link
Member Author

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

_TITLE_ISSUE_EXTRACT_RE matches #<number> anywhere in the title, risking false positives.

The pattern #(\d+) will match any space-preceded #<number> token in the title, including text that isn't an issue reference (e.g., [docs] Updated Python #3.12 instructions would extract 3 as an issue number). Consider anchoring to the end of the title or requiring specific placement if this isn't desired.

This is not worth it, if something starts with #<number> in a commit title nowadays it's 99.9% a reference to an issue. The rest 0.1%, if ever happens, will be dealt with manually.

Additionally, _BODY_ISSUE_RE only captures one issue per keyword occurrence. A line like Fixes #123 #124would only capture `123`. Each issue number needs its own keyword (e.g., `Fixes `#123`\nFixes `#124). If that is the intended convention, a brief comment documenting it would help.

This is an issue and is going to be addressed in the next commit.

Copy link

Choose a reason for hiding this comment

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

@nemesifier, understood on both points!

For the title pattern, I've noted your preference to keep the simpler regex. It makes sense to optimize for the common case and handle rare edge cases manually when they arise.

Looking forward to seeing the fix for the body issue pattern in the next commit—being able to capture multiple issues from a single keyword line will make the validation more flexible.


✏️ Learnings added
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.

@github-project-automation github-project-automation bot moved this from Needs review to In progress in OpenWISP Contributor's Board Feb 10, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openwisp_utils/releaser/commitizen.py`:
- Around line 88-97: The set comparison fails because body_issues uses raw
matches from _BODY_ISSUE_RE (e.g. " `#123`") while title_issues uses digits from
_TITLE_ISSUE_EXTRACT_RE; update the body extraction so each match returned by
_BODY_ISSUE_RE is post-processed with _TITLE_ISSUE_EXTRACT_RE to extract the
digit strings (mirroring _extract_body_issues helper) — i.e., build body_issues
by iterating over _BODY_ISSUE_RE.findall(body) and collecting the digit captures
from _TITLE_ISSUE_EXTRACT_RE.findall(match) into a set so missing_issues
compares correctly in message().
🧹 Nitpick comments (3)
openwisp_utils/releaser/tests/test_commitizen_rules.py (2)

140-144: Test name test_issue_not_at_end_of_title is slightly misleading.

The name implies this should be invalid, but the test asserts success (code == 0). A name like test_valid_commit_with_issue_in_middle_of_title would better convey the intent.


94-111: Nit: unused out/err variables flagged by Ruff (RUF059).

In test_empty_commit_message, test_invalid_prefix_format, and test_title_not_capitalized, the out and err return values from run_cz_check are unpacked but unused. You could use _ to suppress the warnings, though this is very minor in test code.

♻️ Suggested fix (optional)
 def test_empty_commit_message():
     """Invalid: empty commit message."""
-    code, out, err = run_cz_check("")
+    code, _, _ = run_cz_check("")
     assert code != 0
 
 
 def test_invalid_prefix_format():
     """Invalid: missing square brackets around prefix."""
     message = "qa Good commit message `#1`\n\n" "Body\n\n" "Fixes `#1`"
-    code, out, err = run_cz_check(message)
+    code, _, _ = run_cz_check(message)
     assert code != 0
 
 
 def test_title_not_capitalized():
     """Invalid: title doesn't start with capital letter."""
     message = "[qa] bad commit message `#1`\n\n" "Body\n\n" "Fixes `#1`"
-    code, out, err = run_cz_check(message)
+    code, _, _ = run_cz_check(message)
     assert code != 0
openwisp_utils/releaser/commitizen.py (1)

237-250: schema_pattern regex doesn't match multi-line commits without issue references, but this doesn't affect validation.

The regex analysis in the original comment is correct: no_issue_pattern uses [^\n]*$ which matches only to the end of the first line, and the lookahead doesn't consume the body. The final \Z then fails because unconsumed text remains. Tests confirm multi-line no-issue commits like "[chores] Good commit\n\nBody" fail the schema_pattern regex.

However, this is not a functional issue because validate_commit_message is the actual validator used by cz check, and the docstring explicitly acknowledges that schema_pattern provides only "basic checks" while "actual symmetry enforcement...happens in the validate_commit_message method." Tests pass for multi-line no-issue commits because the validation flow uses validate_commit_message, not schema_pattern.

That said, fixing the regex for consistency would be prudent: append [\s\S]* after the lookahead in no_issue_pattern to properly consume the entire message, ensuring the regex matches what the code actually validates.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a32bfc and 264ae08.

📒 Files selected for processing (2)
  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.
📚 Learning: 2026-02-10T20:38:21.134Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.

Applied to files:

  • openwisp_utils/releaser/commitizen.py
  • openwisp_utils/releaser/tests/test_commitizen_rules.py
📚 Learning: 2026-02-06T20:46:32.980Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 560
File: openwisp_utils/commitizen/openwisp.py:9-25
Timestamp: 2026-02-06T20:46:32.980Z
Learning: In openwisp-utils, the commit message prefix "chores" (plural) is intentionally used instead of "chore" (singular) in the Commitizen ALLOWED_PREFIXES configuration.

Applied to files:

  • openwisp_utils/releaser/commitizen.py
🧬 Code graph analysis (1)
openwisp_utils/releaser/tests/test_commitizen_rules.py (1)
openwisp_utils/releaser/commitizen.py (1)
  • message (83-97)
🪛 Ruff (0.15.0)
openwisp_utils/releaser/tests/test_commitizen_rules.py

[error] 6-6: Starting a process with a partial executable path

(S607)


[warning] 96-96: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 96-96: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 103-103: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 103-103: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 110-110: Unpacked variable out is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[warning] 110-110: Unpacked variable err is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


[error] 150-150: Starting a process with a partial executable path

(S607)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
🔇 Additional comments (3)
openwisp_utils/releaser/tests/test_commitizen_rules.py (3)

14-18: LGTM!

Good test coverage for the core valid-commit-with-symmetric-issue-reference scenario.


28-32: Test is now differentiated from test_valid_commit_without_issue.

The message now uses "Some explanation" (no period) vs "Some explanation.", addressing the previous duplicate concern.


64-84: Good coverage for multiple-issue scenarios.

Both separate-line (Fixes #123\nFixes #124) and same-line (`Fixes `#123` `#124) variants are tested, exercising the _BODY_ISSUE_RE group-capture and post-processing logic well.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@nemesifier nemesifier merged commit 24e9775 into master Feb 10, 2026
35 checks passed
@nemesifier nemesifier deleted the fix-commitizen branch February 10, 2026 21:23
@github-project-automation github-project-automation bot moved this from In progress to Done in OpenWISP Contributor's Board Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug enhancement releaser Related to the OpenWISP Release Tool

Projects

Development

Successfully merging this pull request may close these issues.

2 participants