DEV Community

Cover image for Solved: Migrate Jira Issues to GitLab Issues: Mapping Fields via API
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Migrate Jira Issues to GitLab Issues: Mapping Fields via API

🚀 Executive Summary

TL;DR: Organizations often struggle with migrating Jira issues to GitLab due to tool sprawl and the inefficiencies of manual data entry. This guide provides a comprehensive Python-based solution, leveraging Jira and GitLab APIs to programmatically fetch, map, and create issues, ensuring a smooth and efficient transition.

🎯 Key Takeaways

  • Programmatic migration of Jira issues to GitLab is achieved using Python, leveraging both Jira and GitLab REST APIs for data extraction and ingestion.
  • Jira’s API pagination is handled by iteratively fetching issues using JQL queries with startAt and maxResults parameters, authenticated via Basic Authentication with an API token.
  • Custom field mapping transforms Jira fields like summary, description, labels, issuetype, priority, and status into GitLab’s title, description, and labels fields.
  • GitLab issue creation requires a Personal Access Token with api scope and involves sending POST requests to the /api/v4/projects/{project\_id}/issues endpoint with the mapped payload.
  • Mitigating API rate limits is crucial, typically by introducing small delays (e.g., time.sleep(0.5)) between successive API calls to prevent 429 Too Many Requests errors.

Migrate Jira Issues to GitLab Issues: Mapping Fields via API

As a Senior DevOps Engineer at TechResolve, I’ve seen countless organizations grapple with tool sprawl and the complexities of data migration. Whether you’re consolidating your SDLC tools under a single roof, moving away from a costly proprietary system, or simply standardizing on GitLab for better integration, migrating existing issue data is often a formidable challenge. Manual issue creation is not just boring; it’s a productivity drain, a breeding ground for errors, and simply unsustainable for any significant volume of data.

This tutorial provides a comprehensive, step-by-step guide to programmatically migrate your Jira issues to GitLab, complete with custom field mapping. We’ll leverage the power of both Jira and GitLab APIs, primarily using Python, to ensure a smooth and efficient transition. By the end of this guide, you’ll have a robust script capable of fetching issues from Jira, transforming their data, and creating them as new issues in GitLab.

Prerequisites

Before diving into the migration script, ensure you have the following in place:

  • Jira Instance: Access to a Jira Cloud or Jira Server instance.
  • Jira API Token: For Jira Cloud, an API token generated from your Atlassian account. For Jira Server, you’ll typically use a username and password. Ensure the user has permissions to read issues.
  • GitLab Instance: Access to a GitLab SaaS or Self-Managed instance.
  • GitLab Personal Access Token: A Personal Access Token (PAT) with at least the api scope. Ensure the user associated with the PAT has permissions to create issues in the target project.
  • Python 3.x: Installed on your local machine.
  • Python requests library: Install it using pip install requests.
  • Understanding of JSON: Familiarity with JSON data structures will be beneficial.
  • Target GitLab Project ID: The numeric ID of the GitLab project where issues will be migrated.

Step-by-Step Guide: Programmatic Issue Migration

Step 1: Gather API Credentials and Endpoints

First, let’s set up our environment by defining the necessary API credentials and endpoints. This will make our script flexible and easy to configure.

# api_config.py
JIRA_BASE_URL = "https://your-jira-domain.atlassian.net"  # e.g., "https://yourcompany.atlassian.net"
JIRA_USER_EMAIL = "your-email@example.com"                # Jira Cloud email
JIRA_API_TOKEN = "YOUR_JIRA_API_TOKEN"                    # Jira Cloud API Token

GITLAB_BASE_URL = "https://gitlab.com"                    # e.g., "https://gitlab.com" or "https://your-gitlab.com"
GITLAB_PRIVATE_TOKEN = "YOUR_GITLAB_PRIVATE_TOKEN"        # GitLab Personal Access Token
GITLAB_PROJECT_ID = 12345678                                # Target GitLab Project ID (numeric)

# Jira JQL query to filter issues. Example: all issues in project "PROJ"
JIRA_JQL_QUERY = "project = PROJ ORDER BY created ASC"

# Optional: Number of issues to fetch per page (Jira API pagination)
JIRA_PAGE_SIZE = 50
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: We’re centralizing all configuration parameters in a single file. Replace the placeholder values with your actual Jira domain, email, API tokens, GitLab URL, PAT, and target project ID. The JIRA_JQL_QUERY allows you to specify which Jira issues you want to migrate using Jira Query Language.

Step 2: Extract Issues from Jira

Next, we’ll write a Python function to connect to your Jira instance, fetch issues using the Jira REST API, and handle pagination to retrieve all desired issues.

# jira_extractor.py
import requests
import base64
from api_config import JIRA_BASE_URL, JIRA_USER_EMAIL, JIRA_API_TOKEN, JIRA_JQL_QUERY, JIRA_PAGE_SIZE

def get_all_jira_issues():
    issues = []
    start_at = 0
    total = -1

    headers = {
        "Accept": "application/json",
        "Authorization": "Basic " + base64.b64encode(f"{JIRA_USER_EMAIL}:{JIRA_API_TOKEN}".encode()).decode()
    }

    print("Fetching issues from Jira...")
    while total == -1 or start_at < total:
        url = f"{JIRA_BASE_URL}/rest/api/3/search?jql={JIRA_JQL_QUERY}&startAt={start_at}&maxResults={JIRA_PAGE_SIZE}"
        try:
            response = requests.get(url, headers=headers)
            response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
            data = response.json()

            issues.extend(data.get("issues", []))
            total = data.get("total", 0)
            start_at += len(data.get("issues", []))
            print(f"Fetched {len(issues)} of {total} issues...")

        except requests.exceptions.HTTPError as err:
            print(f"HTTP error occurred: {err} - Response: {err.response.text}")
            break
        except requests.exceptions.ConnectionError as err:
            print(f"Connection error occurred: {err}")
            break
        except Exception as err:
            print(f"An unexpected error occurred: {err}")
            break

    print(f"Successfully fetched {len(issues)} issues from Jira.")
    return issues

if __name__ == "__main__":
    jira_issues = get_all_jira_issues()
    # You can inspect the fetched issues here
    # import json
    # with open("jira_issues.json", "w") as f:
    #     json.dump(jira_issues, f, indent=2)
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: This script uses a while loop to handle Jira’s API pagination. It sends authenticated GET requests to the Jira /rest/api/3/search endpoint, using Basic Authentication with your email and API token. The JQL query specifies which issues to retrieve. Each response is parsed, and issues are appended to a list until all issues matching the JQL are fetched. Error handling for network and HTTP issues is included.

Step 3: Transform and Map Issue Data

This is where we define how Jira fields translate to GitLab fields. GitLab’s issue creation API is simpler than Jira’s, primarily focusing on title, description, and labels. We’ll map common Jira fields to these.

# mapper.py
def map_jira_to_gitlab_issue(jira_issue):
    fields = jira_issue.get("fields", {})

    # Basic field mapping
    gitlab_title = fields.get("summary", f"Jira Issue {jira_issue.get('key', '')}")

    # Combine description and other relevant fields
    gitlab_description = f"**Jira Key:** {jira_issue.get('key', 'N/A')}\n"
    gitlab_description += f"**Jira Type:** {fields.get('issuetype', {}).get('name', 'N/A')}\n"
    gitlab_description += f"**Jira Status:** {fields.get('status', {}).get('name', 'N/A')}\n"
    if fields.get("priority"):
        gitlab_description += f"**Jira Priority:** {fields.get('priority', {}).get('name', 'N/A')}\n"
    if fields.get("creator"):
        gitlab_description += f"**Jira Creator:** {fields.get('creator', {}).get('displayName', 'N/A')}\n"
    if fields.get("assignee"):
        gitlab_description += f"**Jira Assignee:** {fields.get('assignee', {}).get('displayName', 'N/A')}\n"
    gitlab_description += f"**Jira Link:** {JIRA_BASE_URL}/browse/{jira_issue.get('key', '')}\n\n"
    gitlab_description += fields.get("description", "") # Original Jira description

    # Map Jira labels and issue type to GitLab labels
    gitlab_labels = []
    if fields.get("labels"):
        gitlab_labels.extend(fields["labels"])
    if fields.get("issuetype"):
        gitlab_labels.append(f"Jira-Type::{fields['issuetype']['name']}")
    if fields.get("priority"):
        gitlab_labels.append(f"Jira-Priority::{fields['priority']['name']}")
    if fields.get("status"):
        gitlab_labels.append(f"Jira-Status::{fields['status']['name']}")

    # Ensure labels are unique and clean
    gitlab_labels = list(set([label.replace(' ', '-').replace('/', '-') for label in gitlab_labels if label]))

    # GitLab issue payload structure
    gitlab_issue_payload = {
        "title": gitlab_title,
        "description": gitlab_description,
        "labels": ",".join(gitlab_labels)
        # For assignee, milestones, etc., you would need to map Jira IDs to GitLab IDs
        # This requires fetching all GitLab users/milestones and creating a lookup table.
        # Example: "assignee_ids": [gitlab_user_id]
        # Example: "milestone_id": gitlab_milestone_id
    }
    return gitlab_issue_payload
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: This function takes a raw Jira issue JSON object and transforms it into a dictionary suitable for the GitLab Issues API. It maps the Jira summary to GitLab’s title. The GitLab description is constructed by concatenating key Jira fields (Jira key, type, status, creator, etc.) along with a direct link back to the original Jira issue, followed by the original Jira description. Jira’s labels, issuetype, priority, and status are all converted into GitLab labels for better categorization within GitLab. We also perform some basic sanitation on labels.

Step 4: Ingest Issues into GitLab

Finally, we’ll create the function to send the mapped issue data to GitLab using its Issues API, creating new issues in your target project.

# gitlab_ingestor.py
import requests
from api_config import GITLAB_BASE_URL, GITLAB_PRIVATE_TOKEN, GITLAB_PROJECT_ID
from mapper import map_jira_to_gitlab_issue
from jira_extractor import get_all_jira_issues
import time

def create_gitlab_issue(issue_payload):
    headers = {
        "Private-Token": GITLAB_PRIVATE_TOKEN,
        "Content-Type": "application/json"
    }
    url = f"{GITLAB_BASE_URL}/api/v4/projects/{GITLAB_PROJECT_ID}/issues"

    try:
        response = requests.post(url, headers=headers, json=issue_payload)
        response.raise_for_status()
        created_issue = response.json()
        print(f"Successfully created GitLab issue: '{created_issue.get('title')}' (ID: {created_issue.get('iid')})")
        return created_issue
    except requests.exceptions.HTTPError as err:
        print(f"Failed to create GitLab issue '{issue_payload.get('title')}'. HTTP error: {err} - Response: {err.response.text}")
    except requests.exceptions.ConnectionError as err:
        print(f"Connection error occurred while creating issue: {err}")
    except Exception as err:
        print(f"An unexpected error occurred while creating issue: {err}")
    return None

def migrate_issues():
    jira_issues = get_all_jira_issues()

    if not jira_issues:
        print("No Jira issues found to migrate.")
        return

    print(f"\nStarting migration of {len(jira_issues)} issues to GitLab...")
    for i, jira_issue in enumerate(jira_issues):
        print(f"Migrating issue {i+1}/{len(jira_issues)}: {jira_issue.get('key')} - {jira_issue.get('fields', {}).get('summary')}")
        gitlab_payload = map_jira_to_gitlab_issue(jira_issue)
        create_gitlab_issue(gitlab_payload)
        time.sleep(0.5) # Small delay to avoid hitting rate limits

    print("\nMigration complete!")

if __name__ == "__main__":
    migrate_issues()
Enter fullscreen mode Exit fullscreen mode

Logic Explanation: This script first calls our get_all_jira_issues function to fetch all issues. It then iterates through each Jira issue, uses map_jira_to_gitlab_issue to transform it, and finally sends a POST request to the GitLab Issues API endpoint (/api/v4/projects/{project_id}/issues) to create the new issue. Authentication is handled via the Private-Token header. A small delay is added between issue creations to respect API rate limits. Comprehensive error handling is included for robust operation.

Common Pitfalls

  • API Rate Limits: Both Jira and GitLab APIs have rate limits. Hitting these limits can result in 429 Too Many Requests errors. Implement exponential backoff for retries, or introduce small delays (like the time.sleep(0.5) in our example) between API calls.
  • Authentication and Permissions: Incorrect API tokens or insufficient permissions are common causes for 401 Unauthorized or 403 Forbidden errors. Double-check your Jira API token/credentials and GitLab Personal Access Token scopes. Ensure the user associated with the tokens has the necessary read (Jira) and write (GitLab) permissions.
  • Data Mismatch / Invalid Data: GitLab expects certain data formats (e.g., labels cannot contain commas if passed as a string and not an array, assignee IDs must be valid GitLab user IDs). If your mapping tries to push data that doesn’t conform to GitLab’s schema, you’ll receive a 400 Bad Request error. Carefully review the error messages in the API response text.
  • Missing Required Fields: While our script dynamically handles many fields, if a Jira issue is missing a crucial piece of data that you map to a GitLab required field (e.g., a title, though Jira issues usually have a summary), the creation might fail.

Conclusion

Migrating issues between complex systems like Jira and GitLab doesn’t have to be a daunting manual task. By leveraging their powerful APIs, you can automate the process, ensuring data integrity and saving countless hours. This tutorial provided a foundational Python script for fetching, mapping, and creating issues, covering the essential fields.

From here, you can extend this script to handle more advanced scenarios:

  • User Mapping: Map Jira user IDs to GitLab user IDs for accurate assignment. This often involves an initial API call to both systems to build a lookup table.
  • Comment Migration: Fetch Jira comments and create them as comments on the corresponding GitLab issues.
  • Attachment Migration: Download attachments from Jira and upload them to GitLab issues.
  • Advanced Field Mapping: Map custom fields, epics, sprints, or other Jira-specific entities to GitLab milestones, epics, or more descriptive labels.
  • Idempotency: Implement logic to prevent duplicate issue creation if the script is run multiple times (e.g., by storing a mapping of Jira key to GitLab IID).
  • Robust Logging: Enhance error reporting and success logging for better traceability.

Embrace the power of automation and streamline your DevOps workflows with TechResolve!


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)