Skip to content

feat: Add foundation for Jira issue creation workflow#166

Open
r-pedraza wants to merge 17 commits intomasterfrom
feat/crerate-issue-jira-core
Open

feat: Add foundation for Jira issue creation workflow#166
r-pedraza wants to merge 17 commits 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-b6e4a6c509 bot commented Feb 24, 2026

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.


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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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


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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already fixed. The function now returns None and raises exceptions instead of returning ClientResult[None], following the operations layer pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already resolved. Function returns UITransition (UI model) and raises exceptions, following the operations pattern correctly.


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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"""
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
Contributor Author

Choose a reason for hiding this comment

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

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                                                                                                                                                                                                                                              

@r-pedraza
Copy link
Contributor Author

@r-pedraza r-pedraza requested a review from finxo March 19, 2026 09:40
)

except Exception as e:
import traceback
Copy link
Collaborator

Choose a reason for hiding this comment

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

imports goes up in the file not here

@@ -0,0 +1,167 @@
# Plantillas Personalizadas para Issues
Copy link
Collaborator

Choose a reason for hiding this comment

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

Never add docs in spanish

result = transition_issue_to_ready_for_dev(mock_client, "TEST-123")

# Assert
assert result is None
Copy link
Collaborator

Choose a reason for hiding this comment

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

The function transition_issue_to_ready_to_dev returns UIJiraTransition not none. This is wrong test

result = transition_issue_to_ready_for_dev(mock_client, "TEST-123")

# Assert
assert result is None
Copy link
Collaborator

Choose a reason for hiding this comment

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

The function transition_issue_to_ready_to_dev returns UIJiraTransition not none. This is wrong test

error_code="CREATE_SUBTASK_ERROR"
)

# ==================== INTERNAL HELPERS ====================
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove this kind of comments



def find_ready_to_dev_transition(
jira_client: "JiraClient", issue_key: str
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is the problem of type_checking, you are returning a string not a class. SAme in the rest of the file

# Call AI
with ctx.textual.loading("Generating description with AI..."):
try:
from titan_cli.ai.models import AIMessage
Copy link
Collaborator

Choose a reason for hiding this comment

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

imports goes up in the file

Centralized to avoid hardcoding and enable easy i18n in the future.
"""

# ==================== Step Titles ====================
Copy link
Collaborator

Choose a reason for hiding this comment

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

Avoid this kind of comments

from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step
from .steps.list_versions_step import list_versions_step

# Technical Specification Workflow steps (COMMENTED - steps don't exist)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this does not exist remove them

"ai_analyze_issue_requirements": ai_analyze_issue_requirements_step,
"list_versions": list_versions_step,

# Technical Specification Workflow steps (COMMENTED - steps don't exist)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this steps does not exist remove them

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 return a UIModel not Network one

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 return a UIModel not a dict

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 return an UI Model not dict

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 return a UIModel not List[dict]

@r-pedraza
Copy link
Contributor Author

1 similar comment
@r-pedraza
Copy link
Contributor Author

@r-pedraza r-pedraza requested a review from finxo March 24, 2026 18:43
priority=priority
)
# Find issue type (delegated to operation)
issue_type_result = find_issue_type_by_name(self, project_key, 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.

Client is calling an operation and passing itself as a parameter. This is backwards - operations should call the client, not vice versa. This violates the 5-layer architecture where Client (layer 3) should never call Operations (layer 2).

description=description
)
# Find subtask issue type (delegated to operation)
subtask_result = find_subtask_issue_type(self, self.project_key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Client is calling an operation and passing itself as a parameter. This is backwards - operations should call the client, not vice versa. This violates the 5-layer architecture where Client (layer 3) should never call Operations (layer 2).

)

except Exception as e:
import traceback
Copy link
Collaborator

Choose a reason for hiding this comment

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

imports go up in the file, not in the exception handler. Move this to the top of the file with other imports.

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