Skip to content

Commit 45d2a69

Browse files
Merge branch 'master' into feature/AI_created_changelog
2 parents d413754 + d9d85c3 commit 45d2a69

19 files changed

+1283
-137
lines changed

CHANGES.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ Version 1.3.0a [unreleased]
66

77
Work in progress.
88

9+
Version 1.2.2 [2026-01-28]
10+
--------------------------
11+
12+
Bugfixes
13+
~~~~~~~~
14+
15+
- Temporarily pinned drf-yasg to 1.21.11 `#565
16+
<https://github.com/openwisp/openwisp-utils/issues/565>`_
17+
- Fixed releaser to only commit tracked files `#552
18+
<https://github.com/openwisp/openwisp-utils/issues/552>`_
19+
- Releaser: Fixed insertion of backported bugfix entries in ReST changelog
20+
`#532 <https://github.com/openwisp/openwisp-utils/issues/532>`_
21+
922
Version 1.2.1 [2025-12-19]
1023
--------------------------
1124

docs/developer/other-utilities.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ Storage Utilities
9191
``openwisp_utils.storage.CompressStaticFilesStorage``
9292
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9393

94-
A static storage backend for compression inheriting from
95-
`django-compress-staticfiles's
96-
<https://pypi.org/project/django-compress-staticfiles/>`_
97-
``CompressStaticFilesStorage`` class.
94+
A static storage backend for minification and compression inheriting from
95+
`django-minify-compress-staticfiles's
96+
<https://github.com/openwisp/django-minify-compress-staticfiles>`_
97+
``MinicompressStorage`` class.
9898

9999
Adds support for excluding file types using
100100
:ref:`OPENWISP_STATICFILES_VERSIONED_EXCLUDE

docs/developer/qa-checks.rst

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ will be skipped.
3030
Shell script to run the following quality assurance checks:
3131

3232
- :ref:`checkmigrations <utils_checkmigrations>`
33-
- :ref:`checkcommit <utils_checkcommit>`
33+
- :ref:`Commit message check <utils_commit_message_checks>`
3434
- :ref:`checkendline <utils_checkendline>`
3535
- :ref:`checkpendingmigrations <utils_checkpendingmigrations>`
3636
- :ref:`checkrst <utils_checkrst>`
@@ -110,10 +110,10 @@ Usage example:
110110
111111
checkmigrations --migration-path ./django_freeradius/migrations/
112112
113-
.. _utils_checkcommit:
113+
.. _utils_commit_message_checks:
114114

115-
``checkcommit``
116-
---------------
115+
``Commit message checks``
116+
-------------------------
117117

118118
Ensures the last commit message follows our :ref:`commit message style
119119
guidelines <openwisp_commit_message_style_guidelines>`.
@@ -122,23 +122,38 @@ We want to keep the commit log readable, consistent and easy to scan in
122122
order to make it easy to analyze the history of our modules, which is also
123123
a very important activity when performing maintenance.
124124

125-
Usage example:
125+
This check uses `Commitizen
126+
<https://commitizen-tools.github.io/commitizen/>`_ with a custom OpenWISP
127+
Commitizen plugin. After staging changes to git, contributors can use
128+
Commitizen to create commit messages easily.
129+
130+
Instead of using :
126131

127132
.. code-block::
128133
129-
checkcommit --message "$(git log --format=%B -n 1)"
134+
git commit
135+
136+
Contributors can use:
137+
138+
.. code-block::
139+
140+
cz commit
141+
142+
This command interactively prompts for the commit prefix, title (including
143+
the related issue number) and a short description of the changes, and
144+
generate a commit messages following the OpenWISP conventions.
130145

131-
If, for some reason, you wish to skip this QA check for a specific commit
132-
message you can add ``#noqa`` to the end of your commit message.
146+
The commit message can be checked using Commitizen with the help of ``cz
147+
check``.
133148

134149
Usage example:
135150

136151
.. code-block::
137152
138-
[qa] Improved #20
153+
cz check --rev-range HEAD^!
139154
140-
Simulation of a special unplanned case
141-
#noqa
155+
This command validates the latest commit message against the defined
156+
conventions and reports any formatting issues.
142157

143158
.. _utils_checkendline:
144159

docs/developer/releaser-tool.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ different version manually.
8383

8484
**2. Change Log Generation & Review** A changelog is generated from your
8585
recent commits. If an OpenAI token is configured, the tool will offer to
86-
generate a more readable summary. You will then be shown the final
87-
changelog block and asked to accept it before the files are modified.
86+
generate a more readable summary (this is disabled by default). You will
87+
then be shown the final changelog block and asked to accept it before the
88+
files are modified.
8889

8990
**3. Resilient Error Handling** If any network operation fails (e.g.,
9091
creating a pull request), the tool won't crash. Instead, it will prompt

openwisp-qa-check

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,32 @@ runblack() {
147147
}
148148

149149
runcheckcommit() {
150-
if [ -z "$COMMIT_MESSAGE" ]; then COMMIT_MESSAGE=$(git log -1 --pretty=%B); fi
151-
152-
checkcommit --message "$COMMIT_MESSAGE" &&
153-
echo "SUCCESS: Commit message check successful!" ||
154-
{
155-
echo -e "Checked commit message:\n\n$COMMIT_MESSAGE\n\n"
156-
echoerr "ERROR: Commit message check failed!"
157-
FAILURE=1
158-
}
150+
if ! command -v cz >/dev/null 2>&1; then
151+
echoerr "ERROR: Commitizen (cz) is not installed or not available in PATH."
152+
echoerr "Please install it to enable commit message enforcement."
153+
FAILURE=1
154+
return
155+
fi
156+
if [ -z "$COMMIT_MESSAGE" ]; then
157+
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
158+
fi
159+
if ! cz check --message "$COMMIT_MESSAGE"; then
160+
echoerr "ERROR: Commit message check failed."
161+
echoerr ""
162+
echoerr "Suggested fix:"
163+
echoerr " git reset --soft HEAD~1; cz commit"
164+
echoerr " # or"
165+
echoerr " git commit --amend"
166+
echoerr ""
167+
echoerr "For more information, run:"
168+
echoerr " cz info"
169+
echoerr ""
170+
echoerr "To see a valid commit message example, run:"
171+
echoerr " cz example"
172+
FAILURE=1
173+
else
174+
echo "SUCCESS: Commit message check successful!"
175+
fi
159176
}
160177

161178
runcheckpendingmigrations() {

openwisp_utils/releaser/changelog.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ def run_git_cliff(version=None):
4545
file=sys.stderr,
4646
)
4747
sys.exit(1)
48-
48+
# Pull latest tags before calculating changelog
49+
try:
50+
subprocess.run(
51+
["git", "pull", "--tags"],
52+
capture_output=True,
53+
text=True,
54+
check=True,
55+
)
56+
except subprocess.CalledProcessError as e:
57+
print(f"Warning: Failed to pull tags: {e.stderr}", file=sys.stderr)
58+
# Run git-cliff to calculate changelog
4959
try:
5060
cmd = ["git", "cliff", "--unreleased", "--config", config_path]
5161
if version:
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import re
2+
3+
from commitizen.cz.base import BaseCommitizen, ValidationResult
4+
5+
_TITLE_ISSUE_RE = re.compile(r"^[A-Z][^\n]* \#(\d+)$")
6+
7+
8+
class OpenWispCommitizen(BaseCommitizen):
9+
"""Commitizen plugin for OpenWISP commit conventions."""
10+
11+
# Single source for allowed prefixes
12+
ALLOWED_PREFIXES = [
13+
"feature",
14+
"change",
15+
"fix",
16+
"docs",
17+
"test",
18+
"ci",
19+
"chores",
20+
"qa",
21+
"deps",
22+
"release",
23+
"bump",
24+
]
25+
26+
ERROR_TEMPLATE = (
27+
"Invalid commit message format\n\n"
28+
"Expected format:\n\n"
29+
" [prefix] Capitalized title #<issue>\n\n"
30+
" <long-description>\n\n"
31+
" Fixes #<issue>\n\n"
32+
"Examples:\n\n"
33+
" [feature] Add subnet import support #104\n\n"
34+
" Add support for importing multiple subnets from a CSV file.\n\n"
35+
" Fixes #104"
36+
)
37+
38+
def _validate_title(self, value: str) -> bool | str:
39+
value = value.strip()
40+
if not value:
41+
return "Commit title cannot be empty."
42+
if not _TITLE_ISSUE_RE.match(value):
43+
return (
44+
"Commit title must start with a capital letter and "
45+
"end with an issue number (e.g. #104)."
46+
)
47+
return True
48+
49+
def questions(self):
50+
return [
51+
{
52+
"type": "list",
53+
"name": "change_type",
54+
"message": "Select the type of change you are committing",
55+
"choices": [
56+
{"value": prefix, "name": f"[{prefix}]"}
57+
for prefix in self.ALLOWED_PREFIXES
58+
],
59+
},
60+
{
61+
"type": "input",
62+
"name": "title",
63+
"message": "Commit title (short, first letter capital)",
64+
"validate": self._validate_title,
65+
},
66+
{
67+
"type": "input",
68+
"name": "how",
69+
"message": ("Describe what you changed and how it addresses the issue"),
70+
"validate": lambda v: (
71+
True if v.strip() else "Commit body cannot be empty."
72+
),
73+
},
74+
]
75+
76+
def message(self, answers):
77+
prefix_value = answers["change_type"]
78+
prefix = f"[{prefix_value}]"
79+
title = answers["title"].strip()
80+
body = answers["how"].strip()
81+
# Extract issue number from title
82+
match = _TITLE_ISSUE_RE.search(title)
83+
if not match:
84+
raise ValueError(
85+
"Commit title must end with an issue reference like #<issue_number>."
86+
)
87+
issue_number = match.group(1)
88+
return f"{prefix} {title}\n\n" f"{body}\n\n" f"Fixes #{issue_number}"
89+
90+
def validate_commit_message(
91+
self,
92+
*,
93+
commit_msg: str,
94+
pattern: re.Pattern[str],
95+
allow_abort: bool,
96+
allowed_prefixes: list[str],
97+
max_msg_length: int | None,
98+
commit_hash: str,
99+
) -> ValidationResult:
100+
"""Validate commit message and return user-friendly errors."""
101+
if not commit_msg:
102+
return ValidationResult(
103+
allow_abort, [] if allow_abort else ["commit message is empty"]
104+
)
105+
# First check if it matches the pattern
106+
match_result = pattern.fullmatch(commit_msg)
107+
if not match_result:
108+
return ValidationResult(False, [self.ERROR_TEMPLATE])
109+
# Then verify it starts with an allowed prefix or is a merge commit
110+
# Use self.ALLOWED_PREFIXES for our custom prefixes
111+
# Allow compound prefixes like [tests:fix] as long as first part is allowed
112+
if commit_msg.startswith("Merge "):
113+
pass # Merge commits are allowed
114+
elif not any(
115+
re.match(rf"\[{prefix}([!/:]|\])", commit_msg)
116+
for prefix in self.ALLOWED_PREFIXES
117+
):
118+
return ValidationResult(False, [self.ERROR_TEMPLATE])
119+
# Check message length limit
120+
if max_msg_length is not None and max_msg_length > 0:
121+
msg_len = len(commit_msg.partition("\n")[0].strip())
122+
if msg_len > max_msg_length:
123+
return ValidationResult(
124+
False,
125+
[
126+
f"commit message length exceeds the limit ({max_msg_length} chars)",
127+
],
128+
)
129+
return ValidationResult(True, [])
130+
131+
def format_error_message(self, message: str) -> str:
132+
return self.ERROR_TEMPLATE
133+
134+
def example(self) -> str:
135+
return (
136+
"[feature] Add commit convention enforcement #110\n\n"
137+
"Introduce a Commitizen-based commit workflow to standardize\n"
138+
"commit messages across the OpenWISP project.\n\n"
139+
"Fixes #110"
140+
)
141+
142+
def schema(self) -> str:
143+
return "[<type>] <Title>"
144+
145+
def schema_pattern(self) -> str:
146+
# Allow merge commits (starting with "Merge") or regular commits with prefix
147+
# Using \Z instead of $ to truly anchor to end-of-string
148+
# Split into two alternatives: merge commits and regular commits
149+
merge_pattern = r"Merge .*"
150+
# Regular commits: header with optional footer section
151+
# Footer section: blank line + optional body + "Fixes #<issue>"
152+
# Body is optional (.* allows empty) and there's no second blank line required
153+
regular_pattern = (
154+
r"\[[a-z0-9!/:-]+\] [A-Z][^\n]*( #(?P<issue>\d+))?"
155+
r"$(\n\n(.*\n)?(?:Close|Closes|Closed|Fix|Fixes|Fixed"
156+
r"|Resolve|Resolves|Resolved|Related to) #(?P=issue)\n?)?"
157+
)
158+
return rf"(?sm)^(?:{merge_pattern}|{regular_pattern})\Z"
159+
160+
def info(self) -> str:
161+
prefixes_list = "\n".join(f" - {prefix}" for prefix in self.ALLOWED_PREFIXES)
162+
return (
163+
"OpenWISP Commit Convention\n\n"
164+
"Commit messages must follow this structure:\n\n"
165+
" [type] Capitalized title #<issue_number>\n\n"
166+
" <description>\n\n"
167+
" Fixes #<issue_number>\n\n"
168+
f"Allowed commit prefixes:\n\n{prefixes_list}\n\n"
169+
"If in doubt, use chores."
170+
)
171+
172+
173+
__all__ = ["OpenWispCommitizen"]

0 commit comments

Comments
 (0)