Skip to content

Prevent Jira auto-linking in NEWA IDs with backwards compatibility#333

Merged
kkaarreell merged 2 commits into
mainfrom
ks_nonbreakable_hyphens
Mar 30, 2026
Merged

Prevent Jira auto-linking in NEWA IDs with backwards compatibility#333
kkaarreell merged 2 commits into
mainfrom
ks_nonbreakable_hyphens

Conversation

@kkaarreell

@kkaarreell kkaarreell commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

Use non-breaking hyphens (U+2011) in NEWA IDs to prevent Jira Cloud from auto-linking issue keys like "RHELMISC-29371", which was corrupting NEWA IDs and preventing issue reuse. Non-breaking hyphens look identical to regular hyphens but don't trigger Jira's auto-linking.

Key changes:

  • Add _format_for_jira() to replace hyphens with non-breaking hyphens
  • Add _newa_id_in_description() helper for backwards-compatible matching
  • New issues created with non-breaking hyphens in NEWA IDs
  • Dual JQL search in get_related_issues() to find both old and new format
  • Sanitize Jira field values to prevent Jinja2 template conflicts ({{ }})

Backwards compatibility:

  • Old issues with regular hyphens are still found and reused
  • Search performs two queries (one per format) and merges results
  • Helper function matches NEWA IDs regardless of hyphen type
  • Old issues are migrated to new format when refreshed/updated

🤖 Generated with Claude Code

Summary by Sourcery

Ensure NEWA IDs remain stable and reusable in Jira by preventing auto-linking and handling both legacy and new formats.

New Features:

  • Introduce non-breaking hyphen formatting for NEWA IDs to avoid Jira auto-linking issue keys.
  • Add a helper to detect NEWA IDs in Jira descriptions regardless of hyphen type for backwards-compatible matching.
  • Sanitize Jira field values before passing them to Jinja templates to avoid syntax conflicts.

Enhancements:

  • Update related-issue discovery to query Jira with both formatted and legacy NEWA ID variants and merge results without duplicates.
  • Adjust issue refresh and update flows to correctly add or replace NEWA IDs while preserving the existing hyphen format in descriptions.
  • Refine logic for identifying reusable Jira issues based on NEWA IDs using the new compatibility helper.

@kkaarreell kkaarreell self-assigned this Mar 30, 2026
@sourcery-ai

sourcery-ai Bot commented Mar 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR changes how NEWA IDs and Jira field values are handled to prevent Jira Cloud auto-linking and Jinja2 template conflicts, while maintaining backwards compatibility with existing issues by dual-format searching and robust NEWA ID matching.

Sequence diagram for dual-format NEWA ID search in get_related_issues

sequenceDiagram
    participant Caller
    participant JiraService
    participant JiraConnection as JiraConnection_or_ServerConnection

    Caller->>JiraService: get_related_issues(action, closed, all_respins)
    activate JiraService
    JiraService->>JiraService: newa_id(action, all_respins)
    JiraService-->>JiraService: base_newa_id (regular hyphens)

    JiraService->>JiraService: _format_for_jira(base_newa_id)
    JiraService-->>JiraService: formatted_newa_id (nonbreaking hyphens)

    JiraService->>JiraService: build JQL query 1 using formatted_newa_id
    JiraService->>JiraService: build JQL query 2 using base_newa_id

    loop for each query in [query1, query2]
        alt jira_connection.is_cloud
            JiraService->>JiraConnection: search_issues_v3(jql, fields, max_results)
            JiraConnection-->>JiraService: search_result (dict)
        else Jira Server
            JiraService->>JiraConnection: search_issues(jql, fields, json_result=True)
            JiraConnection-->>JiraService: search_result (dict)
        end

        JiraService->>JiraService: validate type(search_result) is dict
        JiraService->>JiraService: merge issues into all_issues by key
        JiraService->>JiraService: short_sleep()
    end

    JiraService->>JiraService: result = {}
    loop for each jira_issue in all_issues.values
        JiraService->>JiraService: description = jira_issue.fields.description or ""
        JiraService->>JiraService: _newa_id_in_description(base_newa_id, description)
        alt match found
            JiraService-->>JiraService: matched NEWA ID string
            JiraService->>JiraService: add entry to result[jira_issue.key]
        else no match
            JiraService-->>JiraService: None
        end
    end

    JiraService-->>Caller: result
    deactivate JiraService
Loading

Class diagram for updated JiraService NEWA ID handling

classDiagram
    class JiraService {
        - jira_connection
        - connection
        - project
        - newa_label
        - transitions
        - logger
        + newa_id(action: IssueAction, partial: bool) str
        + get_related_issues(action: IssueAction, closed: bool, all_respins: bool) dict
        + refresh_issue(action: IssueAction, issue: Issue) bool
        + update_issue(action: IssueAction, issue: Issue) bool
        - _format_for_jira(text: str) str
        - _newa_id_in_description(newa_id: str, description: str) str
    }

    class JiraConnection {
        + is_cloud bool
        + search_issues_v3(jql: str, fields: list, max_results: int) dict
        + search_issues(jql: str, fields: list, json_result: bool) dict
    }

    class IssueAction
    class Issue

    JiraService --> JiraConnection : uses
    JiraService --> IssueAction : uses
    JiraService --> Issue : uses
Loading

File-Level Changes

Change Details Files
Replace prior Jira Cloud formatting with non-breaking hyphen normalization for NEWA IDs and introduce a helper to find IDs regardless of hyphen type.
  • Update _format_for_jira to replace regular hyphens with non-breaking hyphens instead of wrapping content in Jira code formatting braces.
  • Add _newa_id_in_description helper that attempts to match a NEWA ID using both formatted (non-breaking hyphens) and original (regular hyphens) forms and returns the exact matched string.
  • Use _newa_id_in_description in refresh_issue and update_issue to detect existing NEWA IDs and decide whether to add or update IDs, preserving existing hyphen formatting when replacing.
newa/services/jira_service.py
Update related-issue discovery to search for both old and new NEWA ID formats and deduplicate results.
  • Derive a base NEWA ID and build two JQL queries: one using the formatted (non-breaking hyphen) ID and one using the original ID.
  • Execute both JQL queries (v3 for Jira Cloud, v2 for Server), validate the response type, and merge issues by key into a combined dictionary, avoiding duplicates.
  • Iterate over the merged issue set and use _newa_id_in_description for final filtering of description matches instead of simple substring checks.
newa/services/jira_service.py
Enhance NEWA-based issue selection in the CLI to rely on the new backwards-compatible NEWA ID matcher.
  • Replace direct substring check on NEWA IDs in _find_or_create_issue with a call to jira_handler._newa_id_in_description to correctly handle both regular and non-breaking hyphens.
  • Preserve existing logic for determining whether an issue is considered new with respect to the action parent relationship.
newa/cli/jira_helpers.py
Sanitize Jira event fields before passing them into Jinja templates to avoid syntax conflicts with Jira’s use of {{ }}.
  • Add _sanitize_jira_field_value utility that recursively replaces '{{' and '}}' with double quotes in strings, lists, and dicts, leaving other types unchanged.
  • Refactor _get_jira_event_fields to convert the Jira fields object into a plain dict via vars()/dir(), applying _sanitize_jira_field_value to each value while skipping private attributes and callables, and then add the issue ID.
  • Retain the existing short_sleep call and fallback to an empty dict when the event type is not JIRA.
newa/cli/jira_helpers.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • Consider avoiding direct use of the private helper jira_handler._newa_id_in_description from jira_helpers.py and either make this a public API on the handler or move the logic into a shared utility to reduce tight coupling on an internal method.
  • The _sanitize_jira_field_value function currently replaces {{/}} with double quotes, which can unintentionally introduce or unbalance quotes in user-visible text; it might be safer to replace them with a more neutral escape sequence (e.g. HTML entities or a non-conflicting token) instead.
  • In get_related_issues, raising a bare Exception for an unexpected search result type makes error handling less precise; using a more specific exception type or defining a custom one would make failures easier to diagnose and handle.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider avoiding direct use of the private helper `jira_handler._newa_id_in_description` from `jira_helpers.py` and either make this a public API on the handler or move the logic into a shared utility to reduce tight coupling on an internal method.
- The `_sanitize_jira_field_value` function currently replaces `{{`/`}}` with double quotes, which can unintentionally introduce or unbalance quotes in user-visible text; it might be safer to replace them with a more neutral escape sequence (e.g. HTML entities or a non-conflicting token) instead.
- In `get_related_issues`, raising a bare `Exception` for an unexpected search result type makes error handling less precise; using a more specific exception type or defining a custom one would make failures easier to diagnose and handle.

## Individual Comments

### Comment 1
<location path="newa/services/jira_service.py" line_range="213-214" />
<code_context>
+        # Build queries for both formats
+        queries = []
+        base_filter = f"project = '{self.project}' AND labels in ({self.newa_label})"
+        status_filter = "" if closed else f" AND status not in ({
+            ','.join(
+                self.transitions.closed)})"
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The multiline f-string used for `status_filter` will inject newlines/indentation into the JQL.

This will likely produce invalid JQL and can cause search failures, depending on how Jira handles the embedded whitespace. Please build the filter on a single line or avoid embedded newlines, for example:

```python
status_filter = "" if closed else f" AND status not in ({','.join(self.transitions.closed)})"
```
</issue_to_address>

### Comment 2
<location path="newa/services/jira_service.py" line_range="204-208" />
<code_context>
+        # Get base NEWA ID (with regular hyphens)
+        base_newa_id = self.newa_id(action, True) if all_respins else self.newa_id(action)
+
+        # Search with both formats for backwards compatibility
+        # New format: non-breaking hyphens
+        formatted_newa_id = self._format_for_jira(base_newa_id)
+        # Old format: regular hyphens
+        original_newa_id = base_newa_id
+
+        # Build queries for both formats
</code_context>
<issue_to_address>
**suggestion (performance):** Unconditionally building two queries can be avoided when the formatted ID is identical to the original.

When `base_newa_id` has no hyphens, `formatted_newa_id` and `original_newa_id` are equal but you still issue two identical queries. Consider only appending the second query when the two IDs differ, e.g.:

```python
formatted_newa_id = self._format_for_jira(base_newa_id)
original_newa_id = base_newa_id

queries = []
if formatted_newa_id == original_newa_id:
    queries.append(f"{base_filter} AND description ~ '{original_newa_id}'{status_filter}")
else:
    queries.append(f"{base_filter} AND description ~ '{formatted_newa_id}'{status_filter}")
    queries.append(f"{base_filter} AND description ~ '{original_newa_id}'{status_filter}")
```
</issue_to_address>

### Comment 3
<location path="newa/cli/jira_helpers.py" line_range="352-356" />
<code_context>

             is_new = False
-            if jira_handler.newa_id(action) in jira_issue["description"] \
+            # Use helper function for backwards-compatible comparison
+            if (jira_handler._newa_id_in_description(
+                    jira_handler.newa_id(action), jira_issue["description"]) is not None
                 and (not action.parent_id
-                     or action.parent_id not in created_action_ids):
+                     or action.parent_id not in created_action_ids)):
                 is_new = True

</code_context>
<issue_to_address>
**suggestion:** Accessing the handler’s private method `_newa_id_in_description` from CLI code couples modules to internals.

Calling `_newa_id_in_description(...)` from the CLI introduces a dependency on a private implementation detail of the Jira handler, which hurts modularity and future refactoring. Either promote this to a public API method or move the shared logic into a separate utility that both the service and CLI can use.

Suggested implementation:

```python
            ctx.logger.debug(f"Checking {jira_issue_key}")

            is_new = False
            # Use Jira handler public helper for backwards-compatible comparison
            if (jira_handler.newa_id_in_description(
                    jira_handler.newa_id(action), jira_issue["description"]) is not None
                and (not action.parent_id
                     or action.parent_id not in created_action_ids)):
                is_new = True

```

To fully implement the suggestion and keep the CLI decoupled from handler internals, you should also:
1. In the Jira handler module (where `_newa_id_in_description` is defined), either:
   - Rename `_newa_id_in_description` to `newa_id_in_description` (removing the leading underscore), or
   - Add a public wrapper method `def newa_id_in_description(self, newa_id: str, description: str): return self._newa_id_in_description(newa_id, description)`.
2. Ensure any other internal callers in the handler are updated consistently (either call the private or the new public method according to your conventions).
3. If you prefer a shared utility instead of a handler method, extract the logic from `_newa_id_in_description` into a utility function (e.g. in a `utils` or `jira_utils` module) and have both the handler and CLI call that utility; in that case, update this CLI call accordingly.
</issue_to_address>

### Comment 4
<location path="newa/cli/jira_helpers.py" line_range="64-73" />
<code_context>
         n += 1


+def _sanitize_jira_field_value(value: Any) -> Any:
+    """
+    Sanitize Jira field values to prevent Jinja2 template conflicts.
+
+    Jira uses {{ }} for code formatting, which conflicts with Jinja2 syntax.
+    This function replaces the braces with double quotes.
+    """
+    if isinstance(value, str):
+        # Replace {{ and }} with double quotes
+        return value.replace('{{', '"').replace('}}', '"')
+    if isinstance(value, list):
+        return [_sanitize_jira_field_value(item) for item in value]
+    if isinstance(value, dict):
+        return {k: _sanitize_jira_field_value(v) for k, v in value.items()}
+    # Return other types unchanged (int, bool, None, objects, etc.)
+    return value
+
+
</code_context>
<issue_to_address>
**question (bug_risk):** Replacing `{{`/`}}` with double quotes can lead to odd or ambiguous content in templates.

Given the current behavior, a value like `"{{foo}}"` becomes `""foo""`, which can distort the rendered content and introduce confusing quoting. If the goal is only to prevent Jinja from treating `{{`/`}}` as delimiters, consider escaping the braces instead (e.g. wrapping in `{% raw %}` or replacing `{`/`}` with `\u007b`/`\u007d` or `\{`/`\}`) so the visual content is preserved without altering quotes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread newa/services/jira_service.py Outdated
Comment thread newa/services/jira_service.py
Comment thread newa/cli/jira_helpers.py
Comment thread newa/cli/jira_helpers.py
kkaarreell and others added 2 commits March 30, 2026 15:48
Use non-breaking hyphens (U+2011) in NEWA IDs to prevent Jira Cloud
from auto-linking issue keys like "RHELMISC-29371", which was corrupting
NEWA IDs and preventing issue reuse. Non-breaking hyphens look identical
to regular hyphens but don't trigger Jira's auto-linking.

Key changes:
- Add _format_for_jira() to replace hyphens with non-breaking hyphens
- Add _newa_id_in_description() helper for backwards-compatible matching
- New issues created with non-breaking hyphens in NEWA IDs
- Dual JQL search in get_related_issues() to find both old and new format
- Sanitize Jira field values to prevent Jinja2 template conflicts ({{ }})

Backwards compatibility:
- Old issues with regular hyphens are still found and reused
- Search performs two queries (one per format) and merges results
- Helper function matches NEWA IDs regardless of hyphen type
- Old issues are migrated to new format when refreshed/updated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously, the _newa_id_in_description method was marked as private but was
being accessed from CLI code in newa/cli/jira_helpers.py. This created
inappropriate coupling between modules and violated encapsulation.

The method provides backwards-compatible NEWA ID matching that checks for both
regular hyphens (old format) and non-breaking hyphens (new format). This
functionality is a legitimate part of the public API since it's needed by
callers who need to check if a NEWA ID exists in a description.

Changes:
- Renamed _newa_id_in_description to newa_id_in_description (removed underscore)
- Updated all references in both jira_service.py and jira_helpers.py
- No functional changes, only API visibility improvement

This addresses code review feedback about module coupling and follows Python
conventions where underscore-prefixed methods should only be accessed within
the class.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@kkaarreell kkaarreell force-pushed the ks_nonbreakable_hyphens branch from ca3814f to edb7ff8 Compare March 30, 2026 13:58
@kkaarreell kkaarreell merged commit f02cf35 into main Mar 30, 2026
18 checks passed
@kkaarreell kkaarreell deleted the ks_nonbreakable_hyphens branch March 30, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant