Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/actions/bot-changelog-generator/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,15 @@ def build_prompt(


def get_openwisp_commitizen():
"""Load the local OpenWISP Commitizen plugin lazily."""
"""Load the local OpenWISP Commitizen plugin lazily.

``commitizen.cz`` is imported first so its plugin auto-discovery
completes before ``openwisp_utils.releaser.commitizen`` enters the
import stack. Without this ordering, discovery re-imports our
module mid-initialization and raises ``AttributeError`` on
``OpenWispCommitizen`` (issue #669).
"""
import commitizen.cz # noqa: F401 — load order matters; see docstring
from openwisp_utils.releaser.commitizen import OpenWispCommitizen

return OpenWispCommitizen(_CommitizenConfig())
Expand Down
46 changes: 46 additions & 0 deletions .github/actions/bot-changelog-generator/test_generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
get_changelog_validation_errors,
get_env_or_exit,
get_linked_issues,
get_openwisp_commitizen,
get_pr_commits,
get_pr_details,
get_pr_diff,
Expand Down Expand Up @@ -524,6 +525,51 @@ def test_raises_on_request_error(self, mock_request):
self.assertIn("Failed to post comment", str(context.exception))


class TestGetOpenwispCommitizen(unittest.TestCase):
"""Regression tests for get_openwisp_commitizen (issue #669)."""

_MODULE_PREFIXES = ("commitizen", "openwisp_utils.releaser.commitizen")

def setUp(self):
"""Evict cached commitizen modules to simulate a fresh process.

The circular import only triggers on the first load of
``commitizen.cz``; if any earlier code path cached it, the bug
is hidden and the test would falsely pass.
"""
self._saved_modules = {
name: module
for name, module in sys.modules.items()
if name == "commitizen"
or name.startswith("commitizen.")
or name == "openwisp_utils.releaser.commitizen"
}
for name in self._saved_modules:
del sys.modules[name]

def tearDown(self):
"""Restore the original module cache.

Avoids leaving partial modules in ``sys.modules`` on failure
and keeps the test isolated from any later test that may rely
on the original commitizen module identity.
"""
for name in list(sys.modules):
if (
name == "commitizen"
or name.startswith("commitizen.")
or name == "openwisp_utils.releaser.commitizen"
):
del sys.modules[name]
sys.modules.update(self._saved_modules)

def test_loads_plugin_without_circular_import(self):
plugin = get_openwisp_commitizen()
self.assertEqual(plugin.__class__.__name__, "OpenWispCommitizen")
self.assertTrue(hasattr(plugin, "schema_pattern"))
self.assertTrue(hasattr(plugin, "validate_commit_message"))


class TestValidateChangelogOutput(unittest.TestCase):
"""Tests for validate_changelog_output function."""

Expand Down
Loading