Skip to content

Commit 2de1f12

Browse files
authored
fix: handle required Epic Name fields during creation (sooperset#568)
* fix: handle required Epic Name fields during creation - Check field requirements before deciding placement - Include required Epic fields in initial creation request - Keep optional Epic fields for post-creation update - Add project_key parameter to enable requirement lookup Github-Issue: sooperset#457 * fix: update test_create_epic mock to match new method signature The prepare_epic_fields method now accepts 5 parameters including project_key, so the test mock's side_effect function needs to be updated accordingly. Reported-by:Hyeonsoo Lee Github-Issue:sooperset#568 * perf: implement caching for get_required_fields to improve Epic creation performance - Add caching mechanism in FieldsMixin.get_required_fields to avoid repeated API calls - Remove unnecessary hasattr check in EpicsMixin since get_required_fields is guaranteed - Add get_required_fields to FieldsOperationsProto for proper type checking - Performance improvement critical for bulk Epic creation operations The caching uses a tuple of (project_key, issue_type) as the cache key to store required field definitions per project/issue type combination. This prevents redundant API calls when creating multiple Epics in the same project. Github-Issue: sooperset#568 * refactor: simplify Jira protocol tests by removing redundant classes and methods - Removed multiple test classes and methods related to protocol definitions and method signatures for Jira operations. - Consolidated tests to focus on protocol compliance checking. - This cleanup reduces code complexity and improves maintainability of the test suite. No functional changes were made to the protocol implementations themselves. * revert: uv.lock
1 parent 865dee7 commit 2de1f12

File tree

7 files changed

+316
-200
lines changed

7 files changed

+316
-200
lines changed

src/mcp_atlassian/jira/epics.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ def _try_discover_fields_from_existing_epic(
111111
logger.error(f"Error discovering fields from existing Epic: {str(e)}")
112112

113113
def prepare_epic_fields(
114-
self, fields: dict[str, Any], summary: str, kwargs: dict[str, Any]
114+
self,
115+
fields: dict[str, Any],
116+
summary: str,
117+
kwargs: dict[str, Any],
118+
project_key: str = None,
115119
) -> None:
116120
"""
117121
Prepare epic-specific fields for issue creation.
@@ -120,33 +124,48 @@ def prepare_epic_fields(
120124
fields: The fields dictionary to update
121125
summary: The issue summary that can be used as a default epic name
122126
kwargs: Additional fields from the user
127+
project_key: Optional project key for checking field requirements
123128
"""
124129
try:
125130
# Get all field IDs
126131
field_ids = self.get_field_ids_to_epic()
127132
logger.info(f"Discovered Jira field IDs for Epic creation: {field_ids}")
128133

129-
# Store Epic-specific fields in kwargs for later update
130-
# This is critical because the Jira API might reject these fields during creation
131-
# due to screen configuration restrictions
134+
# Get required fields for Epic issue type if project_key provided
135+
required_fields = {}
136+
if project_key:
137+
try:
138+
required_fields = self.get_required_fields("Epic", project_key)
139+
logger.debug(
140+
f"Required fields for Epic in project {project_key}: {list(required_fields.keys())}"
141+
)
142+
except Exception as e:
143+
logger.warning(f"Could not check field requirements: {e}")
132144

133-
# Extract and store epic_name for later update
145+
# Extract and handle epic_name
134146
epic_name_field = self._get_epic_name_field_id(field_ids)
135147
if epic_name_field:
136-
# Get epic name value but don't add to fields yet
148+
# Get epic name value
137149
epic_name = kwargs.pop(
138150
"epic_name", kwargs.pop("epicName", summary)
139151
) # Use summary as default if epic_name not provided
140152

141-
# Instead of adding to fields, store in kwargs under a special key
142-
# This will be used for the post-creation update
143-
kwargs["__epic_name_field"] = epic_name_field
144-
kwargs["__epic_name_value"] = epic_name
145-
logger.info(
146-
f"Storing Epic Name ({epic_name_field}: {epic_name}) for post-creation update"
147-
)
153+
# Check if this field is required
154+
if epic_name_field in required_fields:
155+
# Add to fields for initial creation
156+
fields[epic_name_field] = epic_name
157+
logger.info(
158+
f"Adding required Epic Name ({epic_name_field}: {epic_name}) to creation fields"
159+
)
160+
else:
161+
# Store for post-creation update as before
162+
kwargs["__epic_name_field"] = epic_name_field
163+
kwargs["__epic_name_value"] = epic_name
164+
logger.info(
165+
f"Storing optional Epic Name ({epic_name_field}: {epic_name}) for post-creation update"
166+
)
148167

149-
# Extract and store epic_color for later update
168+
# Extract and handle epic_color
150169
epic_color_field = self._get_epic_color_field_id(field_ids)
151170
if epic_color_field:
152171
epic_color = (
@@ -156,23 +175,41 @@ def prepare_epic_fields(
156175
or "green" # Default color
157176
)
158177

159-
# Store for post-creation update
160-
kwargs["__epic_color_field"] = epic_color_field
161-
kwargs["__epic_color_value"] = epic_color
162-
logger.info(
163-
f"Storing Epic Color ({epic_color_field}: {epic_color}) for post-creation update"
164-
)
178+
# Check if this field is required
179+
if epic_color_field in required_fields:
180+
# Add to fields for initial creation
181+
fields[epic_color_field] = epic_color
182+
logger.info(
183+
f"Adding required Epic Color ({epic_color_field}: {epic_color}) to creation fields"
184+
)
185+
else:
186+
# Store for post-creation update
187+
kwargs["__epic_color_field"] = epic_color_field
188+
kwargs["__epic_color_value"] = epic_color
189+
logger.info(
190+
f"Storing optional Epic Color ({epic_color_field}: {epic_color}) for post-creation update"
191+
)
165192

166-
# Store any other epic-related fields for later update
193+
# Handle any other epic-related fields
167194
for key, value in list(kwargs.items()):
168195
if key.startswith("epic_") and key in field_ids:
169196
field_key = key.replace("epic_", "")
170-
# Store for post-creation update
171-
kwargs[f"__epic_{field_key}_field"] = field_ids[key]
172-
kwargs[f"__epic_{field_key}_value"] = value
173-
logger.info(
174-
f"Storing Epic field ({field_ids[key]} from {key}: {value}) for post-creation update"
175-
)
197+
field_id = field_ids[key]
198+
199+
# Check if this field is required
200+
if field_id in required_fields:
201+
# Add to fields for initial creation
202+
fields[field_id] = value
203+
logger.info(
204+
f"Adding required Epic field ({field_id} from {key}: {value}) to creation fields"
205+
)
206+
else:
207+
# Store for post-creation update
208+
kwargs[f"__epic_{field_key}_field"] = field_id
209+
kwargs[f"__epic_{field_key}_value"] = value
210+
logger.info(
211+
f"Storing optional Epic field ({field_id} from {key}: {value}) for post-creation update"
212+
)
176213
kwargs.pop(key) # Remove from kwargs to avoid duplicate processing
177214

178215
# Warn if epic_name field is required but wasn't discovered

src/mcp_atlassian/jira/fields.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ def get_required_fields(self, issue_type: str, project_key: str) -> dict[str, An
188188
Returns:
189189
Dictionary mapping required field names to their definitions
190190
"""
191+
# Initialize cache if it doesn't exist
192+
if not hasattr(self, "_required_fields_cache"):
193+
self._required_fields_cache = {}
194+
195+
# Check cache first
196+
cache_key = (project_key, issue_type)
197+
if cache_key in self._required_fields_cache:
198+
logger.debug(
199+
f"Returning cached required fields for {issue_type} in {project_key}"
200+
)
201+
return self._required_fields_cache[cache_key]
202+
191203
try:
192204
# Step 1: Get the ID for the given issue type name within the project
193205
if not hasattr(self, "get_project_issue_types"):
@@ -236,6 +248,13 @@ def get_required_fields(self, issue_type: str, project_key: str) -> dict[str, An
236248
f"in project '{project_key}'"
237249
)
238250

251+
# Cache the result before returning
252+
self._required_fields_cache[cache_key] = required_fields
253+
logger.debug(
254+
f"Cached required fields for {issue_type} in {project_key}: "
255+
f"{len(required_fields)} fields"
256+
)
257+
239258
return required_fields
240259

241260
except Exception as e:

src/mcp_atlassian/jira/issues.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,11 +732,19 @@ def _prepare_epic_fields(
732732
summary: The epic summary
733733
kwargs: Additional fields from the user
734734
"""
735-
# Delegate to EpicsMixin.prepare_epic_fields
735+
# Extract project_key from fields if available
736+
project_key = None
737+
if "project" in fields:
738+
if isinstance(fields["project"], dict):
739+
project_key = fields["project"].get("key")
740+
elif isinstance(fields["project"], str):
741+
project_key = fields["project"]
742+
743+
# Delegate to EpicsMixin.prepare_epic_fields with project_key
736744
# Since JiraFetcher inherits from both IssuesMixin and EpicsMixin,
737745
# this will correctly use the prepare_epic_fields method from EpicsMixin
738746
# which implements the two-step Epic creation approach
739-
self.prepare_epic_fields(fields, summary, kwargs)
747+
self.prepare_epic_fields(fields, summary, kwargs, project_key)
740748

741749
def _prepare_parent_fields(
742750
self, fields: dict[str, Any], kwargs: dict[str, Any]

src/mcp_atlassian/jira/protocols.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ def update_epic_fields(self, issue_key: str, kwargs: dict[str, Any]) -> JiraIssu
9191

9292
@abstractmethod
9393
def prepare_epic_fields(
94-
self, fields: dict[str, Any], summary: str, kwargs: dict[str, Any]
94+
self,
95+
fields: dict[str, Any],
96+
summary: str,
97+
kwargs: dict[str, Any],
98+
project_key: str = None,
9599
) -> None:
96100
"""
97101
Prepare epic-specific fields for issue creation.
@@ -153,6 +157,19 @@ def get_field_ids_to_epic(self) -> dict[str, str]:
153157
(e.g., {'epic_link': 'customfield_10014', 'epic_name': 'customfield_10011'})
154158
"""
155159

160+
@abstractmethod
161+
def get_required_fields(self, issue_type: str, project_key: str) -> dict[str, Any]:
162+
"""
163+
Get required fields for creating an issue of a specific type in a project.
164+
165+
Args:
166+
issue_type: The issue type (e.g., 'Bug', 'Story', 'Epic')
167+
project_key: The project key (e.g., 'PROJ')
168+
169+
Returns:
170+
Dictionary mapping required field names to their definitions
171+
"""
172+
156173

157174
@runtime_checkable
158175
class ProjectsOperationsProto(Protocol):

0 commit comments

Comments
 (0)