Comprehensive Migration from Redmine to GitLab
How EasyLauncher engineered an automated data migration platform to transfer 10+ years of legacy project tracking, 25,000+ tickets, attachments, and complex wikis from Redmine into a unified enterprise GitLab environment.

Executive Summary
Legacy project tracking systems often suffer from organizational inertia. For a leading enterprise engineering organization, Redmine had served as the central ticket repository, defect tracker, and documentation wiki for over a decade. However, maintaining Redmine alongside modern git version control systems created massive friction.
Developers worked in two disconnected worlds: writing and reviewing code in modern git workflows while manually updating status notes, pasting URLs, and referencing defect IDs in an aging Ruby on Rails Redmine application. Crucial context was routinely lost in context switching, plugin breakdowns, and broken database attachments.
“When source code, CI/CD pipelines, and issue trackers are separated by different tools, developers spend more time syncing status than shipping software.”
EasyLauncher DevOps Engineering PracticeEasyLauncher was commissioned to execute an automated, high-fidelity migration from Redmine to GitLab Enterprise. The project unified source code, automated pipelines, issues, milestones, and documentation into a single enterprise pane of glass while maintaining 100% fidelity across 25,000+ historical issues, comment threads, attachments, and user attributions.
The Legacy Redmine Fragmentation Challenge
Prior to engineering automated data extractors, EasyLauncher audited the Redmine database schema and operational dependencies:
No Native Code Integration
Engineers spent hours manually cross-referencing commit hashes and pull requests with Redmine issue IDs, causing compliance gaps and missed bug tracking.
60+ GB of Orphaned Assets
Historical screenshots, architecture specs, and log dumps were scattered across unindexed server directories with inconsistent filename encoding and broken database pointers.
Textile Markup & Raw HTML
Over 1,400 wiki pages used legacy Textile syntax and nested HTML tags that rendered as broken, unreadable code when pasted directly into modern markdown systems.
Inactive & Renamed Users
Over a decade, hundreds of developers had departed, changed email addresses, or used varied usernames across different Redmine project boards.
Automated Relational Mapping Engine
To prevent data truncation or lost relationships, EasyLauncher developed a custom Python-based migration pipeline utilizing the Redmine REST API and direct MySQL read replicas:
- Issue Hierarchy & Epics: Extracted hierarchical parent-child ticket trees and mapped them cleanly to GitLab Epics, Issues, and Sub-tasks with preserved nesting.
- Comments & Audit Logs: Transferred full journal histories, including author name, original timestamp, status changes, and assigned field transitions into formatted GitLab discussion threads.
- Custom Field Translation: Converted 40+ project-specific custom fields into scoped GitLab labels (e.g.,
type::bug,priority::high), ensuring searchability across filtered issue boards.
User Identity Normalization & SSO Mapping
A primary risk in issue migrations is attributing historical tickets to a single generic administrative user. EasyLauncher engineered an automated user reconciliation pipeline:
Three-Tier Identity Resolution Strategy
- Exact SAML / Active Directory Match: Active employees were matched via enterprise corporate email addresses and mapped directly to authenticated GitLab accounts.
- Historical Alias Resolution: Mapped legacy usernames, secondary email addresses, and name formatting variations using a deterministic JSON reconciliation dictionary.
- Archival Attribution Footnote: For departed employees without GitLab accounts, tickets were authored with a clear, audited provenance header: “Originally authored by John Doe (jdoe_legacy) on October 14, 2018”, preserving regulatory audit compliance.
Wiki HTML to GitLab Markdown Transpilation
Over 1,400 engineering wikis, architectural runbooks, and onboarding guides existed inside Redmine formatted in complex Textile markup. We deployed an automated compiler:
- Parsed Textile table syntax (
|_. Header 1 |_. Header 2 |) into clean GitHub/GitLab Flavored Markdown tables. - Converted internal Redmine page links (
[[Project Architecture]]) into relative GitLab wiki links ([Project Architecture](project-architecture)). - Extracted all inline screenshots, uploaded them into GitLab git-backed wiki storage, and updated image paths automatically.
Migration Tooling Comparison
Why the EasyLauncher custom migration suite delivered superior data fidelity compared to open-source scripts:
| Feature Dimension | Standard CSV Export | Generic Community Scripts | EasyLauncher Migration Suite |
|---|---|---|---|
| Creation Date Preservation | Overwritten with import date | Overwritten with import date | 100% exact historical timestamps |
| Comment Author Attribution | Single admin user | Partial username tags | Sudo impersonation + SAML mapping |
| File Attachments | Excluded completely | Frequent HTTP timeouts | SHA-256 validated direct upload |
| Wiki & Markup Formatting | Not supported | Raw broken Textile strings | Full GFM Markdown transpilation |
Relational Extraction Blueprint
Representative extraction snippet demonstrating timestamp preservation and administrative impersonation:
import requests
def migrate_redmine_issue(issue_data, gitlab_project_id, admin_token):
url = f"https://gitlab.enterprise.internal/api/v4/projects/{gitlab_project_id}/issues"
headers = {
"Private-Token": admin_token,
# Sudo impersonation preserves authentic author attribution
"Sudo": issue_data["author_gitlab_username"]
}
payload = {
"title": issue_data["subject"],
"description": issue_data["converted_markdown_body"],
"created_at": issue_data["created_on"], # Preserves historical ISO 8601 timestamp
"labels": issue_data["scoped_labels"],
"milestone_id": issue_data["matched_milestone_id"],
"weight": issue_data["story_points"]
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()Verified Outcomes & Productivity ROI
Consolidating into GitLab Enterprise delivered sweeping productivity improvements across all engineering pods:
Every defect, feature request, and customer issue transferred with full comment chains and attachments.
Native branch creation directly from tickets and automatic issue closing via commit keywords eliminated administrative tracking overhead.
Over 60 GB of design specs, PDFs, and diagnostic logs preserved and relinked seamlessly into GitLab storage.
Decommissioned 4 legacy Ruby application servers and eliminated recurring Redmine hosting, backup, and plugin licensing costs.
Frequently Asked Questions
Common questions regarding legacy issue tracking migrations and GitLab consolidation:
How do you preserve original creation dates and author attributions when migrating into GitLab?
Standard API creation calls overwrite the author and creation timestamp with the API token owner and current time. We utilized GitLab sudo and administrative impersonation tokens along with direct created_at overrides via the GitLab Issues API to preserve original author usernames, creation dates, and full historical comment timelines.
How are complex Redmine textile formatting and raw HTML tables converted into GitLab markdown?
We built an automated syntax converter using regular expressions and an AST parser. It translates Redmine Textile syntax (such as h1., *bold*, @code@, and inline tables) into standard GitLab Flavored Markdown (GFM), including syntax-highlighted code fences and native markdown tables.
What happens to large file attachments, design PDFs, and screenshots stored in Redmine?
Attachments were extracted from Redmine disk storage, hashed using SHA-256 for integrity verification, and uploaded via GitLab project upload endpoints. Every issue description and comment containing an inline image reference was rewritten to link to the new authenticated GitLab upload URL.
How are Redmine subtasks, related issues, and blockers mapped into GitLab?
Redmine parent-child relationships and issue relations (blocks, precedes, relates_to) were extracted into a relational graph. We mapped parent-child tickets into GitLab Epics and Tasks, and translated issue dependencies into GitLab Linked Issues ('is blocked by', 'relates to').
Can custom fields and status workflows from Redmine be preserved in GitLab?
Yes. Redmine custom fields (such as 'Root Cause', 'Customer Severity', or 'Billable') were mapped into scoped GitLab labels (e.g., 'severity::p1', 'category::backend') and GitLab Issue Custom Fields, ensuring searchability and reporting continuity without data truncation.
Consolidating Legacy Toolchains into Modern DevOps?
EasyLauncher helps technology organizations unify fragmented project trackers, git repositories, and CI/CD pipelines into modern, high-velocity developer platforms.
Explore More Case Stories
Discover how we help enterprises optimize operations, automate workflows, and scale architectures.


