This document covers the technical implementation of Jira Cloud API integration, including authentication, error handling, and batch processing.
The Jira Cloud integration provides direct import capabilities to Jira Cloud instances via the REST API v3. It includes comprehensive error handling, batch processing, and hierarchical issue type support.
src/jira_importer/import_pipeline/
├── sinks/
│ └── cloud_sink.py # Main cloud integration orchestrator
└── cloud/
├── client.py # HTTP client wrapper
├── auth.py # Authentication providers
├── mappers.py # Data mapping to Jira format
├── metadata.py # Jira metadata caching
├── bulk.py # Batch processing utilities
└── constants.py # Cloud-specific constants
graph TD
A[ProcessorResult] --> B[CloudSink]
B --> C[Authentication Test]
C --> D[Issue Classification]
D --> E[Batch Creation]
E --> F[Jira Cloud API]
F --> G[Response Processing]
G --> H[CloudSubmitReport]
Before starting any import operations, the system performs a pre-flight authentication test:
# Test authentication before proceeding
test_response = client.get("/myself")
if test_response.status_code == HTTP_OK:
user_info = test_response.json()
logger.info(f"Authentication successful - connected as: {user_info.get('displayName', 'Unknown')}")Currently supports Basic Authentication (OAuth 2.0 is scaffolded but not functional):
from ..cloud.auth import BasicAuthProvider
client = JiraCloudClient(
base_url=f"{base_url.rstrip('/')}/rest/api/3",
auth_provider=BasicAuthProvider(email=email, api_token=api_token)
)The system provides specific error messages for different authentication scenarios:
| HTTP Status | Error Message | User Action |
|---|---|---|
| 401 | Token expired | Refresh token at profile page |
| 403 | Invalid token/permissions | Check token and project access |
| 404 | Wrong site address | Verify site_address configuration |
| 429 | Rate limit exceeded | Wait and retry |
| 5xx | Server error | Contact Jira administrator |
Issue types are configured with hierarchical levels:
{
"jira": {
"issuetypes": [
{"name": "Initiative", "level": 1},
{"name": "Epic", "level": 2},
{"name": "Story", "level": 3},
{"name": "Task", "level": 3},
{"name": "Bug", "level": 3},
{"name": "Sub-Task", "level": 4}
]
}
}- Level 1 (Initiative): Can parent levels 2, 3, 4
- Level 2 (Epic): Can parent levels 3, 4
- Level 3 (Story/Task/Bug): Can parent level 4
- Level 4 (Sub-Task): Cannot parent, must have parent
The old format is still supported:
{
"jira": {
"validation": {
"issue_types": ["Epic", "Story", "Task", "Sub-task", "Bug"]
}
}
}Issues are processed in a specific order to handle parent-child relationships:
- Epics (Level 2) - Created first as standalone issues
- Stories & Tasks (Level 3) - Created with parent references to Epics
- Sub-tasks (Level 4) - Created with parent references to Stories/Tasks
BATCH_SIZE = 50 # Issues per batchEach batch includes comprehensive error handling:
if resp.status_code == HTTP_UNAUTHORIZED:
error_msg = "Authentication failed (HTTP 401) - your API token may have expired"
logger.error(error_msg)
raise ValueError(error_msg)
elif resp.status_code == HTTP_FORBIDDEN:
error_msg = "Authentication failed (HTTP 403) - your API token may be invalid or you lack permissions"
logger.error(error_msg)
raise ValueError(error_msg)
# ... additional status code handlingThe IssueMapper class handles conversion from processed rows to Jira issue payloads:
class IssueMapper:
def __init__(self, cfg: ConfigView, metadata: MetadataCache):
self.cfg = cfg
self.metadata = metadata
def map_row(self, row: list[str], indices: ColumnIndices) -> dict[str, Any]:
# Convert row data to Jira issue format
passCustom fields are automatically mapped from Excel columns to Jira issue payloads based on their configured type:
def _map_custom_fields(
self,
fields: dict[str, Any],
row: Sequence[Any],
indices: ColumnIndices,
custom_configs_by_id: dict[str, CustomFieldConfig],
) -> None:
"""Map custom fields from row data to Jira fields dict."""
for field_id, col_idx in indices.custom_fields.items():
cfg = custom_configs_by_id.get(field_id)
if cfg is None:
continue
raw = row[col_idx]
transformed = self._transform_custom_value(raw, cfg)
if transformed is not None:
fields[field_id] = transformedValue Transformation by Type:
- Text fields: Passed through as string (empty values are skipped)
- Number fields: Converted to integer or float based on value
- Date fields: Converted to ISO 8601 format (
YYYY-MM-DD) for Jira API - Select fields: Passed through as string (must match allowed values in Jira)
- Any fields: Passed through as-is without any transformation or validation
Example Mapping:
# Excel row data
row = ["Fix bug", "High", "Bug", "Important", "5", "2024-12-31"]
# Custom field configs
custom_fields = [
CustomFieldConfig(name="Notes", id="customfield_10125", type="text"),
CustomFieldConfig(name="Story Points", id="customfield_10002", type="number"),
CustomFieldConfig(name="Due Date", id="customfield_10130", type="date"),
]
# Resulting Jira payload
{
"fields": {
"summary": "Fix bug",
"priority": {"name": "High"},
"issuetype": {"name": "Bug"},
"customfield_10125": "Important", # Text field
"customfield_10002": 5, # Number field
"customfield_10130": "2024-12-31" # Date field (ISO format)
}
}Configuration Requirements:
Custom fields must be configured in either:
- JSON config:
jira.custom_fieldsarray - Excel table:
CfgCustomFieldstable in Config sheet
The name in the configuration must match the Excel column header (case-insensitive).
Jira metadata (projects, issue types, fields) is cached to reduce API calls:
class MetadataCache:
def __init__(self, client: JiraCloudClient):
self.client = client
self._cache = {}
def get_project_metadata(self, project_key: str) -> dict:
# Cache and return project metadata
passerror_str = str(e).lower()
if any(keyword in error_str for keyword in ["timeout", "connection", "network", "dns", "ssl"]):
raise ValueError(
f"Network connection failed to {base_url}. Please check your internet connection and try again. Error: {e!s}"
) from etry:
user_info = test_response.json()
logger.info(f"Authentication successful - connected as: {user_info.get('displayName', 'Unknown')}")
except (ValueError, KeyError) as json_error:
logger.warning(f"Authentication successful but received malformed response: {json_error}")
logger.info("Authentication successful - proceeding with import")# HTTP status codes for better error handling
HTTP_OK = 200
HTTP_UNAUTHORIZED = 401
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404
HTTP_TOO_MANY_REQUESTS = 429
HTTP_SERVER_ERROR_START = 500
HTTP_SERVER_ERROR_END = 600The system now properly respects the --config parameter:
# Check if a specific config file was provided (not the default)
elif args.config != DEFAULT_CONFIG_FILENAME:
# Use specified config file with specific path search
config_path = find_config_path(args.config, args.input_file, config_specific=True)
logging.debug(f"Specific config provided: {config_path}")
# Smart default: if input is Excel, try using it as config first
elif args.input_file and Path(args.input_file).suffix.lower() in {".xlsx", ".xlsm"}:
# ... smart default logic-c, --config <file>: Use specific JSON file-ce, --config-excel: Use Excel file as config source-ci, --config-input: Use config next to input file-cd, --config-default: Use default config
class JiraCloudClient:
def __init__(self, base_url: str, auth_provider: AuthProvider):
self.base_url = base_url
self.auth_provider = auth_provider
def get(self, endpoint: str) -> Response:
# Make GET request to Jira API
pass
def post(self, endpoint: str, json: dict) -> Response:
# Make POST request to Jira API
passdef build_bulk_create_payload(issues: list[dict]) -> dict:
return {
"issueUpdates": issues
}When debug mode is enabled, API payloads are written to JSON files:
def _write_payload_debug(payload: dict[str, Any], batch_num: int, output_dir: Path) -> None:
debug_file = output_dir / f"jira_cloud_payload_batch_{batch_num:03d}.json"
with open(debug_file, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False)- INFO: Authentication success, batch creation progress
- WARNING: Malformed responses, non-critical issues
- ERROR: Authentication failures, API errors
- DEBUG: Detailed request/response information
# Run cloud integration tests
python -m pytest tests/unit/test_cloud_sink.py -v# Run end-to-end cloud tests
python -m pytest tests/integration/test_cloud_integration.py -v- Default: 50 issues per batch
- Adjustable: Based on Jira instance performance
- Error handling: Individual batch failures don't stop the entire process
- Project metadata: Cached for the duration of the import
- Issue type metadata: Cached to avoid repeated API calls
- Field metadata: Cached for validation and mapping
- 429 handling: Automatic retry with exponential backoff
- Request throttling: Built into the client
- User guidance: Clear messages when rate limits are hit
- No token storage: Tokens are only held in memory during execution
- Secure transmission: HTTPS only for all API calls
- Token validation: Pre-flight testing before bulk operations
- No sensitive data: Error messages don't expose tokens or internal details
- User guidance: Clear instructions without revealing system internals
- Logging: Sensitive data is redacted in logs
- OAuth 2.0 support: For enterprise Jira instances (currently scaffolded but not functional - only Basic Auth is supported)
- Retry mechanisms: Automatic retry for transient failures
- Progress tracking: Real-time progress updates for large imports
- Validation caching: Cache validation results across runs
- Custom mappers: Support for custom issue type mappings
- Plugin system: Extensible authentication providers
- Webhook support: Real-time import status updates
- Authentication failures: Check token expiration and permissions
- Rate limiting: Implement exponential backoff
- Network issues: Handle timeouts and connection failures
- Data validation: Ensure issue types and fields exist in Jira
Enable debug mode for detailed logging:
python -m jira_importer your-data.xlsx --cloud --debugThis will:
- Write API payloads to JSON files
- Enable detailed request/response logging
- Show authentication test results
- Provide step-by-step import progress
def write_cloud(
result: ProcessorResult,
config: object,
*,
dry_run: bool = False,
output_dir: Path | None = None,
ui=None
) -> CloudSubmitReport:
"""Submit processed issues to Jira Cloud in batches."""@dataclass
class CloudSubmitReport:
created: int
failed: int
batches: int
errors: list[dict[str, Any]]
created_issue_keys: list[str] = field(default_factory=list)Note: This is a developer-focused document. For user documentation, see CONFIG.md and README.md.
:GeneratedFile