linear-migration-deep-dive by jeremylongshore
Migrate from Jira, Asana, GitHub Issues, or other tools to Linear.Use when planning a migration to Linear, executing data transfer,or mapping workflows between tools.Trigger with phrases like "migrate to linear", "jira to linear","asana to linear", "import to linear", "linear migration".
Content & Writing
1.9K Stars
264 Forks
Updated Apr 3, 2026, 03:47 AM
Why Use This
This skill provides specialized capabilities for jeremylongshore's codebase.
Use Cases
- Developing new features in the jeremylongshore repository
- Refactoring existing code to follow jeremylongshore standards
- Understanding and working with jeremylongshore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Repository claude-code-plugins-plus-skills
Skill Version
main
Community
1.9K 264
Updated At Apr 3, 2026, 03:47 AM
Skill Stats
SKILL.md 352 Lines
Total Files 2
Total Size 10.4 KB
License MIT
--- name: linear-migration-deep-dive description: | Migrate from Jira, Asana, GitHub Issues, or other tools to Linear. Use when planning a migration, executing data transfer, or mapping workflows between issue tracking tools. Trigger: "migrate to linear", "jira to linear", "asana to linear", "import to linear", "linear migration", "github issues to linear". allowed-tools: Read, Write, Edit, Bash(node:*), Bash(npx:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <[email protected]> compatible-with: claude-code, codex, openclaw tags: [saas, linear, migration, workflow] --- # Linear Migration Deep Dive ## Overview Comprehensive guide for migrating from Jira, Asana, or GitHub Issues to Linear. Covers assessment, workflow mapping, data export, transformation, batch import with hierarchy support, and post-migration validation. Linear also has a built-in importer (Settings > Import) for Jira, Asana, GitHub, and CSV. ## Prerequisites - Admin access to source system (Jira/Asana/GitHub) - Linear workspace with admin access - API keys for both source and Linear - Migration timeline and rollback plan ## Instructions ### Step 1: Migration Assessment Checklist ``` Data Volume [ ] Total issues/tasks: ___ [ ] Projects/boards: ___ [ ] Users to map: ___ [ ] Attachments: ___ [ ] Custom fields: ___ [ ] Comments: ___ Workflow Analysis [ ] Source statuses documented [ ] Status-to-state mapping defined [ ] Priority mapping defined [ ] Issue type-to-label mapping defined [ ] Automations to recreate: ___ Timeline [ ] Migration window: ___ [ ] Parallel run period: ___ [ ] Cutover date: ___ [ ] Rollback deadline: ___ ``` ### Step 2: Workflow Mapping **Jira -> Linear:** | Jira Status | Linear State (type) | |-------------|-------------------| | To Do | Todo (unstarted) | | In Progress | In Progress (started) | | In Review | In Review (started) | | Blocked | In Progress (started) + "Blocked" label | | Done | Done (completed) | | Won't Do | Canceled (canceled) | | Jira Priority | Linear Priority | |---------------|----------------| | Highest/Blocker | 1 (Urgent) | | High | 2 (High) | | Medium | 3 (Medium) | | Low/Lowest | 4 (Low) | | Jira Issue Type | Linear Label | |-----------------|-------------| | Bug | Bug | | Story | Feature | | Task | Task | | Epic | (becomes Project or parent issue) | **Asana -> Linear:** | Asana Section | Linear State | |---------------|-------------| | Backlog | Backlog (backlog) | | To Do | Todo (unstarted) | | In Progress | In Progress (started) | | Review | In Review (started) | | Done | Done (completed) | ### Step 3: Export from Source System **Jira Export:** ```typescript // src/migration/jira-exporter.ts interface JiraIssue { key: string; summary: string; description: string; status: string; priority: string; issuetype: string; assignee?: string; labels: string[]; storyPoints?: number; parent?: string; subtasks: string[]; } async function exportJiraProject( baseUrl: string, projectKey: string, authToken: string ): Promise<JiraIssue[]> { const issues: JiraIssue[] = []; let startAt = 0; const maxResults = 100; while (true) { const jql = `project = ${projectKey} ORDER BY created ASC`; const response = await fetch( `${baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=summary,description,status,priority,issuetype,assignee,labels,customfield_10016,parent,subtasks`, { headers: { Authorization: `Basic ${authToken}`, Accept: "application/json" } } ); const data = await response.json(); for (const issue of data.issues) { issues.push({ key: issue.key, summary: issue.fields.summary, description: issue.fields.description?.content ? convertAtlassianDocToMarkdown(issue.fields.description) : issue.fields.description ?? "", status: issue.fields.status.name, priority: issue.fields.priority?.name ?? "Medium", issuetype: issue.fields.issuetype.name, assignee: issue.fields.assignee?.emailAddress, labels: issue.fields.labels ?? [], storyPoints: issue.fields.customfield_10016, parent: issue.fields.parent?.key, subtasks: issue.fields.subtasks?.map((s: any) => s.key) ?? [], }); } startAt += maxResults; if (startAt >= data.total) break; } console.log(`Exported ${issues.length} issues from Jira ${projectKey}`); return issues; } ``` **Jira Markup -> Markdown Converter:** ```typescript function convertJiraToMarkdown(text: string): string { if (!text) return ""; return text .replace(/h([1-6])\.\s/g, (_, level) => "#".repeat(parseInt(level)) + " ") .replace(/\*([^*]+)\*/g, "**$1**") .replace(/_([^_]+)_/g, "*$1*") .replace(/\{code(?::([^}]*))?\}([\s\S]*?)\{code\}/g, "```$1\n$2\n```") .replace(/\{noformat\}([\s\S]*?)\{noformat\}/g, "```\n$1\n```") .replace(/^\*\s/gm, "- ") .replace(/^#\s/gm, "1. ") .replace(/\[([^|]+)\|([^\]]+)\]/g, "[$1]($2)"); } ``` ### Step 4: Transform to Linear Format ```typescript interface LinearImportIssue { title: string; description: string; priority: number; stateId: string; assigneeId?: string; labelIds: string[]; estimate?: number; parentId?: string; sourceId: string; // Original ID for tracking } async function transformJiraIssue( jiraIssue: JiraIssue, stateMap: Map<string, string>, userMap: Map<string, string>, labelMap: Map<string, string> ): Promise<LinearImportIssue> { // Priority mapping const priorityMap: Record<string, number> = { Highest: 1, Blocker: 1, High: 2, Medium: 3, Low: 4, Lowest: 4, }; // Map labels const labelIds: string[] = []; // Issue type becomes a label const typeLabel = labelMap.get(jiraIssue.issuetype); if (typeLabel) labelIds.push(typeLabel); // Original Jira labels for (const label of jiraIssue.labels) { const mapped = labelMap.get(label); if (mapped) labelIds.push(mapped); } return { title: jiraIssue.summary, description: convertJiraToMarkdown(jiraIssue.description), priority: priorityMap[jiraIssue.priority] ?? 3, stateId: stateMap.get(jiraIssue.status) ?? stateMap.get("Todo")!, assigneeId: jiraIssue.assignee ? userMap.get(jiraIssue.assignee) : undefined, labelIds, estimate: jiraIssue.storyPoints ?? undefined, sourceId: jiraIssue.key, }; } ``` ### Step 5: Import to Linear ```typescript import { LinearClient } from "@linear/sdk"; async function importToLinear( client: LinearClient, teamId: string, issues: JiraIssue[], stateMap: Map<string, string>, userMap: Map<string, string>, labelMap: Map<string, string> ): Promise<{ created: number; errors: number; idMap: Map<string, string> }> { const idMap = new Map<string, string>(); // sourceId -> linearId let created = 0; let errors = 0; // Sort: parents first, then children const sorted = [...issues].sort((a, b) => { if (a.subtasks.length > 0 && !a.parent) return -1; // Parents first if (b.subtasks.length > 0 && !b.parent) return 1; return 0; }); for (const jiraIssue of sorted) { try { const transformed = await transformJiraIssue(jiraIssue, stateMap, userMap, labelMap); // Set parent if it was already imported if (jiraIssue.parent && idMap.has(jiraIssue.parent)) { transformed.parentId = idMap.get(jiraIssue.parent); } const result = await client.createIssue({ teamId, title: transformed.title, description: `${transformed.description}\n\n---\n*Migrated from ${jiraIssue.key}*`, priority: transformed.priority, stateId: transformed.stateId, assigneeId: transformed.assigneeId, labelIds: transformed.labelIds, estimate: transformed.estimate, parentId: transformed.parentId, }); if (result.success) { const issue = await result.issue; idMap.set(jiraIssue.key, issue!.id); created++; if (created % 25 === 0) console.log(`Imported ${created}/${sorted.length}`); } // Rate limit: 100ms between requests await new Promise(r => setTimeout(r, 100)); } catch (error: any) { console.error(`Failed to import ${jiraIssue.key}: ${error.message}`); errors++; } } console.log(`Import complete: ${created} created, ${errors} errors`); return { created, errors, idMap }; } ``` ### Step 6: Post-Migration Validation ```typescript async function validateMigration( client: LinearClient, teamId: string, sourceIssues: JiraIssue[], idMap: Map<string, string> ): Promise<{ valid: boolean; issues: string[] }> { const problems: string[] = []; // Check all issues were imported if (idMap.size < sourceIssues.length) { problems.push(`Missing: ${sourceIssues.length - idMap.size} issues not imported`); } // Sample validation: check 50 random issues const sample = sourceIssues.slice(0, 50); for (const source of sample) { const linearId = idMap.get(source.key); if (!linearId) { problems.push(`${source.key}: not imported`); continue; } try { const issue = await client.issue(linearId); if (issue.title !== source.summary) { problems.push(`${source.key}: title mismatch`); } } catch { problems.push(`${source.key}: not found in Linear (${linearId})`); } await new Promise(r => setTimeout(r, 50)); } return { valid: problems.length === 0, issues: problems }; } ``` ## Post-Migration Checklist ``` [ ] All issues imported and validated [ ] Parent/child relationships correct [ ] Labels and priorities mapped correctly [ ] User assignments transferred [ ] Integrations reconfigured (GitHub, Slack) [ ] Team workflows customized in Linear [ ] Team trained on Linear [ ] Source system set to read-only [ ] Parallel run period started (2 weeks recommended) [ ] Archive source system after parallel run ``` ## Error Handling | Issue | Cause | Solution | |-------|-------|----------| | User not found | Unmapped email | Add to userMap | | Rate limited | Too fast import | Increase delay to 200ms | | State not found | Unmapped status | Update stateMap | | Parent not found | Import order wrong | Sort parents before children | | Markup broken | Incomplete conversion | Improve markdown converter | ## Resources - [Linear Import (Built-in)](https://linear.app/docs/import-issues) - [Jira REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/) - [Asana API](https://developers.asana.com/reference) - [Linear GraphQL API](https://linear.app/developers/graphql)
Name Size