From 2715883b6c272aa9ae925deb493e72e2f88993c6 Mon Sep 17 00:00:00 2001 From: David Thiel Date: Wed, 8 Apr 2026 12:48:25 +0100 Subject: [PATCH 1/3] linear: map issue priority onto taskwarrior priority Linear's GraphQL exposes a per-issue priority integer (0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low) that wasn't being queried, so every imported task got the service-wide default_priority. Add the field to the issues query and translate it onto taskwarrior's H/M/L buckets, falling back to default_priority for "No priority". --- bugwarrior/services/linear.py | 17 +++++++++++++++- tests/test_linear.py | 37 ++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/bugwarrior/services/linear.py b/bugwarrior/services/linear.py index 65590c97..598c4ea9 100644 --- a/bugwarrior/services/linear.py +++ b/bugwarrior/services/linear.py @@ -64,6 +64,20 @@ class LinearIssue(Issue): UNIQUE_KEY = (URL,) + # Linear exposes issue priority as an integer: + # 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low. + # Map onto taskwarrior's three priority buckets. ``None`` (used for "No + # priority") tells ``get_priority`` to fall back to ``default_priority``. + PRIORITY_MAP = {0: None, 1: "H", 2: "H", 3: "M", 4: "L"} + + def get_priority(self): + priority = self.record.get("priority") + if priority is not None: + mapped = self.PRIORITY_MAP.get(priority) + if mapped is not None: + return mapped + return self.config.default_priority + def to_taskwarrior(self): description = self.record.get("description") created = self.parse_date(self.record.get("createdAt")) @@ -89,7 +103,7 @@ def get(v, k, default=None): ).lower() or None ), - "priority": self.config.default_priority, + "priority": self.get_priority(), "entry": created, "annotations": get(self.extra, "annotations", []), "tags": self.get_tags(), @@ -178,6 +192,7 @@ def __init__(self, *args, **kwargs): name } identifier + priority team { name } diff --git a/tests/test_linear.py b/tests/test_linear.py index 0224db8e..0d7f02b2 100644 --- a/tests/test_linear.py +++ b/tests/test_linear.py @@ -35,6 +35,7 @@ "name": "Done" }, "identifier": "DUS-5", + "priority": 4, "team": { "name": "Dustin's Doings" } @@ -65,6 +66,7 @@ "name": "Todo" }, "identifier": "DUS-1", + "priority": 1, "team": { "name": "Dustin's Doings" } @@ -143,7 +145,7 @@ def test_to_taskwarrior(self): closed_timestamp = datetime(2025, 7, 26, 17, 3, 4, 0, tzinfo=timezone.utc) expected_output = { "project": "prj", - "priority": "M", + "priority": "L", "entry": created_timestamp, "annotations": [], "tags": [], @@ -170,7 +172,7 @@ def test_to_taskwarrior(self): updated_timestamp = datetime(2025, 7, 24, 17, 8, 33, 0, tzinfo=timezone.utc) expected_output = { "project": None, - "priority": "M", + "priority": "H", "entry": created_timestamp, "annotations": [], "tags": ["Improvement", "Feature"], @@ -212,12 +214,41 @@ def test_issues(self): "linearteam": "Dustin's Doings", "linearupdated": updated_timestamp, "linearurl": "https://linear.app/dustins-doings/issue/DUS-5/do-stuff", - "priority": "M", + "priority": "L", "project": 'prj', "tags": [], } self.assertEqual(TaskConstructor(issue).get_taskwarrior_record(), expected) + def test_priority_mapping(self): + # Linear priority integers must map onto taskwarrior's H/M/L buckets, + # with "No priority" (0) and a missing field both falling back to the + # service-wide default. + cases = [ + (0, "M"), # No priority -> default_priority (M) + (1, "H"), # Urgent + (2, "H"), # High + (3, "M"), # Medium + (4, "L"), # Low + ] + for linear_priority, expected in cases: + with self.subTest(linear_priority=linear_priority): + record = { + **RESPONSE["data"]["issues"]["nodes"][0], + "priority": linear_priority, + } + issue = self.service.get_issue_for_record(record, {}) + self.assertEqual(issue.to_taskwarrior()["priority"], expected) + + # A record without a priority key at all should also fall back. + record = { + k: v + for k, v in RESPONSE["data"]["issues"]["nodes"][0].items() + if k != "priority" + } + issue = self.service.get_issue_for_record(record, {}) + self.assertEqual(issue.to_taskwarrior()["priority"], "M") + @responses.activate def test_issues_paginates(self): """Drains every page when Linear signals ``hasNextPage``.""" From b40c85d1a86dd18258f6219b1aaf84400b1386ab Mon Sep 17 00:00:00 2001 From: David Thiel Date: Thu, 9 Apr 2026 02:22:01 -0700 Subject: [PATCH 2/3] Update bugwarrior/services/linear.py Co-authored-by: ryneeverett --- bugwarrior/services/linear.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/bugwarrior/services/linear.py b/bugwarrior/services/linear.py index 598c4ea9..e8ea6fd9 100644 --- a/bugwarrior/services/linear.py +++ b/bugwarrior/services/linear.py @@ -66,18 +66,7 @@ class LinearIssue(Issue): # Linear exposes issue priority as an integer: # 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low. - # Map onto taskwarrior's three priority buckets. ``None`` (used for "No - # priority") tells ``get_priority`` to fall back to ``default_priority``. - PRIORITY_MAP = {0: None, 1: "H", 2: "H", 3: "M", 4: "L"} - - def get_priority(self): - priority = self.record.get("priority") - if priority is not None: - mapped = self.PRIORITY_MAP.get(priority) - if mapped is not None: - return mapped - return self.config.default_priority - + PRIORITY_MAP = {1: "H", 2: "H", 3: "M", 4: "L"} def to_taskwarrior(self): description = self.record.get("description") created = self.parse_date(self.record.get("createdAt")) From ee9e55df9080f1cdd9243f99bf29deb9fbde424e Mon Sep 17 00:00:00 2001 From: David Thiel Date: Thu, 9 Apr 2026 10:28:28 +0100 Subject: [PATCH 3/3] Format --- bugwarrior/services/linear.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bugwarrior/services/linear.py b/bugwarrior/services/linear.py index e8ea6fd9..e62af7e1 100644 --- a/bugwarrior/services/linear.py +++ b/bugwarrior/services/linear.py @@ -67,6 +67,7 @@ class LinearIssue(Issue): # Linear exposes issue priority as an integer: # 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low. PRIORITY_MAP = {1: "H", 2: "H", 3: "M", 4: "L"} + def to_taskwarrior(self): description = self.record.get("description") created = self.parse_date(self.record.get("createdAt"))