linear-migration-deep-dive

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". allowed-tools: Read, Write, Edit, Bash(node:*), Bash(npx:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>

Allowed Tools

No tools specified

Provided by Plugin

linear-pack

Claude Code skill pack for Linear (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the linear-pack plugin:

/plugin install linear-pack@claude-code-plugins-plus

Click to copy

Instructions

# Linear Migration Deep Dive ## Overview Comprehensive guide for migrating from other issue trackers to Linear. ## Prerequisites - Admin access to source system - Linear workspace with admin access - API access to both systems - Migration timeline and rollback plan ## Migration Planning ### Phase 1: Assessment ```markdown ## Migration Assessment Checklist ### Data Volume - [ ] Count total issues: ____ - [ ] Count total projects: ____ - [ ] Count total users: ____ - [ ] Attachments size: ____ GB - [ ] Custom fields count: ____ ### Workflow Analysis - [ ] Document current statuses/states - [ ] Map status transitions - [ ] Identify automation rules - [ ] List integrations in use ### User Mapping - [ ] Export user list from source - [ ] Map to Linear users - [ ] Plan for unmapped users ### Timeline - [ ] Migration window: ____ - [ ] Parallel run period: ____ - [ ] Cutover date: ____ - [ ] Rollback deadline: ____ ``` ### Phase 2: Workflow Mapping ```typescript // migration/workflow-mapping.ts // Jira to Linear status mapping const JIRA_STATUS_MAP: Record = { "To Do": "Todo", "In Progress": "In Progress", "In Review": "In Review", "Done": "Done", "Closed": "Done", "Backlog": "Backlog", "Blocked": "In Progress", // Linear uses labels for blocked }; // Jira to Linear priority mapping const JIRA_PRIORITY_MAP: Record = { "Highest": 1, // Urgent "High": 2, "Medium": 3, "Low": 4, "Lowest": 4, }; // Jira to Linear issue type mapping const JIRA_TYPE_MAP: Record = { "Bug": { labelName: "Bug" }, "Story": { labelName: "Feature" }, "Task": { labelName: "Task" }, "Epic": { labelName: "Epic" }, "Subtask": { labelName: "Subtask" }, }; // Asana to Linear mapping const ASANA_SECTION_MAP: Record = { "To Do": "Todo", "Doing": "In Progress", "Review": "In Review", "Complete": "Done", }; ``` ## Instructions ### Step 1: Export from Source System **Jira Export:** ```typescript // migration/jira-export.ts import JiraClient from "jira-client"; const jira = new JiraClient({ host: process.env.JIRA_HOST, basic_auth: { email: process.env.JIRA_EMAIL, api_token: process.env.JIRA_API_TOKEN, }, }); interface JiraIssue { key: string; fields: { summary: string; description: string; status: { name: string }; priority: { name: string }; issuetype: { name: string }; assignee: { emailAddress: string } | null; reporter: { emailAddress: string }; created: string; updated: string; parent?: { key: string }; subtasks: { key: string }[]; labels: string[]; customfield_10001?: number; // Story points }; } export async function exportJiraProject(projectKey: string): Promise { const issues: JiraIssue[] = []; let startAt = 0; const maxResults = 100; while (true) { const result = await jira.searchJira( `project = ${projectKey} ORDER BY created ASC`, { startAt, maxResults, fields: [ "summary", "description", "status", "priority", "issuetype", "assignee", "reporter", "created", "updated", "parent", "subtasks", "labels", "customfield_10001", // Story points ], } ); issues.push(...result.issues); if (issues.length >= result.total) break; startAt += maxResults; console.log(`Exported ${issues.length}/${result.total} issues...`); } // Save to file for backup await fs.writeFile( `jira-export-${projectKey}-${Date.now()}.json`, JSON.stringify(issues, null, 2) ); return issues; } ``` **Asana Export:** ```typescript // migration/asana-export.ts import Asana from "asana"; const asana = Asana.Client.create().useAccessToken(process.env.ASANA_TOKEN); export async function exportAsanaProject(projectGid: string) { const tasks = []; const result = await asana.tasks.getTasks({ project: projectGid, opt_fields: [ "name", "notes", "assignee", "due_on", "completed", "memberships.section.name", "tags.name", "parent.gid", "subtasks.gid", "created_at", "modified_at", ], }); for await (const task of result) { tasks.push(task); } return tasks; } ``` ### Step 2: Transform Data ```typescript // migration/transform.ts import { LinearClient } from "@linear/sdk"; interface LinearIssueInput { teamId: string; title: string; description?: string; priority?: number; stateId?: string; assigneeId?: string; labelIds?: string[]; estimate?: number; parentId?: string; } interface TransformContext { linearClient: LinearClient; teamId: string; stateMap: Map; userMap: Map; labelMap: Map; issueIdMap: Map; // sourceId -> linearId } export async function transformJiraIssue( jiraIssue: JiraIssue, context: TransformContext ): Promise { // Map status to Linear state const linearStatus = JIRA_STATUS_MAP[jiraIssue.fields.status.name] || "Todo"; const stateId = context.stateMap.get(linearStatus); // Map priority const priority = JIRA_PRIORITY_MAP[jiraIssue.fields.priority?.name] || 0; // Map assignee const assigneeEmail = jiraIssue.fields.assignee?.emailAddress; const assigneeId = assigneeEmail ? context.userMap.get(assigneeEmail) : undefined; // Map labels const labelIds: string[] = []; // Add issue type as label const typeLabel = JIRA_TYPE_MAP[jiraIssue.fields.issuetype.name]; if (typeLabel && context.labelMap.has(typeLabel.labelName)) { labelIds.push(context.labelMap.get(typeLabel.labelName)!); } // Add Jira labels for (const label of jiraIssue.fields.labels) { const linearLabelId = context.labelMap.get(label); if (linearLabelId) { labelIds.push(linearLabelId); } } // Convert description const description = convertJiraToMarkdown(jiraIssue.fields.description); return { teamId: context.teamId, title: `[${jiraIssue.key}] ${jiraIssue.fields.summary}`, description, priority, stateId, assigneeId, labelIds, estimate: jiraIssue.fields.customfield_10001, // Story points }; } function convertJiraToMarkdown(jiraMarkup: string | null): string { if (!jiraMarkup) return ""; let md = jiraMarkup; // Headers md = md.replace(/h1\. /g, "# "); md = md.replace(/h2\. /g, "## "); md = md.replace(/h3\. /g, "### "); // Bold and italic md = md.replace(/\*([^*]+)\*/g, "**$1**"); md = md.replace(/_([^_]+)_/g, "*$1*"); // Code blocks md = md.replace(/\{code(:([^}]+))?\}([\s\S]*?)\{code\}/g, "```$2\n$3\n```"); md = md.replace(/\{noformat\}([\s\S]*?)\{noformat\}/g, "```\n$1\n```"); // Lists md = md.replace(/^# /gm, "1. "); md = md.replace(/^\* /gm, "- "); // Links md = md.replace(/\[([^\]|]+)\|([^\]]+)\]/g, "[$1]($2)"); md = md.replace(/\[([^\]]+)\]/g, "[$1]($1)"); return md; } ``` ### Step 3: Import to Linear ```typescript // migration/import.ts import { LinearClient } from "@linear/sdk"; interface ImportStats { total: number; created: number; skipped: number; errors: { sourceId: string; error: string }[]; } export async function importToLinear( issues: JiraIssue[], context: TransformContext ): Promise { const stats: ImportStats = { total: issues.length, created: 0, skipped: 0, errors: [], }; // Sort issues: parents first, then children const sorted = sortByHierarchy(issues); for (const jiraIssue of sorted) { try { // Check if already imported if (context.issueIdMap.has(jiraIssue.key)) { stats.skipped++; continue; } const input = await transformJiraIssue(jiraIssue, context); // Set parent if exists if (jiraIssue.fields.parent) { input.parentId = context.issueIdMap.get(jiraIssue.fields.parent.key); } // Create in Linear const result = await context.linearClient.createIssue(input); if (result.success) { const issue = await result.issue; context.issueIdMap.set(jiraIssue.key, issue!.id); stats.created++; // Rate limit await sleep(100); } else { throw new Error("Create failed"); } console.log(`Imported ${stats.created}/${stats.total}: ${jiraIssue.key}`); } catch (error) { stats.errors.push({ sourceId: jiraIssue.key, error: error instanceof Error ? error.message : "Unknown error", }); console.error(`Failed to import ${jiraIssue.key}:`, error); } } return stats; } function sortByHierarchy(issues: JiraIssue[]): JiraIssue[] { const byKey = new Map(issues.map(i => [i.key, i])); const sorted: JiraIssue[] = []; const processed = new Set(); function addWithDependencies(issue: JiraIssue): void { if (processed.has(issue.key)) return; // Add parent first if (issue.fields.parent) { const parent = byKey.get(issue.fields.parent.key); if (parent) addWithDependencies(parent); } sorted.push(issue); processed.add(issue.key); } for (const issue of issues) { addWithDependencies(issue); } return sorted; } ``` ### Step 4: Validation & Verification ```typescript // migration/validate.ts export async function validateMigration( sourceIssues: JiraIssue[], context: TransformContext ): Promise<{ valid: boolean; issues: string[] }> { const issues: string[] = []; // Check all issues were migrated for (const source of sourceIssues) { if (!context.issueIdMap.has(source.key)) { issues.push(`Missing: ${source.key}`); } } // Verify sample of migrated issues const sampleSize = Math.min(50, sourceIssues.length); const sample = sourceIssues.slice(0, sampleSize); for (const source of sample) { const linearId = context.issueIdMap.get(source.key); if (!linearId) continue; try { const linearIssue = await context.linearClient.issue(linearId); // Check title contains original key if (!linearIssue.title.includes(source.key)) { issues.push(`Title mismatch: ${source.key}`); } // Check priority mapping const expectedPriority = JIRA_PRIORITY_MAP[source.fields.priority?.name] || 0; if (linearIssue.priority !== expectedPriority) { issues.push(`Priority mismatch: ${source.key} (${linearIssue.priority} != ${expectedPriority})`); } } catch (error) { issues.push(`Verify failed: ${source.key} - ${error}`); } } return { valid: issues.length === 0, issues, }; } ``` ### Step 5: Post-Migration ```typescript // migration/post-migration.ts export async function createMigrationReport( stats: ImportStats, context: TransformContext ): Promise { const report = ` # Migration Report **Date:** ${new Date().toISOString()} **Source:** Jira **Target:** Linear ## Statistics - Total issues: ${stats.total} - Successfully imported: ${stats.created} - Skipped (duplicates): ${stats.skipped} - Errors: ${stats.errors.length} ## ID Mapping ${Array.from(context.issueIdMap.entries()) .map(([source, linear]) => `- ${source} -> ${linear}`) .join("\n")} ## Errors ${stats.errors.map(e => `- ${e.sourceId}: ${e.error}`).join("\n") || "None"} ## Next Steps 1. Verify critical issues manually 2. Update integrations to use Linear 3. Archive source project after parallel run 4. Train team on Linear workflows `; await fs.writeFile("migration-report.md", report); return report; } ``` ## Migration Checklist ``` ## Pre-Migration [ ] Backup source system data [ ] Create Linear workspace and teams [ ] Set up workflow states and labels [ ] Map users between systems [ ] Create API credentials ## Migration [ ] Export data from source [ ] Transform to Linear format [ ] Import in batches [ ] Validate sample issues [ ] Import attachments (if needed) ## Post-Migration [ ] Run full validation [ ] Set up redirects (if applicable) [ ] Update integrations [ ] Train team [ ] Run parallel for 1-2 weeks [ ] Archive source after cutover ``` ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | `User not found` | Unmapped user | Add to user mapping | | `Rate limited` | Too fast import | Add delays between requests | | `State not found` | Unmapped status | Update state mapping | | `Parent not found` | Import order wrong | Sort by hierarchy | ## Resources - [Linear Import Documentation](https://linear.app/docs/import-issues) - [Jira API Reference](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/) - [Asana API Reference](https://developers.asana.com/reference) ## Conclusion You have completed the Linear Flagship Skill Pack. You now have comprehensive knowledge of Linear integrations from basic setup through enterprise deployment.

Skill file: plugins/saas-packs/linear-pack/skills/linear-migration-deep-dive/SKILL.md