Skip to content
Skillv1.0.0

linear-migration-deep-dive

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 linea

by jeremylongshore(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jeremylongshore/tons-of-skills-marketplace (plugins/saas-packs/linear-pack/skills/linear-migration-deep-dive/SKILL.md). Install upstream with npx skills add jeremylongshore/tons-of-skills-marketplace --skill linear-migration-deep-dive. Copyright stays with the author (MIT).

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:

// 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:

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");
}

Step 4: Transform to Linear Format

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

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

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

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jeremylongshore-tons-of-skills-marketplace-linear-migrat-e30c05/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jeremylongshore-tons-of-skills-marketplace-linear-migrat-e30c05.ocm.jsonjson
{
  "ocm": "1",
  "id": "jeremylongshore-tons-of-skills-marketplace-linear-migrat-e30c05",
  "kind": "skill",
  "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\".",
  "publisher": "jeremylongshore",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "saas",
      "linear",
      "migration",
      "workflow",
      "skills-sh"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "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\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "skills.sh",
      "repository": "https://github.com/jeremylongshore/tons-of-skills-marketplace",
      "path": "plugins/saas-packs/linear-pack/skills/linear-migration-deep-dive/SKILL.md",
      "ref": "HEAD",
      "url": "https://github.com/jeremylongshore/tons-of-skills-marketplace/blob/HEAD/plugins/saas-packs/linear-pack/skills/linear-migration-deep-dive/SKILL.md",
      "key": "jeremylongshore/tons-of-skills-marketplace/plugins/saas-packs/linear-pack/skills/linear-migration-deep-dive/SKILL.md"
    },
    "compatibility": "Designed for Claude Code",
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit,",
      "Bash(node:*),",
      "Bash(npx:*),",
      "Grep"
    ],
    "license": "MIT"
  },
  "instructions": "# Linear Migration Deep Dive\n\n## Overview\n\nComprehensive 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.\n\n## Prerequisites\n\n- Admin access to source system (Jira/Asana/GitHub)\n- Linear workspace with admin access\n- API keys for both source and Linear\n- Migration timeline and rollback plan\n\n## Instructions\n\n### Step 1: Migration Assessment Checklist\n\n```\nDa",
  "cost": {
    "context_tokens": 2515
  }
}

Fetch it by URL: GET /api/v1/registry/jeremylongshore-tons-of-skills-marketplace-linear-migrat-e30c05/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.