Skip to content

feat: Add foundation for Jira issue creation workflow#166

Open
r-pedraza wants to merge 1 commit intomasterfrom
feat/crerate-issue-jira-core
Open

feat: Add foundation for Jira issue creation workflow#166
r-pedraza wants to merge 1 commit intomasterfrom
feat/crerate-issue-jira-core

Conversation

@r-pedraza
Copy link
Contributor

Pull Request

📝 Summary

This PR lays the groundwork for integrating a Jira issue creation workflow. It enables the Jira plugin in the configuration and refactors the create_branch_step to use a more robust Result object pattern for handling git operations. This shift from exception-based error handling to a match/case structure improves code clarity and resilience.

🔧 Changes Made

  • Enabled the [plugins.jira] in .titan/config.toml.
  • Refactored create_branch_step.py to use ClientSuccess and ClientError result objects instead of raising GitError exceptions.
  • Replaced try/except blocks with match statements for handling the outcomes of all git commands (get_branches, checkout, delete_branch, create_branch).
  • Updated imports to use the new result types from titan_cli.core.result.

🧪 Testing

  • Unit tests added/updated (poetry run pytest)
  • All tests passing (make test)
  • Manual testing with titan-dev

The refactoring touches core git functionality. Existing unit tests for create_branch_step should be updated to validate the new Result-based logic, ensuring all success and error paths are handled correctly.

📊 Logs

  • No new log events

✅ Checklist

  • Self-review done
  • Follows the project's logging rules (no secrets, no content in logs)
  • New and existing tests pass
  • Documentation updated if needed

@r-pedraza r-pedraza self-assigned this Feb 24, 2026
@wiz-b6e4a6c509
Copy link

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 1 Medium
Software Management Finding Software Management Findings -
Total 1 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.


# Render template
try:
template = Template(template_content)

Choose a reason for hiding this comment

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

Medium SAST Finding

Jinja2 template rendering without autoescaping (CWE-116)

More Details

The application was found using the Jinja2 templating engine without enabling autoescaping. This could lead to Cross-Site Scripting (XSS) vulnerabilities when rendering user-supplied input in an HTML context. XSS attacks allow an attacker to inject malicious scripts into web pages, potentially compromising sensitive data or hijacking user sessions.

Attribute Value
Impact Medium
Likelihood Medium

Remediation

Jinja2 is a popular templating engine for Python that allows developers to generate dynamic HTML pages. By default, Jinja2 does not automatically escape user-supplied data, which can lead to Cross-Site Scripting (XSS) vulnerabilities if the data is rendered in the HTML output without proper sanitization. XSS vulnerabilities allow attackers to inject malicious scripts into web pages, potentially compromising the application and its users.

To mitigate this vulnerability, Jinja2 provides an autoescape option that automatically escapes all variables rendered in the template. When autoescape is set to True, Jinja2 will escape all variables by default, preventing XSS attacks. It is recommended to enable autoescape globally for the entire application or selectively for specific templates.

Code examples:

// VULNERABLE CODE - Jinja2 environment without autoescape enabled
import jinja2

env = jinja2.Environment()
template = env.from_string("Hello {{ name }}")
print(template.render(name="<script>alert('XSS')</script>"))

// SECURE CODE - Jinja2 environment with autoescape enabled
import jinja2

env = jinja2.Environment(autoescape=True)
template = env.from_string("Hello {{ name }}")
print(template.render(name="<script>alert('XSS')</script>"))

Additional recommendations:

  • Follow the OWASP XSS Prevention Cheat Sheet for best practices on handling user input and preventing XSS vulnerabilities.
  • Regularly update your dependencies, including Jinja2, to ensure you have the latest security patches and features.
  • Consider using a Content Security Policy (CSP) to further mitigate XSS risks by restricting the execution of inline scripts and other potentially dangerous content.
  • Implement input validation and sanitization mechanisms to ensure user input is properly validated and sanitized before rendering.
  • Conduct regular security testing, including penetration testing and code reviews, to identify and address potential vulnerabilities.

Rule ID: WS-I007-PYTHON-00008


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate


[plugins.jira]
enabled = false
enabled = true
Copy link
Collaborator

Choose a reason for hiding this comment

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

This project does not use Jira so this should be false

find_ready_to_dev_transition,
transition_issue_to_ready_for_dev,
)
from titan_plugin_jira.models import UITransition
Copy link
Collaborator

Choose a reason for hiding this comment

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

This does not exist, the model is called UiJiraTransition. Also you are instanciating UITransition(has_screen=False) but this does not exist in UiJiraTransition


return self._metadata_service.list_project_versions(key)

def get_priorities(self) -> ClientResult[List["UIPriority"]]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment belong to line 333 not to this, but I couldn't add it there. Now you are here, if you can change this: ClientResult[List[NetworkJiraIssueType]] UI Model should be returned.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also this ones:

  def list_statuses(...)       -> ClientResult[List[dict]]:
  def get_current_user(...)    -> ClientResult[dict]:
  def list_project_versions(...)-> ClientResult[List[dict]]:

This should return UIModel

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also in this function 'get_priorities' makes a map, and this should be done in the service. Never map in the client

Copy link
Collaborator

Choose a reason for hiding this comment

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

In the function create_issue, there are Busssines logic here that () should be in the service IssueService or in the operator.

I mean this:

if isinstance(issue_types_result, ClientError):  # ❌ también: isinstance en vez de match
    return issue_types_result

issue_type_obj = None
for it in issue_types_result.data:    # ← lógica de búsqueda en el Client
    if it.name.lower() == issue_type.lower():


def find_ready_to_dev_transition(
jira_client: "JiraClient", issue_key: str
) -> ClientResult["UITransition"]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should not return the ClientResult, just the UITransition


def transition_issue_to_ready_for_dev(
jira_client: "JiraClient", issue_key: str
) -> ClientResult[None]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

ClientResult, should not be returned in operations. It's just to Client/Service. The step that calls this should call try/except

- index: Zero-based index if valid, None otherwise
- error_message: Error description if invalid, None otherwise

Examples:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove the examples from the docstrings


# Get priorities from Jira
priorities = None
try:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should not use try/except here, just send ClientError or ClientResult


selected_type = issue_types[index]

if not selected_type:
Copy link
Collaborator

Choose a reason for hiding this comment

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

It should never be false here

match transition_result:
case ClientSuccess():
# Get transition details to show user
find_result = ctx.jira.get_transitions(issue_key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You are calling the api twice. In line 128 you have already called the api. You need to add the necessary data to the clientSuccess.message, or in the data model

value = int(selection)
index = value - 1

if index < 0 or index >= max_value:
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should validate min_value as well

"""
ctx.textual.begin_step(StepTitles.DESCRIPTION)

ctx.textual.markdown("## 📝 Task Description")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should not be a Markdown. Maybe better a bold_text

"""
ctx.textual.begin_step(StepTitles.ISSUE_TYPE)

ctx.textual.markdown("## 🏷️ Issue Type")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should not be a Markdown, maybe better a bold_text

"""
ctx.textual.begin_step(StepTitles.PRIORITY)

ctx.textual.markdown("## 🔥 Priority")
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should not be a markdown. Check the rest of the steps, cause the title of something's should not be a markdown,

Copy link
Collaborator

@finxo finxo left a comment

Choose a reason for hiding this comment

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

Also testing the PR I found this bug:

────────────────────────────────────────────────────────────────────────────────
 SESSION START  2026-03-02 10:14:41 UTC   PID 85720
────────────────────────────────────────────────────────────────────────────────
{"version": "0.1.11", "mode": "development", "log_level": "WARNING", "pid": 85720, "log_file": "/home/alex/.local/state/titan/logs/titan.log", "event": "session_started", "level": "info", "logger": "titan", "timestamp": "2026-03-02T10:14:41.119017Z"}
{"name": "git", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.150177Z"}
{"name": "jira", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.226858Z"}
{"name": "github", "event": "plugin_initialized", "level": "info", "logger": "titan_cli.core.plugins.plugin_registry", "timestamp": "2026-03-02T10:14:41.591243Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.015, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:41.623199Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.022, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:42.668828Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.021, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:14:45.829840Z"}
{"message": "Status retrieved", "result_type": "UIGitStatus", "duration": 0.021, "event": "get_status_success", "level": "info", "logger": "titan_plugin_git.clients.services.status_service", "timestamp": "2026-03-02T10:15:00.475315Z"}
{"workflow": "Create Jira Issue", "source": "plugin", "total_steps": 7, "is_nested": false, "event": "workflow_started", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:00.581044Z"}
{"workflow": "Create Jira Issue", "step_id": "description", "message": "Brief description captured: 87 characters", "duration": 16.898, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:17.485003Z"}
{"message": "Found 21 issue types", "result_type": "list", "duration": 0.164, "event": "get_issue_types_success", "level": "info", "logger": "titan_plugin_jira.clients.services.metadata_service", "timestamp": "2026-03-02T10:15:17.657802Z"}
{"workflow": "Create Jira Issue", "step_id": "issue_type", "message": "Issue type selected: Epic", "duration": 13.333, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:30.846453Z"}
{"workflow": "Create Jira Issue", "step_id": "priority", "message": "Priority selected: Highest", "duration": 10.62, "event": "step_success", "level": "info", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:41.478548Z"}
{"event": "HTTP Request: POST https://llm.tools.cloud.masorange.es/v1/messages \"HTTP/1.1 200 OK\"", "timestamp": "2026-03-02T10:15:58.747430Z"}
{"workflow": "Create Jira Issue", "step_id": "generate_with_ai", "error": "Error executing step 'ai_enhance_issue_description' from plugin 'jira': object of type 'NoneType' has no len()", "on_error": "fail", "duration": 17.276, "event": "step_failed", "level": "error", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:58.755240Z"}
{"workflow": "Create Jira Issue", "failed_at_step": "generate_with_ai", "error": "Error executing step 'ai_enhance_issue_description' from plugin 'jira': object of type 'NoneType' has no len()", "steps_completed": 4, "duration": 58.186, "event": "workflow_failed", "level": "error", "logger": "titan_cli.ui.tui.textual_workflow_executor", "timestamp": "2026-03-02T10:15:58.761589Z"}
● Encontré el bug. La cadena del problema:                                                                                                                                                                                         
                             
  1. _parse_ai_response inicializa "title": ""                                                                                                                                                                                     
  2. En el cleanup final: si queda vacío, lo pone a None → sections["title"] = None                                                                                                                                                
  3. parsed.pop("title", DEFAULT_TITLE) devuelve None (la clave existe pero vale None — el default solo aplica si la clave no existe)                                                                                              
  4. len(None) → TypeError                                                                                                                                                                                                                                              

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.

2 participants