Repository: RedHatQE/openshift-python-wrapper
Pull request #2356: ci: add script to generate changelog for new release
Author: login: myakove
Labels: name: size/L
name: verified
name: branch-main
name: can-be-merged
name: approved-rnetser
Created: 2025-03-20T20:10:21Z | Merged: 2025-03-25T10:45:47Z

Script to generate changelog with sections to use in release-it

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Introduced an automated changelog generator that produces structured release notes between versions.
- **Chores**
	- Updated release configuration to support the new changelog process.
	- Removed an obsolete cluster management script.
	- Added `subprocess` to the exclusion list for linting checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.flake8 b/.flake8
index 614ff503e9..8d62edb9ac 100644
--- a/.flake8
+++ b/.flake8
@@ -59,6 +59,7 @@ fcn_exclude_functions =
     ast,
     filecmp,
     datetime,
+    subprocess,
 
 enable-extensions =
     FCN,
diff --git a/.release-it.json b/.release-it.json
index c43bbd323e..baaa392356 100644
--- a/.release-it.json
+++ b/.release-it.json
@@ -19,7 +19,7 @@
     "push": true,
     "pushArgs": ["--follow-tags"],
     "pushRepo": "",
-    "changelog": "git log --pretty=format:\"* %s (%h) by %an on %as\" ${from}...${to}"
+    "changelog": "uv run scripts/generate_changelog.py ${from} ${to}"
   },
   "github": {
     "release": true,
diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py
new file mode 100644
index 0000000000..31902492f1
--- /dev/null
+++ b/scripts/generate_changelog.py
@@ -0,0 +1,157 @@
+import json
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def json_line(line: str) -> dict:
+    """
+    Format str line to str that can be parsed with json.
+
+    In case line is not formatted for json for example:
+    '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    """
+    try:
+        return json.loads(line)
+    except json.JSONDecodeError:
+        # split line like by `,`
+        # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+        line_split = line.split(",")
+
+        # Pop and save `title key` and `title body` from '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"'
+        title_key, title_body = line_split.pop(0).split(":", 1)
+
+        if title_body.count('"') > 2:
+            # reconstruct the `title_body` without the extra `"`
+            # "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"'
+            # replace all `"` with empty char and add `"` char to the beginning and the end of the string
+            stripted_body = title_body.replace('"', "")
+            title_body = f'"{stripted_body}"'
+
+            line_split.append(f"{title_key}: {title_body.strip()}")
+            line = ",".join(line_split)
+
+        return json.loads(line)
+
+
+def execute_git_log(from_tag: str, to_tag: str) -> str:
+    """Executes git log and returns the output, or raises an exception on error."""
+    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+
+    try:
+        command = f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"
+        proc = subprocess.run(
+            shlex.split(command), stdout=subprocess.PIPE, text=True, check=True
+        )  # Use check=True to raise an exception for non-zero return codes
+        return proc.stdout
+    except subprocess.CalledProcessError as ex:
+        print(f"Error executing git log: {ex}")
+        sys.exit(1)
+    except FileNotFoundError:
+        print("Error: git not found.  Please ensure git is installed and in your PATH.")
+        sys.exit(1)
+
+
+def parse_commit_line(line: str) -> dict:
+    """Parses a single JSON formatted git log line."""
+    try:
+        return json_line(line=line)
+    except json.decoder.JSONDecodeError as ex:
+        print(f"Error parsing JSON: {line} - {ex}")
+        return {}
+
+
+def categorize_commit(commit: dict, title_to_type_map: dict, default_category: str = "Other Changes:") -> str:
+    """Categorizes a commit based on its title prefix."""
+    if not commit or "title" not in commit:
+        return default_category
+    title = commit["title"]
+    try:
+        prefix = title.split(":", 1)[0].lower()  # Extract the prefix before the first colon
+        return title_to_type_map.get(prefix, default_category)
+    except IndexError:
+        return default_category
+
+
+def format_changelog_entry(change: dict, section: str) -> str:
+    """Formats a single changelog entry."""
+    title = change["title"].split(":", 1)[1] if section != "Other Changes:" else change["title"]
+    return f"- {title} ({change['commit']}) by {change['author']} on {change['date']}\n"
+
+
+def main(from_tag: str, to_tag: str) -> str:
+    title_to_type_map: dict[str, str] = {
+        "ci": "CI:",
+        "docs": "Docs:",
+        "feat": "New Feature:",
+        "fix": "Bugfixes:",
+        "refactor": "Refactor:",
+        "test": "Tests:",
+        "release": "New Release:",
+        "cherrypicked": "Cherry Pick:",
+        "merge": "Merge:",
+    }
+    changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("New Feature:", []),
+        ("Bugfixes:", []),
+        ("CI:", []),
+        ("New Release:", []),
+        ("Docs:", []),
+        ("Refactor:", []),
+        ("Tests:", []),
+        ("Other Changes:", []),
+        ("Cherry Pick:", []),
+        ("Merge:", []),
+    ])
+
+    changelog: str = "## What's Changed\n"
+
+    res = execute_git_log(from_tag=from_tag, to_tag=to_tag)
+
+    for line in res.splitlines():
+        commit = parse_commit_line(line=line)
+        if commit:
+            category = categorize_commit(commit=commit, title_to_type_map=title_to_type_map)
+            changelog_dict[category].append(commit)
+
+    for section, changes in changelog_dict.items():
+        if not changes:
+            continue
+
+        changelog += f"#### {section}\n"
+        for change in changes:
+            changelog += format_changelog_entry(change, section)
+        changelog += "\n"
+
+    changelog += (
+        f"**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
+    )
+
+    return changelog
+
+
+if __name__ == "__main__":
+    """
+    Generate a changelog between two Git tags, formatted as markdown.
+
+    This script parses Git commit logs between two specified tags and categorizes them
+    by commit type (feat, fix, ci, etc.). It formats the output as a markdown document
+    with sections for different types of changes, intended for use with release-it.
+
+    Each commit is expected to follow the conventional commit format:
+    <type>: <description>
+
+    where <type> is one of: feat, fix, docs, style, refactor, test, chore, etc.
+    Commits that don't follow this format are categorized under "Other Changes".
+
+    Generate a changelog between two tags, output as markdown
+
+    Usage: python generate-changelog.py <from_tag> <to_tag>
+    """
+    if len(sys.argv) != 3:
+        print("Usage: python generate-changelog.py <from_tag> <to_tag>")
+        sys.exit(1)
+
+    print(main(from_tag=sys.argv[1], to_tag=sys.argv[2]))
diff --git a/scripts/k3d-runner.sh b/scripts/k3d-runner.sh
deleted file mode 100755
index 5af0217c19..0000000000
--- a/scripts/k3d-runner.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-set -e
-
-k3d_installer="/tmp/k3d.sh"
-action="$1"
-
-if [ "$action" = "start" ]; then
-  echo "Installing k3d..."
-  curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh >"$k3d_installer"
-  chmod +x "$k3d_installer"
-  "$k3d_installer"
-  rm -rf "$k3d_installer"
-  k3d cluster create opw-local-cluster
-
-elif [ "$action" = "stop" ]; then
-  k3d cluster delete opw-local-cluster
-fi


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (3)</summary><blockquote>

`33-38`: **Consider adding error handling for subprocess execution.**

The script runs a git command without handling potential errors if the command fails, which could occur if invalid tags are provided or if there are git repository issues.


```diff
- res = subprocess.run(
-     shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"), stdout=subprocess.PIPE, text=True
- ).stdout
+ try:
+     proc = subprocess.run(
+         shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
+         stdout=subprocess.PIPE,
+         stderr=subprocess.PIPE,
+         text=True,
+         check=True
+     )
+     res = proc.stdout
+ except subprocess.CalledProcessError as e:
+     print(f"Error executing git command: {e.stderr}", file=sys.stderr)
+     sys.exit(1)
```

---

`39-47`: **Improve exception handling for more specific error messages.**

The current exception handling is too broad and silently categorizes any parsing errors as "Other Changes". Consider handling specific exceptions and providing more informative error messages.


```diff
- try:
-     _map = title_to_type_map[_json_line["title"].split(":", 1)[0]]
-     changelog_dict[_map].append(_json_line)
- except Exception:
-     changelog_dict["Other Changes:"].append(_json_line)
+ try:
+     prefix = _json_line["title"].split(":", 1)[0]
+     if prefix in title_to_type_map:
+         _map = title_to_type_map[prefix]
+         changelog_dict[_map].append(_json_line)
+     else:
+         changelog_dict["Other Changes:"].append(_json_line)
+ except IndexError:
+     # Handle case when title doesn't have a colon
+     changelog_dict["Other Changes:"].append(_json_line)
+ except KeyError as e:
+     # Handle case when JSON is missing expected keys
+     print(f"Warning: Missing key in commit data: {e}", file=sys.stderr)
+     changelog_dict["Other Changes:"].append(_json_line)
```

---

`62-65`: **Add docstring and argument validation to improve script usability.**

Consider adding a docstring explaining the purpose of the script and validating the command-line arguments to handle errors gracefully.


```diff
 if __name__ == "__main__":
+    """
+    Generate a changelog between two git tags.
+    
+    Usage: python generate-changelog.py <from_tag> <to_tag>
+    """
+    if len(sys.argv) != 3:
+        print("Usage: python generate-changelog.py <from_tag> <to_tag>", file=sys.stderr)
+        sys.exit(1)
+        
     from_tag, to_tag = sys.argv[1], sys.argv[2]
     generated_changelog = main(from_tag=from_tag, to_tag=to_tag)
     print(generated_changelog)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 808952283b7e9479d58ac21eecc5b8afd47ca2ad and 7abff98293ac3f5244f6fdba14541ab4fd37206d.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `.flake8` (1 hunks)
* `.release-it.json` (1 hunks)
* `scripts/generate-changelog.py` (1 hunks)
* `scripts/k3d-runner.sh` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* scripts/k3d-runner.sh

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: python-module-install
* GitHub Check: tox
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>.flake8 (1)</summary>

`62-62`: **LGTM: Adding subprocess to excluded functions.**

This change appropriately adds the `subprocess` module to the list of excluded functions for Flake8, which is necessary to support the new changelog generation script.

</details>
<details>
<summary>.release-it.json (1)</summary>

`22-22`: **Updated changelog generation to use custom script.**

The change replaces the direct git command with a call to a new Python script that will format the changelog in a more structured way. This is a good improvement as it allows for better organization of the changelog content.

</details>
<details>
<summary>scripts/generate-changelog.py (3)</summary>

`1-6`: **LGTM: Appropriate imports for the changelog generation script.**

The script imports necessary modules for JSON parsing, subprocess execution, and command-line arguments handling.

---

`8-32`: **Good organization of changelog sections with OrderedDict.**

The implementation uses OrderedDict to maintain a consistent order of sections in the changelog, which is a good practice. The mapping of commit prefixes to section titles is also well-structured.

---

`48-58`: **LGTM: Clean formatting of changelog sections.**

The section formatting logic is clean and produces a well-structured changelog with proper indentation and spacing.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-20T20:13:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (7)</summary><blockquote>

`14-14`: **Fix typo in section name.**

There's a typo in the section name "Bugfixs:" which should be "Bugfixes:".

```diff
-        "fix": "Bugfixs:",
+        "fix": "Bugfixes:",
```

---

`17-17`: **Inconsistent naming style in title_to_type_map.**

"CherryPicked" uses camelCase while other keys use lowercase, which is inconsistent with the project's coding style.

```diff
-        "CherryPicked": "Cherry Pick:",
+        "cherrypicked": "Cherry Pick:",
```
Alternatively, if the inconsistency is intentional because Git uses this exact term in commit messages, consider adding a comment explaining this exception.

---

`22-22`: **Update section name to match the title_to_type_map correction.**

Make sure to update this section name to match the corrected "Bugfixes:" from the title_to_type_map.

```diff
-        ("Bugfixs:", []),
+        ("Bugfixes:", []),
```

---

`34-34`: **Consider using triple quotes for the format string.**

Using triple quotes for format strings that contain quotes can improve readability and avoid escaping issues.

```diff
-    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+    _format: str = '''{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'''
```

---

`54-58`: **Improve error handling specificity.**

The current exception handling is too broad and might hide unexpected errors. Consider catching specific exceptions like KeyError for the dictionary lookup and IndexError for the split operation.

```diff
-        try:
-            _map = title_to_type_map[_json_line["title"].split(":", 1)[0]]
-            changelog_dict[_map].append(_json_line)
-        except Exception:
-            changelog_dict["Other Changes:"].append(_json_line)
+        try:
+            prefix = _json_line["title"].split(":", 1)[0]
+            if prefix in title_to_type_map:
+                _map = title_to_type_map[prefix]
+                changelog_dict[_map].append(_json_line)
+            else:
+                changelog_dict["Other Changes:"].append(_json_line)
+        except IndexError:
+            # Handle case where title doesn't contain a colon
+            changelog_dict["Other Changes:"].append(_json_line)
```

---

`71-73`: **Consider making the GitHub URL configurable.**

The GitHub repository URL is hardcoded in the script. Consider making it configurable, either through a constant at the top of the file or through an environment variable.

```diff
+# At the top of the file
+GITHUB_REPO_URL = "https://github.com/RedHatQE/openshift-python-wrapper"

# Then in the code
-    changelog += (
-        f"**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
-    )
+    changelog += (
+        f"**Full Changelog**: {GITHUB_REPO_URL}/compare/{from_tag}...{to_tag}"
+    )
```

---

`36-49`: **Add validation for Git tags.**

The script doesn't validate if the provided tags exist in the repository. This could lead to confusing errors if invalid tags are provided.


Consider adding validation for the input tags before running the git log command:

```diff
+    # Validate tags exist
+    try:
+        for tag in [from_tag, to_tag]:
+            proc = subprocess.run(
+                shlex.split(f"git tag --list {tag}"),
+                stdout=subprocess.PIPE,
+                text=True,
+            )
+            if not proc.stdout.strip():
+                print(f"Error: Tag '{tag}' does not exist in the repository")
+                sys.exit(1)
+    except Exception as ex:
+        print(f"Error validating git tags: {ex}")
+        sys.exit(1)
+
     try:
         proc = subprocess.run(
             shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4d8351e83657ea6bf0181d479313f48c9ab1bcd5 and 4af836300afd2a77bbd0c207b2db84db00c5be4d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate-changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: tox
* GitHub Check: python-module-install
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (2)</summary>

`66-66`: **Handle split operation more safely.**

The current approach might cause issues if the title doesn't contain a colon, especially for items categorized as "Other Changes". Consider a more robust approach.

```diff
-            _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
+            _title = (_change["title"].split(":", 1)[1].strip() if ":" in _change["title"] and section != "Other Changes:" 
+                      else _change["title"])
```

---

`8-76`: **LGTM - Overall good script structure and implementation.**

The overall script is well-structured with:
- Clear function definition with type hints
- Appropriate error handling for subprocess execution
- Well-formatted changelog output
- Good organization of changes into categories

The script effectively meets the purpose described in the PR objectives to generate a changelog for the release-it tool.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T08:26:08Z
user: login: rnetser
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T08:33:59Z
user: login: myakove
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T08:50:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (8)</summary><blockquote>

`9-10`: **Add a proper docstring to the utility function.**

The `remove_at_index` function lacks a descriptive docstring explaining its purpose, parameters, and return value. This would improve maintainability and aid users who might need to understand or modify this function in the future.

```diff
def remove_at_index(chr_index: int, string_: str) -> str:
+    """
+    Remove a character at the specified index from a string.
+    
+    Args:
+        chr_index: The index of the character to remove
+        string_: The original string
+        
+    Returns:
+        The string with the character at chr_index removed
+    """
    return string_[:chr_index] + string_[chr_index + 1 :]
```

---

`13-36`: **Improve the robustness of JSON string handling.**

The current implementation relies on string manipulation to handle nested quotes, which can be fragile. Consider using proper JSON escaping methods for more robust handling of complex strings.

Here's an alternative approach that might be more robust:

```diff
def format_line_for_json(line: str) -> str:
    """
    Format a git log line to be valid JSON by handling nested quotes.
    
    Args:
        line: A line from git log output
        
    Returns:
        A properly formatted JSON string
    """
    try:
        # First attempt to parse as-is
        json.loads(line)
        return line
    except json.JSONDecodeError:
        # If it fails, handle the nested quotes
-    # In case line is not formatted for json for example:
-    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
-    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
-    line_split = line.split(",")
-    title_key = line_split[0].split(":")[0]
-    title_split = line_split.pop(0).split(":", 1)[-1]
-
-    if title_split.count('"') > 2:
-        # Find all `"` indexes
-        quote_match = [match.start() for match in re.finditer('"', title_split)]
-
-        # Remove first and last matched `"`
-        quote_to_remove = quote_match[1:-1]
-
-        for idx, _index in enumerate(quote_to_remove):
-            # send _index -1 if not the first iter since the title_split changed (removed one character on first iter)
-            _index_to_send = _index if idx == 0 else _index - 1
-            title_split = remove_at_index(chr_index=_index_to_send, string_=title_split)
-
-        line_split.insert(0, f"{title_key}: {title_split.strip()}")
-        line = ",".join(line_split)
+        # Extract the title part that's causing problems
+        match = re.match(r'(\{"title": ")(.+)(",.+)', line)
+        if match:
+            prefix, title, suffix = match.groups()
+            # Escape any quotes in the title
+            escaped_title = title.replace('"', '\\"')
+            # Reconstruct the line
+            line = f'{prefix}{escaped_title}{suffix}'
+            return line
+        return line

    return line
```

Note: This is a suggestion, and the current implementation may be sufficient if it handles all the expected patterns in your commit messages.

---

`86-96`: **Add logging for unexpected commit formats.**

When a commit message doesn't follow the expected format (prefix:description), it's silently categorized under "Other Changes" without any indication that this might be unexpected. Consider adding debug logging to help identify and fix inconsistent commit formats.

```diff
import json
import re
import shlex
import subprocess
import sys
+import logging
from collections import OrderedDict

+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

# ... rest of the code ...

try:
    prefix = _json_line["title"].split(":", 1)[0]
    if prefix.lower() in title_to_type_map:
        _map = title_to_type_map[prefix]
        changelog_dict[_map].append(_json_line)
    else:
+        logging.debug(f"Commit with non-standard prefix: {prefix} - {_json_line['title']}")
        changelog_dict["Other Changes:"].append(_json_line)
except IndexError:
+    logging.debug(f"Commit without expected format: {_json_line['title']}")
    changelog_dict["Other Changes:"].append(_json_line)
```

---

`40-50`: **Consider making title mappings configurable.**

Currently, the mappings from commit prefixes to changelog sections are hardcoded. You could make this more maintainable by allowing configuration from an external file or environment variables.

```diff
def main(from_tag: str, to_tag: str) -> str:
-    title_to_type_map: dict[str, str] = {
-        "ci": "CI:",
-        "docs": "Docs:",
-        "feat": "New Feature:",
-        "fix": "Bugfixes:",
-        "refactor": "Refactor:",
-        "test": "Tests:",
-        "release": "New Release:",
-        "cherrypicked": "Cherry Pick:",
-        "merge": "Merge:",
-    }
+    # First check if there's a custom config file
+    title_to_type_map: dict[str, str] = {}
+    config_file = os.environ.get("CHANGELOG_CONFIG", ".changelog-config.json")
+    
+    try:
+        if os.path.exists(config_file):
+            with open(config_file, "r") as f:
+                title_to_type_map = json.load(f)
+    except Exception as ex:
+        print(f"Warning: Failed to load custom changelog config: {ex}")
+        
+    # Use default mappings if no custom config or as fallback
+    if not title_to_type_map:
+        title_to_type_map = {
+            "ci": "CI:",
+            "docs": "Docs:",
+            "feat": "New Feature:",
+            "fix": "Bugfixes:",
+            "refactor": "Refactor:",
+            "test": "Tests:",
+            "release": "New Release:",
+            "cherrypicked": "Cherry Pick:",
+            "merge": "Merge:",
+        }
```

You would need to add `import os` at the top of the file for this implementation.

---

`102-104`: **Improve formatting of changelog entries.**

The current formatting could be enhanced to make it more readable and consistent with common changelog formats.

```diff
for _change in _changelogs:
    _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
-    changelog += f"-{_title} ({_change['commit']}) by {_change['author']} on {_change['date']}\n"
+    # Add a space after the hyphen and clean up title whitespace
+    changelog += f"- {_title.strip()} ([{_change['commit']}](https://github.com/RedHatQE/openshift-python-wrapper/commit/{_change['commit']})) by {_change['author']} on {_change['date']}\n"
```

This change:
1. Adds a space after the hyphen for better readability
2. Trims whitespace from the title
3. Makes the commit hash a clickable link to the specific commit

---

`115-125`: **Add a more comprehensive docstring.**

The current docstring is good but could be enhanced to provide more details about what the script does and how it formats the output.

```diff
if __name__ == "__main__":
    """
-    Generate a changelog between two tags, output as markdown
+    Generate a changelog between two Git tags, formatted as markdown.
+    
+    This script parses Git commit logs between two specified tags and categorizes them
+    by commit type (feat, fix, ci, etc.). It formats the output as a markdown document
+    with sections for different types of changes, intended for use with release-it.
+    
+    Each commit is expected to follow the conventional commit format:
+    <type>: <description>
+    
+    where <type> is one of: feat, fix, docs, style, refactor, test, chore, etc.
+    Commits that don't follow this format are categorized under "Other Changes".

    Usage: python generate-changelog.py <from_tag> <to_tag>
    """
```

---

`64-64`: **Consider making section headings configurable.**

The section heading level is currently hardcoded as "##" for the main heading. Consider making this configurable to allow for different heading levels in different contexts.

```diff
-    changelog: str = "## What's Changed\n"
+    heading_level = os.environ.get("CHANGELOG_HEADING_LEVEL", "##")
+    changelog: str = f"{heading_level} What's Changed\n"
```

You would need to add `import os` at the top of the file for this implementation if not already added.

---

`108-110`: **Make the GitHub repository URL configurable.**

The GitHub repository URL is currently hardcoded. It would be more maintainable to make this configurable, especially if the script might be used for different repositories.

```diff
+    # Get repository URL from git remote or from environment variable
+    repo_url = os.environ.get("GITHUB_REPO_URL", "")
+    if not repo_url:
+        try:
+            # Try to extract from git remote
+            proc = subprocess.run(
+                ["git", "remote", "get-url", "origin"],
+                stdout=subprocess.PIPE,
+                text=True,
+                check=True
+            )
+            remote_url = proc.stdout.strip()
+            # Convert SSH to HTTPS format if needed
+            if remote_url.startswith("git@github.com:"):
+                repo_path = remote_url.split("git@github.com:")[1].replace(".git", "")
+                repo_url = f"https://github.com/{repo_path}"
+            elif remote_url.startswith("https://github.com/"):
+                repo_url = remote_url.replace(".git", "")
+        except Exception:
+            # Default fallback
+            repo_url = "https://github.com/RedHatQE/openshift-python-wrapper"
+
-    changelog += (
-        f"**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
-    )
+    changelog += f"**Full Changelog**: {repo_url}/compare/{from_tag}...{to_tag}"
```

You would need to add `import os` at the top of the file for this implementation if not already added.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4af836300afd2a77bbd0c207b2db84db00c5be4d and 842c50cb66cfb27d5d55a1a4051729b9c121e245.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate-changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: python-module-install
* GitHub Check: tox
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (1)</summary>

`68-73`: **Consider handling potential command injection risks.**

While using `shlex.split` helps prevent shell injection, it's still good practice to validate tag names to ensure they only contain expected characters.

```shell
#!/bin/bash
# Check if tags look like valid git tags with some basic pattern matching
# Most git tags follow semantic versioning (like v1.2.3) or simple names

echo "Checking for tag naming patterns that might indicate injection attempts"
echo "Valid git tags usually match patterns like: v1.2.3, release-1.2, feature-xyz"

# Example of what a valid git tag reference might look like
echo "Example of valid git tag references:"
git tag -l | head -5

# Look for suspicious characters that could be used for command injection
echo "Looking for suspicious characters in tag references that could indicate command injection attempts:"
git tag -l | grep -E "[;&|]" || echo "No suspicious tags found."

# Check for validation in similar projects
echo "Checking if other changelog generation tools perform tag validation:"
fd --type file -e py . | xargs grep -l "tag validation" | head -5 || echo "No explicit tag validation examples found in Python files."
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T10:33:07Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (3)</summary><blockquote>

`67-80`: **Consider validating git tag inputs for security.**

The script directly uses tag parameters in the git command without validation, which could potentially lead to command injection if malicious input is provided.

```diff
+ def validate_git_tag(tag: str) -> bool:
+     """Validate that a string is a valid git tag."""
+     # Git tags cannot contain spaces or control characters
+     if not tag or re.search(r'[\s\x00-\x1F\x7F]', tag):
+         return False
+     # Simple additional check to prevent command injection
+     if ";" in tag or "&" in tag or "|" in tag or ">" in tag or "<" in tag:
+         return False
+     return True

  def main(from_tag: str, to_tag: str) -> str:
+     if not validate_git_tag(from_tag) or not validate_git_tag(to_tag):
+         print("Error: Invalid git tag format")
+         sys.exit(1)
      title_to_type_map: dict[str, str] = {
```

---

`67-72`: **Add check for git availability.**

The script assumes git is installed and the current directory is a git repository. Consider adding a check at the beginning.

```diff
  try:
+     # Check if git is available and we're in a git repository
+     check_proc = subprocess.run(
+         ["git", "rev-parse", "--is-inside-work-tree"],
+         stdout=subprocess.PIPE,
+         stderr=subprocess.PIPE,
+         text=True,
+     )
+     if check_proc.returncode != 0:
+         print("Error: Not a git repository or git is not installed")
+         sys.exit(1)
+
      proc = subprocess.run(
          shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
          stdout=subprocess.PIPE,
          text=True,
      )
```

---

`82-89`: **Improve handling of JSON parse errors.**

Consider adding more context when a JSON parse error occurs, such as showing the specific error message from the exception.

```diff
      try:
          _json_line = json.loads(line)
      except json.decoder.JSONDecodeError as e:
-         print(f"Error parsing json line: {line}")
+         print(f"Error parsing json line: {line}\nError details: {str(e)}")
          sys.exit(1)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 842c50cb66cfb27d5d55a1a4051729b9c121e245 and cf8da4d49bbb8763474ae67641c4efad161ed146.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate-changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: tox
* GitHub Check: python-module-install
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (8)</summary>

`1-7`: **Well-structured imports and good use of OrderedDict.**

The script uses appropriate modules and OrderedDict to ensure the changelog sections appear in a specific order, which is important for readability and consistency.

---

`9-10`: **Simple and effective utility function.**

The `remove_at_index` function is well-typed and handles its single responsibility clearly.

---

`13-36`: **Good documentation and handling of complex edge cases.**

The function includes detailed comments explaining the purpose and providing an example of the complex case it's handling. The logic for handling embedded quotes in commit messages is thorough.

---

`39-62`: **Well-structured mapping for changelog categorization.**

The use of typed dictionaries and OrderedDict for maintaining section order is a good approach. This ensures the changelog has a consistent and logical structure.

---

`68-72`: **Consider using run_command to match project patterns.**

There was a previous comment about using `run_command`, but I understand this is intended as a standalone script that shouldn't depend on other modules.

---

`82-101`: **Robust parsing and categorization of commits.**

The code correctly handles the parsing of JSON, categorizes commits based on prefixes, and provides appropriate error handling for JSON parsing errors.

---

`102-115`: **Clean formatting of the changelog output.**

The formatting logic creates a well-structured Markdown changelog with proper sections and entries.

---

`120-130`: **Well-documented CLI with proper argument checking.**

The script provides clear usage instructions and checks for the correct number of arguments.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T10:35:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (7)</summary><blockquote>

`23-46`: **Add docstring for `format_line_for_json` function.**

While the function has good inline comments explaining how it works, adding a proper docstring (similar to `remove_at_index`) would improve consistency and make the purpose and behavior of this function clearer.


```diff
def format_line_for_json(line: str) -> str:
+    """
+    Format a line to be valid JSON by handling nested quotes.
+
+    Args:
+        line: The string line potentially containing nested quotes
+
+    Returns:
+        Properly formatted string for JSON parsing
+    """
    # In case line is not formatted for json for example:
```

---

`31-45`: **Consider simplifying the quote removal logic.**

The current implementation for handling nested quotes is quite complex with manual index tracking. Consider using a regular expression replacement approach which might be more readable and maintainable.


```diff
-    if title_split.count('"') > 2:
-        # Find all `"` indexes
-        quote_match = [match.start() for match in re.finditer('"', title_split)]
-
-        # Remove first and last matched `"`
-        quote_to_remove = quote_match[1:-1]
-
-        for idx, _index in enumerate(quote_to_remove):
-            # send _index -1 if not the first iter since the title_split changed (removed one character on first iter)
-            _index_to_send = _index if idx == 0 else _index - 1
-            title_split = remove_at_index(chr_index=_index_to_send, string_=title_split)
+    if title_split.count('"') > 2:
+        # Extract content between first and last quotes
+        first_quote = title_split.find('"')
+        last_quote = title_split.rfind('"')
+        
+        if first_quote != -1 and last_quote != -1 and first_quote != last_quote:
+            # Get content between quotes and replace internal quotes with escaped ones
+            inner_content = title_split[first_quote+1:last_quote]
+            inner_content = inner_content.replace('"', '\\"')
+            
+            # Reconstruct the title
+            title_split = title_split[:first_quote+1] + inner_content + title_split[last_quote:]
```

---

`49-73`: **Add docstring to the `main` function.**

The `main` function lacks a docstring that would explain its purpose, parameters, and return value. This would be consistent with the documentation style of other functions in the file.


```diff
def main(from_tag: str, to_tag: str) -> str:
+    """
+    Generate a changelog between two Git tags.
+
+    Args:
+        from_tag: The starting Git tag
+        to_tag: The ending Git tag
+
+    Returns:
+        A formatted changelog as a string in Markdown format
+    """
    title_to_type_map: dict[str, str] = {
```

---

`77-91`: **Extract git command execution into a helper function.**

The subprocess execution logic could be extracted into a helper function to improve readability and facilitate reuse if needed elsewhere in the script.


```diff
+def execute_git_command(command: str) -> str:
+    """
+    Execute a git command and return its output.
+    
+    Args:
+        command: The git command to execute
+        
+    Returns:
+        The command output as string
+        
+    Raises:
+        SystemExit: If the command execution fails
+    """
+    try:
+        proc = subprocess.run(
+            shlex.split(command),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=True,
+        )
+        if proc.returncode != 0:
+            print(f"Error executing git command: {proc.stderr}")
+            sys.exit(1)
+
+        return proc.stdout
+    except Exception as ex:
+        print(f"Error executing git command: {ex}")
+        sys.exit(1)
+
+
 try:
-    proc = subprocess.run(
-        shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
-        stdout=subprocess.PIPE,
-        text=True,
-    )
-    if proc.returncode != 0:
-        print("Error executing git log command")
-        sys.exit(1)
-
-    res = proc.stdout
+    res = execute_git_command(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}")
 except Exception as ex:
     print(f"Error executing git log command: {ex}")
     sys.exit(1)
```

---

`77-91`: **Improve error messages with specific stderr output.**

The current error message for git command failure just indicates that an error occurred without showing the actual error output from git. Including stderr would help diagnose issues more easily.


```diff
 try:
     proc = subprocess.run(
         shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
         stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
         text=True,
     )
     if proc.returncode != 0:
-        print("Error executing git log command")
+        print(f"Error executing git log command: {proc.stderr}")
         sys.exit(1)

     res = proc.stdout
```

---

`130-152`: **Consider using argparse for better CLI handling.**

While the current argument parsing is simple, using `argparse` would provide better help messages, validation, and extensibility if more options are added in the future.


```diff
 if __name__ == "__main__":
     """
     Generate a changelog between two Git tags, formatted as markdown.
     ...
     """
-    if len(sys.argv) != 3:
-        print("Usage: python generate-changelog.py <from_tag> <to_tag>")
-        sys.exit(1)
-
-    print(main(from_tag=sys.argv[1], to_tag=sys.argv[2]))
+    import argparse
+    
+    parser = argparse.ArgumentParser(description="Generate a changelog between two Git tags")
+    parser.add_argument("from_tag", help="Starting Git tag")
+    parser.add_argument("to_tag", help="Ending Git tag")
+    
+    args = parser.parse_args()
+    
+    print(main(from_tag=args.from_tag, to_tag=args.to_tag))
```

---

`1-129`: **Add unit tests for the script.**

Consider adding unit tests to verify the functionality of the script, especially for the complex logic in `format_line_for_json`. This would help ensure correctness and catch regressions when making future changes.


You could create a `test_generate_changelog.py` file with tests for each function. For example:

```python
import unittest
from generate_changelog import remove_at_index, format_line_for_json

class TestGenerateChangelog(unittest.TestCase):
    def test_remove_at_index(self):
        self.assertEqual(remove_at_index(1, "abc"), "ac")
        self.assertEqual(remove_at_index(0, "abc"), "bc")
        self.assertEqual(remove_at_index(2, "abc"), "ab")
        
    def test_format_line_for_json(self):
        # Test with nested quotes
        test_line = '{"title": "Revert "feat: Use git cliff" (#2324)", "commit": "123", "author": "Test", "date": "2023-01-01"}'
        expected = '{"title": "Revert feat: Use git cliff (#2324)", "commit": "123", "author": "Test", "date": "2023-01-01"}'
        self.assertEqual(format_line_for_json(test_line), expected)
        
        # Test without nested quotes
        test_line = '{"title": "Simple title", "commit": "123", "author": "Test", "date": "2023-01-01"}'
        self.assertEqual(format_line_for_json(test_line), test_line)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cf8da4d49bbb8763474ae67641c4efad161ed146 and a96cae0e1fc55b45114f06782f6a5e13abd385fb.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate-changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: tox
* GitHub Check: python-module-install
* GitHub Check: conventional-title

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T10:42:25Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (2)</summary><blockquote>

`74-75`: _🛠️ Refactor suggestion_

**Validate input tags before execution**

The script doesn't verify if the provided Git tags exist before attempting to use them. Adding a validation step would prevent cryptic errors and provide better user feedback.



```diff
 changelog: str = "## What's Changed\n"
 _format: str = """{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}"""

+# Validate that tags exist
+try:
+    # Check if tags exist
+    for tag in [from_tag, to_tag]:
+        proc = subprocess.run(
+            shlex.split(f"git rev-parse --verify {tag}"),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=True,
+        )
+        if proc.returncode != 0:
+            print(f"Error: Tag '{tag}' does not exist in the repository")
+            sys.exit(1)
+except Exception as ex:
+    print(f"Error validating git tags: {ex}")
+    sys.exit(1)
+
 try:
```

---

`118-119`: _🛠️ Refactor suggestion_

**Add handling for empty or malformed commit titles**

Line 118 assumes that commit titles will always contain a colon separator when they belong to a section other than "Other Changes". Add explicit handling for cases where the split operation might fail.



```diff
 for _change in _changelogs:
-    _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
+    if section != "Other Changes:" and ":" in _change["title"]:
+        _title = _change["title"].split(":", 1)[1].strip()
+    else:
+        _title = _change["title"]
     changelog += f"-{_title} ({_change['commit']}) by {_change['author']} on {_change['date']}\n"
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (3)</summary><blockquote>

`23-46`: **Add a proper docstring to format_line_for_json**

While you have good inline comments explaining what this function does, adding a formal docstring would maintain consistency with other functions and improve documentation.


```diff
def format_line_for_json(line: str) -> str:
+    """
+    Format a Git log line for proper JSON parsing.
+
+    Handles cases where commit messages contain double quotes that would break JSON parsing.
+
+    Args:
+        line: A line from git log output formatted as JSON
+
+    Returns:
+        The line with properly escaped quotes for JSON parsing
+    """
    # In case line is not formatted for json for example:
```

---

`78-91`: **Consider handling an empty git log result**

The script doesn't handle the case where there are no commits between the specified tags. This could happen if the tags are the same or if they are in reverse order.


```diff
     try:
         proc = subprocess.run(
             shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
             stdout=subprocess.PIPE,
             text=True,
         )
         if proc.returncode != 0:
             print("Error executing git log command")
             sys.exit(1)
 
         res = proc.stdout
+        
+        # Check if there are no commits between tags
+        if not res.strip():
+            print(f"No commits found between {from_tag} and {to_tag}")
+            return f"## No changes found between {from_tag} and {to_tag}\n\n**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
     except Exception as ex:
         print(f"Error executing git log command: {ex}")
         sys.exit(1)
```

---

`130-153`: **Improve command-line argument handling**

Consider enhancing the command-line interface with better argument handling and help options. This would make the script more user-friendly.


```diff
 if __name__ == "__main__":
     """
     Generate a changelog between two Git tags, formatted as markdown.
     
     [... existing docstring ...]
     """
-    if len(sys.argv) != 3:
-        print("Usage: python generate-changelog.py <from_tag> <to_tag>")
-        sys.exit(1)
-
-    print(main(from_tag=sys.argv[1], to_tag=sys.argv[2]))
+    import argparse
+    
+    parser = argparse.ArgumentParser(
+        description="Generate a changelog between two Git tags, formatted as markdown."
+    )
+    parser.add_argument("from_tag", help="Starting Git tag for the changelog")
+    parser.add_argument("to_tag", help="Ending Git tag for the changelog")
+    args = parser.parse_args()
+    
+    print(main(from_tag=args.from_tag, to_tag=args.to_tag))
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a96cae0e1fc55b45114f06782f6a5e13abd385fb and 5db628985ffe42f9d0419591982c9f5c52d7af05.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate-changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: tox
* GitHub Check: python-module-install
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>scripts/generate-changelog.py (3)</summary>

`1-7`: **Clean imports and good dependency choices!**

Your imports are clean and appropriate for the script's functionality. Using only standard Python libraries ensures the script can run without external dependencies, which aligns with your comment about keeping it standalone.

---

`9-20`: **Well-documented helper function**

The `remove_at_index` function has a clear docstring that explains its purpose, arguments, and return value. The implementation is simple and correct.

---

`49-128`: **Well-structured main function with clear categorization logic**

The main function is well-designed with:
- Clear mapping of commit types to changelog sections
- Organized structure using OrderedDict to maintain section order
- Good error handling for the git command and JSON parsing
- Appropriate formatting of the final changelog

The generated changelog will be well-structured and consistent with GitHub's style.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T10:45:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>scripts/generate_changelog.py (7)</summary><blockquote>

`9-30`: **Consider a more robust approach for handling nested quotes**

The `format_line_for_json` function uses a regex-based approach to handle quotes within the title field. While it works for the example case, it might be fragile for more complex nested quotes or escape sequences.


```diff
-def format_line_for_json(line: str) -> str:
-    # In case line is not formatted for json for example:
-    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
-    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
-    line_split = line.split(",")
-    title_key = line_split[0].split(":")[0]
-    title_split = line_split.pop(0).split(":", 1)[-1]
-
-    if title_split.count('"') > 2:
-        # Find all `"` indexes
-        quote_match = [match.start() for match in re.finditer('"', title_split)]
-
-        # Remove first and last matched `"`
-        inner_quotes = quote_match[1:-1]
-
-        for index in reversed(inner_quotes):
-            title_split = title_split[:index] + title_split[index + 1 :]
-
-        line_split.insert(0, f"{title_key}: {title_split.strip()}")
-        line = ",".join(line_split)
-
-    return line
+def format_line_for_json(line: str) -> str:
+    """
+    Format a git log line that may contain nested quotes to be valid JSON.
+    
+    Example input:
+    '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    """
+    try:
+        # Try parsing as JSON first
+        json.loads(line)
+        return line
+    except json.JSONDecodeError:
+        # If it fails, try to fix the title field
+        match = re.match(r'^\{"title": "(.*)", "commit": "(.*)", "author": "(.*)", "date": "(.*)"\}$', line)
+        if match:
+            title, commit, author, date = match.groups()
+            # Escape any quotes in the title
+            escaped_title = title.replace('"', '\\"')
+            # Create a new JSON-compatible string
+            return f'{{"title": "{escaped_title}", "commit": "{commit}", "author": "{author}", "date": "{date}"}}'
+        return line
```

This approach first tries to parse the line as JSON, and only applies fixing logic if needed. It uses regex to extract all fields and properly escapes quotes in the title.

---

`51-58`: **Improve error handling for JSON parsing**

The `parse_commit_line` function returns an empty dictionary when JSON parsing fails. This could lead to silent failures and make debugging difficult.


```diff
 def parse_commit_line(line: str) -> dict:
     """Parses a single JSON formatted git log line."""
     try:
         return json.loads(format_line_for_json(line))
     except json.decoder.JSONDecodeError as e:
         print(f"Error parsing JSON: {line} - {e}")
-        return {}
+        # Return a fallback dictionary with error information to make debugging easier
+        return {
+            "title": f"[PARSING ERROR] Original line: {line[:50]}...",
+            "commit": "ERROR",
+            "author": "ERROR",
+            "date": "ERROR"
+        }
```

---

`72-76`: **Use constants for category names and add validation**

The function hardcodes the "Other Changes:" string and doesn't validate required keys in the change dictionary.


```diff
+# Constants for category names
+OTHER_CHANGES = "Other Changes:"
+
 def format_changelog_entry(change: dict, section: str) -> str:
     """Formats a single changelog entry."""
-    title = change["title"].split(":", 1)[1] if section != "Other Changes:" else change["title"]
-    return f"- {title} ({change['commit']}) by {change['author']} on {change['date']}\n"
+    # Validate required keys
+    if not all(key in change for key in ["title", "commit", "author", "date"]):
+        return f"- [Incomplete data: {str(change)}]\n"
+        
+    title = change["title"].split(":", 1)[1] if section != OTHER_CHANGES else change["title"]
+    return f"- {title.strip()} ({change['commit']}) by {change['author']} on {change['date']}\n"
```

---

`78-127`: **Make the repository URL configurable**

The GitHub repository URL is hardcoded, which limits the script's reusability for other repositories.


```diff
-def main(from_tag: str, to_tag: str) -> str:
+def main(from_tag: str, to_tag: str, repo_url: str = "https://github.com/RedHatQE/openshift-python-wrapper") -> str:
     title_to_type_map: dict[str, str] = {
         "ci": "CI:",
         "docs": "Docs:",
         "feat": "New Feature:",
```

And later in the function:

```diff
     changelog += (
-        f"**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
+        f"**Full Changelog**: {repo_url}/compare/{from_tag}...{to_tag}"
     )
```

---

`129-152`: **Use argparse for better command-line argument handling**

The current command-line argument handling is basic. Using `argparse` would provide better help messages, argument validation, and flexibility.


```diff
 if __name__ == "__main__":
-    """
-    Generate a changelog between two Git tags, formatted as markdown.
-
-    This script parses Git commit logs between two specified tags and categorizes them
-    by commit type (feat, fix, ci, etc.). It formats the output as a markdown document
-    with sections for different types of changes, intended for use with release-it.
-
-    Each commit is expected to follow the conventional commit format:
-    <type>: <description>
-
-    where <type> is one of: feat, fix, docs, style, refactor, test, chore, etc.
-    Commits that don't follow this format are categorized under "Other Changes".
-
-    Generate a changelog between two tags, output as markdown
-
-    Usage: python generate-changelog.py <from_tag> <to_tag>
-    """
-    if len(sys.argv) != 3:
-        print("Usage: python generate-changelog.py <from_tag> <to_tag>")
-        sys.exit(1)
-
-    print(main(from_tag=sys.argv[1], to_tag=sys.argv[2]))
+    import argparse
+    
+    parser = argparse.ArgumentParser(
+        description="""
+        Generate a changelog between two Git tags, formatted as markdown.
+        
+        This script parses Git commit logs between two specified tags and categorizes them
+        by commit type (feat, fix, ci, etc.). It formats the output as a markdown document
+        with sections for different types of changes, intended for use with release-it.
+        
+        Each commit is expected to follow the conventional commit format:
+        <type>: <description>
+        
+        where <type> is one of: feat, fix, docs, style, refactor, test, chore, etc.
+        Commits that don't follow this format are categorized under "Other Changes".
+        """
+    )
+    
+    parser.add_argument('from_tag', help='The starting tag for the changelog range')
+    parser.add_argument('to_tag', help='The ending tag for the changelog range')
+    parser.add_argument('--repo-url', 
+                        default="https://github.com/RedHatQE/openshift-python-wrapper",
+                        help='Repository URL for the changelog links')
+    
+    args = parser.parse_args()
+    
+    print(main(from_tag=args.from_tag, to_tag=args.to_tag, repo_url=args.repo_url))
```

---

`1-152`: **Add unit tests for the changelog generation functionality**

The script contains several complex functions that would benefit from unit tests to ensure they work correctly with various inputs, especially edge cases.


I recommend creating a separate test file (`test_generate_changelog.py`) with unit tests for each function, focusing on:

1. Testing `format_line_for_json` with various quote patterns
2. Mocking `subprocess.run` to test `execute_git_log` without requiring git
3. Testing `categorize_commit` with various commit formats
4. Testing the entire pipeline with sample git log outputs

This would help catch regressions and ensure the script remains robust.

---

`91-101`: **Consider adding category for breaking changes**

The changelog categories are comprehensive, but you might want to add a "Breaking Changes:" category to highlight changes that require user attention during upgrades.


```diff
 changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("Breaking Changes:", []),
         ("New Feature:", []),
         ("Bugfixes:", []),
```

And update the title mapping:

```diff
 title_to_type_map: dict[str, str] = {
+        "breaking": "Breaking Changes:",
         "ci": "CI:",
         "docs": "Docs:",
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5db628985ffe42f9d0419591982c9f5c52d7af05 and 22e155a131e68147377c4d3f39362103332b814f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `.release-it.json` (1 hunks)
* `scripts/generate_changelog.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* .release-it.json

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: conventional-title
* GitHub Check: conventional-title
* GitHub Check: tox
* GitHub Check: python-module-install
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>scripts/generate_changelog.py (1)</summary>

`1-7`: **LGTM: Import statements are appropriate**

The import statements include all necessary modules for the script's functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-21T15:40:40Z
user: login: rnetser
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T10:55:58Z
user: login: myakove
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T11:13:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>scripts/generate_changelog.py (1)</summary><blockquote>

`37-53`: _⚠️ Potential issue_

**Add validation for tag inputs to prevent command injection**

The `execute_git_log` function doesn't validate the tag parameters, which could lead to command injection if inputs are user-controlled.



```diff
def execute_git_log(from_tag: str, to_tag: str) -> str:
    """Executes git log and returns the output, or raises an exception on error."""
    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+    
+    # Validate tags to prevent command injection
+    if not re.match(r'^[a-zA-Z0-9._-]+$', from_tag) or not re.match(r'^[a-zA-Z0-9._-]+$', to_tag):
+        print("Error: Invalid tag format. Tags should only contain alphanumeric characters, dots, underscores, or hyphens.")
+        sys.exit(1)

    try:
        command = f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>scripts/generate_changelog.py (4)</summary><blockquote>

`9-34`: **Simplify JSON line parsing**

The current implementation of `format_line_for_json` is quite complex and could be simplified using regex as suggested in the previous review. The function currently manipulates the string by finding and removing inner quotes, which could be brittle with edge cases.


Consider using a more robust regex approach:

```diff
def format_line_for_json(line: str) -> str:
    """
    Format str line to str that can be parsed with json.

    In case line is not formatted for json for example:
    '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
    title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
    """
-    line_split = line.split(",")
-    title_key = line_split[0].split(":")[0]
-    title_split = line_split.pop(0).split(":", 1)[-1]
-
-    if title_split.count('"') > 2:
-        # Find all `"` indexes
-        quote_match = [match.start() for match in re.finditer('"', title_split)]
-
-        # Remove first and last matched `"`
-        inner_quotes = quote_match[1:-1]
-
-        for index in reversed(inner_quotes):
-            title_split = title_split[:index] + title_split[index + 1 :]
-
-        line_split.insert(0, f"{title_key}: {title_split.strip()}")
-        line = ",".join(line_split)
+    # Use regex to match the title part and the rest of the JSON
+    match = re.match(r'({"title": ")(.*?)("(?:, ".*}))$', line)
+    if match and match.group(2).count('"') > 0:
+        # Extract groups and replace quotes in the title content
+        prefix, title_content, suffix = match.groups()
+        # Remove inner quotes from title content
+        sanitized_title = title_content.replace('"', '')
+        line = f"{prefix}{sanitized_title}{suffix}"

    return line
```

This approach uses regex to identify parts of the line and simplifies quote handling.

---

`82-131`: **Avoid hardcoding GitHub repository URL**

In the `main` function, the GitHub repository URL is hardcoded, which limits the script's reusability across different repositories.


Consider extracting the repository URL as a configurable parameter:

```diff
-def main(from_tag: str, to_tag: str) -> str:
+def main(from_tag: str, to_tag: str, repo_url: str = "RedHatQE/openshift-python-wrapper") -> str:
    # ... existing code ...
    
    changelog += (
-        f"**Full Changelog**: https://github.com/RedHatQE/openshift-python-wrapper/compare/{from_tag}...{to_tag}"
+        f"**Full Changelog**: https://github.com/{repo_url}/compare/{from_tag}...{to_tag}"
    )
```

You could then extract the repository URL dynamically from the git config:

```python
def get_repo_url():
    """Extract repository URL from git config."""
    try:
        command = "git config --get remote.origin.url"
        proc = subprocess.run(
            shlex.split(command), stdout=subprocess.PIPE, text=True, check=True
        )
        url = proc.stdout.strip()
        # Convert SSH URL to HTTPS URL if needed
        ssh_match = re.match(r'git@github\.com:(.+)\.git', url)
        if ssh_match:
            return ssh_match.group(1)
        # Extract path from HTTPS URL
        https_match = re.match(r'https://github\.com/(.+)(\.git)?', url)
        if https_match:
            return https_match.group(1)
        return "RedHatQE/openshift-python-wrapper"  # Default fallback
    except:
        return "RedHatQE/openshift-python-wrapper"  # Default fallback
```

And update the script entry point to pass this to the main function.

---

`133-156`: **Improve command-line interface with argument validation**

The script validates the number of arguments but doesn't validate the content of the tags before passing them to the main function.


Consider using the `argparse` module to create a more robust CLI:

```diff
+import argparse

if __name__ == "__main__":
    """
    Generate a changelog between two Git tags, formatted as markdown.

    This script parses Git commit logs between two specified tags and categorizes them
    by commit type (feat, fix, ci, etc.). It formats the output as a markdown document
    with sections for different types of changes, intended for use with release-it.

    Each commit is expected to follow the conventional commit format:
    <type>: <description>

    where <type> is one of: feat, fix, docs, style, refactor, test, chore, etc.
    Commits that don't follow this format are categorized under "Other Changes".

    Generate a changelog between two tags, output as markdown

    Usage: python generate-changelog.py <from_tag> <to_tag>
    """
-    if len(sys.argv) != 3:
-        print("Usage: python generate-changelog.py <from_tag> <to_tag>")
-        sys.exit(1)
-
-    print(main(from_tag=sys.argv[1], to_tag=sys.argv[2]))
+    parser = argparse.ArgumentParser(
+        description='Generate a changelog between two Git tags, formatted as markdown.'
+    )
+    parser.add_argument('from_tag', help='Starting tag for the changelog')
+    parser.add_argument('to_tag', help='Ending tag for the changelog')
+    parser.add_argument('--repo', default=None, help='GitHub repository (owner/repo)')
+    
+    args = parser.parse_args()
+    
+    # Validate tags to prevent command injection
+    for tag in [args.from_tag, args.to_tag]:
+        if not re.match(r'^[a-zA-Z0-9._-]+$', tag):
+            print(f"Error: Invalid tag format '{tag}'. Tags should only contain alphanumeric characters, dots, underscores, or hyphens.")
+            sys.exit(1)
+    
+    repo_url = args.repo or get_repo_url()
+    print(main(from_tag=args.from_tag, to_tag=args.to_tag, repo_url=repo_url))
```

This enhances the command-line interface with proper help messages and validation while maintaining backward compatibility.

---

`58-58`: **Use error logging for JSON parsing failures**

The error handling in `parse_commit_line` could be improved to provide more context about failures.


```diff
-        return json.loads(format_line_for_json(line))
+        formatted_line = format_line_for_json(line)
+        try:
+            return json.loads(formatted_line)
+        except json.decoder.JSONDecodeError as e:
+            print(f"Error parsing JSON after formatting: {formatted_line}")
+            raise
```

This change provides better debugging information by showing the formatted line that failed to parse.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 22e155a131e68147377c4d3f39362103332b814f and 8006e642190ee32b086a11e2fbad86d5256c3264.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate_changelog.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: python-module-install
* GitHub Check: tox
* GitHub Check: conventional-title

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>scripts/generate_changelog.py (1)</summary>

`76-79`: **LGTM: Well-structured formatting function**

The `format_changelog_entry` function cleanly formats each changelog entry with commit details.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T11:16:10Z
user: login: rnetser
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T10:44:54Z

Review comments:
diff_hunk: @@ -0,0 +1,88 @@
+import json
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def main(from_tag: str, to_tag: str) -> str:
+    title_to_type_map: dict[str, str] = {
+        "ci": "CI:",
+        "docs": "Docs:",
+        "feat": "New Feature:",
+        "fix": "Bugfixs:",
+        "refactor": "Refactor:",
+        "test": "Tests:",
+        "release": "New Release:",
+        "CherryPicked": "Cherry Pick:",
+        "merge": "Merge:",
+    }
+    changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("New Feature:", []),
+        ("Bugfixs:", []),
+        ("CI:", []),
+        ("New Release:", []),
+        ("Docs:", []),
+        ("Refactor:", []),
+        ("Tests:", []),
+        ("Other Changes:", []),
+        ("Cherry Pick:", []),
+        ("Merge:", []),
+    ])
+
+    changelog: str = "## What's Changed\n"
+    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+
+    try:
+        proc = subprocess.run(
path: scripts/generate-changelog.py
user: login: rnetser
body: why not use run_command?
created_at: 2025-03-21T08:33:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,88 @@
+import json
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def main(from_tag: str, to_tag: str) -> str:
+    title_to_type_map: dict[str, str] = {
+        "ci": "CI:",
+        "docs": "Docs:",
+        "feat": "New Feature:",
+        "fix": "Bugfixs:",
+        "refactor": "Refactor:",
+        "test": "Tests:",
+        "release": "New Release:",
+        "CherryPicked": "Cherry Pick:",
+        "merge": "Merge:",
+    }
+    changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("New Feature:", []),
+        ("Bugfixs:", []),
+        ("CI:", []),
+        ("New Release:", []),
+        ("Docs:", []),
+        ("Refactor:", []),
+        ("Tests:", []),
+        ("Other Changes:", []),
+        ("Cherry Pick:", []),
+        ("Merge:", []),
+    ])
+
+    changelog: str = "## What's Changed\n"
+    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+
+    try:
+        proc = subprocess.run(
path: scripts/generate-changelog.py
user: login: myakove
body: This is a standalone script and shouldn't be depends on other modules, you can run it with python3 (without uv).

created_at: 2025-03-21T08:50:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,152 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def remove_at_index(chr_index: int, string_: str) -> str:
+    """
+    Remove a character at the specified index from a string.
+
+    Args:
+        chr_index: The index of the character to remove
+        string_: The original string
+
+    Returns:
+        The string with the character at chr_index removed
+    """
+    return string_[:chr_index] + string_[chr_index + 1 :]
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
+    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
+    if title_split.count('"') > 2:
+        # Find all `"` indexes
+        quote_match = [match.start() for match in re.finditer('"', title_split)]
+
+        # Remove first and last matched `"`
+        quote_to_remove = quote_match[1:-1]
+
+        for idx, _index in enumerate(quote_to_remove):
+            # send _index -1 if not the first iter since the title_split changed (removed one character on first iter)
+            _index_to_send = _index if idx == 0 else _index - 1
+            title_split = remove_at_index(chr_index=_index_to_send, string_=title_split)
+
+        line_split.insert(0, f"{title_key}: {title_split.strip()}")
+        line = ",".join(line_split)
+
+    return line
+
+
+def main(from_tag: str, to_tag: str) -> str:
+    title_to_type_map: dict[str, str] = {
+        "ci": "CI:",
+        "docs": "Docs:",
+        "feat": "New Feature:",
+        "fix": "Bugfixes:",
+        "refactor": "Refactor:",
+        "test": "Tests:",
+        "release": "New Release:",
+        "cherrypicked": "Cherry Pick:",
+        "merge": "Merge:",
+    }
+    changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("New Feature:", []),
+        ("Bugfixes:", []),
+        ("CI:", []),
+        ("New Release:", []),
+        ("Docs:", []),
+        ("Refactor:", []),
+        ("Tests:", []),
+        ("Other Changes:", []),
+        ("Cherry Pick:", []),
+        ("Merge:", []),
+    ])
+
+    changelog: str = "## What's Changed\n"
+    _format: str = """{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}"""
+
+    try:
+        proc = subprocess.run(
+            shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
+            stdout=subprocess.PIPE,
+            text=True,
+        )
+        if proc.returncode != 0:
+            print("Error executing git log command")
+            sys.exit(1)
+
+        res = proc.stdout
+    except Exception as ex:
+        print(f"Error executing git log command: {ex}")
+        sys.exit(1)
+
path: scripts/generate-changelog.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Validate input tags before execution.**

The script doesn't verify if the provided Git tags exist before attempting to use them. Adding a validation step would prevent cryptic errors and provide better user feedback.


```diff
 changelog: str = "## What's Changed\n"
 _format: str = """{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}"""

+# Validate that tags exist
+try:
+    # Check if tags exist
+    for tag in [from_tag, to_tag]:
+        proc = subprocess.run(
+            shlex.split(f"git rev-parse --verify {tag}"),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=True,
+        )
+        if proc.returncode != 0:
+            print(f"Error: Tag '{tag}' does not exist in the repository")
+            sys.exit(1)
+except Exception as ex:
+    print(f"Error validating git tags: {ex}")
+    sys.exit(1)
+
 try:
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    changelog: str = "## What's Changed\n"
    _format: str = """{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}"""

    # Validate that tags exist
    try:
        # Check if tags exist
        for tag in [from_tag, to_tag]:
            proc = subprocess.run(
                shlex.split(f"git rev-parse --verify {tag}"),
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
            if proc.returncode != 0:
                print(f"Error: Tag '{tag}' does not exist in the repository")
                sys.exit(1)
    except Exception as ex:
        print(f"Error validating git tags: {ex}")
        sys.exit(1)

    try:
        proc = subprocess.run(
            shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
            stdout=subprocess.PIPE,
            text=True,
        )
        if proc.returncode != 0:
            print("Error executing git log command")
            sys.exit(1)

        res = proc.stdout
    except Exception as ex:
        print(f"Error executing git log command: {ex}")
        sys.exit(1)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-21T10:42:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,152 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def remove_at_index(chr_index: int, string_: str) -> str:
+    """
+    Remove a character at the specified index from a string.
+
+    Args:
+        chr_index: The index of the character to remove
+        string_: The original string
+
+    Returns:
+        The string with the character at chr_index removed
+    """
+    return string_[:chr_index] + string_[chr_index + 1 :]
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
+    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
+    if title_split.count('"') > 2:
+        # Find all `"` indexes
+        quote_match = [match.start() for match in re.finditer('"', title_split)]
+
+        # Remove first and last matched `"`
+        quote_to_remove = quote_match[1:-1]
+
+        for idx, _index in enumerate(quote_to_remove):
+            # send _index -1 if not the first iter since the title_split changed (removed one character on first iter)
+            _index_to_send = _index if idx == 0 else _index - 1
+            title_split = remove_at_index(chr_index=_index_to_send, string_=title_split)
+
+        line_split.insert(0, f"{title_key}: {title_split.strip()}")
+        line = ",".join(line_split)
+
+    return line
+
+
+def main(from_tag: str, to_tag: str) -> str:
+    title_to_type_map: dict[str, str] = {
+        "ci": "CI:",
+        "docs": "Docs:",
+        "feat": "New Feature:",
+        "fix": "Bugfixes:",
+        "refactor": "Refactor:",
+        "test": "Tests:",
+        "release": "New Release:",
+        "cherrypicked": "Cherry Pick:",
+        "merge": "Merge:",
+    }
+    changelog_dict: OrderedDict[str, list[dict[str, str]]] = OrderedDict([
+        ("New Feature:", []),
+        ("Bugfixes:", []),
+        ("CI:", []),
+        ("New Release:", []),
+        ("Docs:", []),
+        ("Refactor:", []),
+        ("Tests:", []),
+        ("Other Changes:", []),
+        ("Cherry Pick:", []),
+        ("Merge:", []),
+    ])
+
+    changelog: str = "## What's Changed\n"
+    _format: str = """{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}"""
+
+    try:
+        proc = subprocess.run(
+            shlex.split(f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"),
+            stdout=subprocess.PIPE,
+            text=True,
+        )
+        if proc.returncode != 0:
+            print("Error executing git log command")
+            sys.exit(1)
+
+        res = proc.stdout
+    except Exception as ex:
+        print(f"Error executing git log command: {ex}")
+        sys.exit(1)
+
+    for line in res.splitlines():
+        line = format_line_for_json(line=line)
+
+        try:
+            _json_line = json.loads(line)
+        except json.decoder.JSONDecodeError:
+            print(f"Error parsing json line: {line}")
+            sys.exit(1)
+
+        try:
+            prefix = _json_line["title"].split(":", 1)[0]
+            if prefix.lower() in title_to_type_map:
+                _map = title_to_type_map[prefix]
+                changelog_dict[_map].append(_json_line)
+
+            else:
+                changelog_dict["Other Changes:"].append(_json_line)
+        except IndexError:
+            changelog_dict["Other Changes:"].append(_json_line)
+
+    for section, _changelogs in changelog_dict.items():
+        if not _changelogs:
+            continue
+
+        changelog += f"#### {section}\n"
+        for _change in _changelogs:
+            _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
+            changelog += f"-{_title} ({_change['commit']}) by {_change['author']} on {_change['date']}\n"
path: scripts/generate-changelog.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add handling for empty or malformed commit titles.**

Line 118 assumes that commit titles will always contain a colon separator when they belong to a section other than "Other Changes". Add explicit handling for cases where the split operation might fail.


```diff
 for _change in _changelogs:
-    _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
+    if section != "Other Changes:" and ":" in _change["title"]:
+        _title = _change["title"].split(":", 1)[1].strip()
+    else:
+        _title = _change["title"]
     changelog += f"-{_title} ({_change['commit']}) by {_change['author']} on {_change['date']}\n"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
 for _change in _changelogs:
-    _title = _change["title"].split(":", 1)[1] if section != "Other Changes:" else _change["title"]
+    if section != "Other Changes:" and ":" in _change["title"]:
+        _title = _change["title"].split(":", 1)[1].strip()
+    else:
+        _title = _change["title"]
     changelog += f"-{_title} ({_change['commit']}) by {_change['author']} on {_change['date']}\n"
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-21T10:42:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,151 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
+    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
+    if title_split.count('"') > 2:
+        # Find all `"` indexes
+        quote_match = [match.start() for match in re.finditer('"', title_split)]
+
+        # Remove first and last matched `"`
+        inner_quotes = quote_match[1:-1]
+
+        for index in reversed(inner_quotes):
+            title_split = title_split[:index] + title_split[index + 1 :]
+
+        line_split.insert(0, f"{title_key}: {title_split.strip()}")
+        line = ",".join(line_split)
+
+    return line
+
+
+def execute_git_log(from_tag: str, to_tag: str) -> str:
+    """Executes git log and returns the output, or raises an exception on error."""
+    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+
+    try:
+        command = f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"
+        proc = subprocess.run(
+            shlex.split(command), stdout=subprocess.PIPE, text=True, check=True
+        )  # Use check=True to raise an exception for non-zero return codes
+        return proc.stdout
+    except subprocess.CalledProcessError as e:
+        print(f"Error executing git log: {e}")
+        sys.exit(1)
+    except FileNotFoundError:
+        print("Error: git not found.  Please ensure git is installed and in your PATH.")
+        sys.exit(1)
+
path: scripts/generate_changelog.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for tag inputs to prevent command injection**

The `execute_git_log` function doesn't validate the tag parameters, which could lead to command injection if inputs are user-controlled.


```diff
 def execute_git_log(from_tag: str, to_tag: str) -> str:
     """Executes git log and returns the output, or raises an exception on error."""
     _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
+    
+    # Validate tags to prevent command injection
+    if not re.match(r'^[a-zA-Z0-9._-]+$', from_tag) or not re.match(r'^[a-zA-Z0-9._-]+$', to_tag):
+        print("Error: Invalid tag format. Tags should only contain alphanumeric characters, dots, underscores, or hyphens.")
+        sys.exit(1)
 
     try:
         command = f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
def execute_git_log(from_tag: str, to_tag: str) -> str:
    """Executes git log and returns the output, or raises an exception on error."""
    _format: str = '{"title": "%s", "commit": "%h", "author": "%an", "date": "%as"}'
    
    # Validate tags to prevent command injection
    if not re.match(r'^[a-zA-Z0-9._-]+$', from_tag) or not re.match(r'^[a-zA-Z0-9._-]+$', to_tag):
        print("Error: Invalid tag format. Tags should only contain alphanumeric characters, dots, underscores, or hyphens.")
        sys.exit(1)
    
    try:
        command = f"git log --pretty=format:'{_format}' {from_tag}...{to_tag}"
        proc = subprocess.run(
            shlex.split(command), stdout=subprocess.PIPE, text=True, check=True
        )  # Use check=True to raise an exception for non-zero return codes
        return proc.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error executing git log: {e}")
        sys.exit(1)
    except FileNotFoundError:
        print("Error: git not found.  Please ensure git is installed and in your PATH.")
        sys.exit(1)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-21T15:40:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,151 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
+    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
+    if title_split.count('"') > 2:
path: scripts/generate_changelog.py
user: login: rnetser
body: what about:
```
line_dict = re.match(r"(?P<title>{\"title\":.*)(?P<rest>\"commit\":.*)", line).groupdict()
if line_dict["title"].count('"') == 6:
    f'"{line_dict["title"].replace('"', "", 5)} {line_dict["rest"]}'

```



created_at: 2025-03-24T10:53:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,151 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
path: scripts/generate_changelog.py
user: login: rnetser
body: please use docstring format
created_at: 2025-03-24T10:54:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,151 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def format_line_for_json(line: str) -> str:
+    # In case line is not formatted for json for example:
+    # '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    # title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
+    if title_split.count('"') > 2:
path: scripts/generate_changelog.py
user: login: myakove
body: This code is not working for me
created_at: 2025-03-24T11:13:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,155 @@
+import json
+import re
+import shlex
+import subprocess
+import sys
+from collections import OrderedDict
+
+
+def format_line_for_json(line: str) -> str:
+    """
+    Format str line to str that can be parsed with json.
+
+    In case line is not formatted for json for example:
+    '{"title": "Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)", "commit": "137331fd", "author": "Meni Yakove", "date": "2025-02-16"}'
+    title have `"` inside the external `"` `"Revert "feat: Use git cliff to generate the change log. (#2322)" (#2324)"`
+    """
+    line_split = line.split(",")
+    title_key = line_split[0].split(":")[0]
+    title_split = line_split.pop(0).split(":", 1)[-1]
+
path: scripts/generate_changelog.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve JSON parsing robustness**

The current approach to extracting the title by splitting on commas and colons is fragile, especially when dealing with complex JSON structures.


Consider using the JSON parser directly after escaping problematic quotes:

```diff
-    line_split = line.split(",")
-    title_key = line_split[0].split(":")[0]
-    title_split = line_split.pop(0).split(":", 1)[-1]
+    # Try to parse as JSON first
+    try:
+        return json.loads(line)
+    except json.JSONDecodeError:
+        # If parsing fails, apply quote escaping logic
+        # (rest of the function)
```

This approach tries to parse as valid JSON first, and only applies special handling when needed.


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-24T11:16:08Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-20T20:10:30Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces changes to several configuration files and scripts. The `.flake8` file now excludes the `subprocess` function from linting checks. The `.release-it.json` file has been updated to utilize a Python script for changelog generation instead of a direct git command. Additionally, the `scripts/k3d-runner.sh` file has been deleted, and a new Python script, `generate_changelog.py`, has been added to automate the creation of a changelog based on git log output.

## Changes
| File(s)                         | Change Summary |
|---------------------------------|----------------|
| `.flake8`                      | Added the `subprocess` function to the `fcn_exclude_functions` list. |
| `.release-it.json`             | Changed the changelog command from a git log format to running `uv run scripts/generate_changelog.py ${from} ${to}`. |
| `scripts/k3d-runner.sh`        | Deleted the Bash script that managed a k3d cluster. |
| `scripts/generate_changelog.py` | Added a new Python script that generates a changelog by executing and processing git log output. |

## Possibly related PRs
- RedHatQE/openshift-python-wrapper#2356: The changes in this PR and the retrieved PR are related as both involve modifications to the `.flake8` configuration file, specifically adding the `subprocess` function to the exclusion list.

## Suggested reviewers
- dbasunag
- rnetser
- omrirh

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 790a576a61c7a262df5297d2411bff7af428a091 and 8826aeaab0d62c4158bc7037dc2d397bfc91cf94.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `scripts/generate_changelog.py` (1 hunks)

</details>

</details>
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=RedHatQE/openshift-python-wrapper&utm_content=2356):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: redhat-qe-bot1
created_at: 2025-03-20T20:10:35Z
body: 
Report bugs in [Issues](https://github.com/myakove/github-webhook-server/issues)

The following are automatically added:
 * Add reviewers from OWNER file (in the root of the repository) under reviewers section.
 * Set PR size label.
 * New issue is created for the PR. (Closed when PR is merged/closed)
 * Run [pre-commit](https://pre-commit.ci/) if `.pre-commit-config.yaml` exists in the repo.

Available user actions:
 * To mark PR as WIP comment `/wip` to the PR, To remove it from the PR comment `/wip cancel` to the PR.
 * To block merging of PR comment `/hold`, To un-block merging of PR comment `/hold cancel`.
 * To mark PR as verified comment `/verified` to the PR, to un-verify comment `/verified cancel` to the PR.
        verified label removed on each new commit push.
 * To cherry pick a merged PR comment `/cherry-pick <target branch to cherry-pick to>` in the PR.
    * Multiple target branches can be cherry-picked, separated by spaces. (`/cherry-pick branch1 branch2`)
    * Cherry-pick will be started when PR is merged
 * To build and push container image command `/build-and-push-container` in the PR (tag will be the PR number).
    * You can add extra args to the Podman build command
        * Example: `/build-and-push-container --build-arg OPENSHIFT_PYTHON_WRAPPER_COMMIT=<commit_hash>`
 * To add a label by comment use `/<label name>`, to remove, use `/<label name> cancel`
 * To assign reviewers based on OWNERS file use `/assign-reviewers`
 * To check if PR can be merged use `/check-can-merge`
 * to assign reviewer to PR use `/assign-reviewer @<reviewer>`

<details>
<summary>Supported /retest check runs</summary>

 * `/retest tox`: Retest tox
 * `/retest python-module-install`: Retest python-module-install
  * `/retest conventional-title`: Retest pre-commit
 * `/retest all`: Retest all

</details>

<details>
<summary>Supported labels</summary>

 * hold
 * verified
 * wip
 * lgtm

</details>
    
author_association: NONE
user: login: myakove
created_at: 2025-03-20T20:11:35Z
body: /verified

The script is verified and working as expected
```
uv run scripts/generate-changelog.py v11.0.39 v11.0.40                                                                                                                                 2s
Bugfix:
     escape docstring with `r` for generated resources using class-generator (#2352) (8927a2aa) by Meni Yakove on 2025-03-20

New Release:
     New Release 11.0.40 is out (5e1f21d4) by Meni Yakove on 2025-03-20
```

Ne to merge in order to check it with release-it
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T08:14:32Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T08:25:00Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T08:56:12Z
body: /verified cancel
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T10:30:48Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T10:32:57Z
body: 
/verified


author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T10:42:40Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-21T21:05:53Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-22T18:26:22Z
body: 
/verified


author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-22T18:47:55Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-24T11:13:45Z
body: /verified
author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-24T11:56:57Z
body: 
/verified


author_association: COLLABORATOR
user: login: myakove
created_at: 2025-03-25T10:45:04Z
body: 
/verified


author_association: COLLABORATOR