Class: RailsErrorDashboard::Services::LinearIssueClient

Inherits:
IssueTrackerClient show all
Defined in:
lib/rails_error_dashboard/services/linear_issue_client.rb

Overview

Linear GraphQL API client for issue management.

API Docs: developers.linear.app/docs/graphql/working-with-the-graphql-api Auth: Personal API key (Settings > Security & access > Personal API keys) Rate limit: 1,500 requests/hour per API key

Unlike the git forges, Linear has no “owner/repo” — issues belong to a team. The ‘repo` argument holds the team key (e.g. “ENG”), and issues are addressed by their human identifier (“ENG-123”), reconstructed from the team key plus the team-scoped issue number we store.

Linear also has no open/closed binary — issues move between typed workflow states. Closing maps to the team’s first ‘completed`-type state, reopening to the first `unstarted` (or `backlog`) state.

Constant Summary collapse

REOPEN_STATE_TYPES =
[ "unstarted", "backlog", "triage" ].freeze

Constants inherited from IssueTrackerClient

IssueTrackerClient::MAX_BODY_LENGTH, IssueTrackerClient::REQUEST_TIMEOUT

Instance Attribute Summary

Attributes inherited from IssueTrackerClient

#api_url, #repo, #token

Instance Method Summary collapse

Methods inherited from IssueTrackerClient

for, from_config

Constructor Details

#initialize(token:, repo:, api_url: nil) ⇒ LinearIssueClient

Returns a new instance of LinearIssueClient.



22
23
24
25
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 22

def initialize(token:, repo:, api_url: nil)
  super
  @api_url = api_url || "https://api.linear.app/graphql"
end

Instance Method Details

#add_comment(number:, body:) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 56

def add_comment(number:, body:)
  issue_id = find_issue_id(number)
  return error_response(@last_error || "Linear issue #{identifier_for(number)} not found") unless issue_id

  data = graphql(<<~GRAPHQL, input: { issueId: issue_id, body: truncate_body(body) })
    mutation($input: CommentCreateInput!) {
      commentCreate(input: $input) { success comment { url } }
    }
  GRAPHQL

  comment = data&.dig("commentCreate", "comment")
  comment ? success_response(url: comment["url"]) : error_response(@last_error || "Linear API error: comment failed")
end

#close_issue(number:) ⇒ Object



48
49
50
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 48

def close_issue(number:)
  update_issue_state(number, "completed")
end

#create_issue(title:, body:, labels: []) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 27

def create_issue(title:, body:, labels: [])
  return error_response(@last_error || "Linear team '#{@repo}' not found") unless team_id

  input = { teamId: team_id, title: title, description: truncate_body(body) }
  label_ids = resolve_label_ids(labels)
  input[:labelIds] = label_ids if label_ids.any?

  data = graphql(<<~GRAPHQL, input: input)
    mutation($input: IssueCreateInput!) {
      issueCreate(input: $input) { success issue { identifier number url } }
    }
  GRAPHQL

  issue = data&.dig("issueCreate", "issue")
  if issue
    success_response(url: issue["url"], number: issue["number"])
  else
    error_response(@last_error || "Linear API error: issue creation failed")
  end
end

#fetch_comments(number:, per_page: 10) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 70

def fetch_comments(number:, per_page: 10)
  data = graphql(<<~GRAPHQL, id: identifier_for(number), first: per_page)
    query($id: String!, $first: Int!) {
      issue(id: $id) {
        comments(first: $first) {
          nodes { body createdAt url user { name avatarUrl } }
        }
      }
    }
  GRAPHQL

  nodes = data&.dig("issue", "comments", "nodes")
  return error_response(@last_error || "Linear API error: could not fetch comments") unless nodes

  comments = nodes.map { |c|
    {
      author: c.dig("user", "name"),
      avatar_url: c.dig("user", "avatarUrl"),
      body: c["body"],
      created_at: c["createdAt"],
      url: c["url"]
    }
  }
  success_response(comments: comments)
end

#fetch_issue(number:) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 96

def fetch_issue(number:)
  data = graphql(<<~GRAPHQL, id: identifier_for(number))
    query($id: String!) {
      issue(id: $id) {
        title
        state { name type }
        assignee { name avatarUrl }
        labels { nodes { name color } }
      }
    }
  GRAPHQL

  issue = data&.dig("issue")
  return error_response(@last_error || "Linear API error: could not fetch issue") unless issue

  assignee = issue["assignee"]
  success_response(
    state: closed_state_type?(issue.dig("state", "type")) ? "closed" : "open",
    title: issue["title"],
    assignees: assignee ? [ { login: assignee["name"], avatar_url: assignee["avatarUrl"] } ] : [],
    labels: (issue.dig("labels", "nodes") || []).map { |l|
      { name: l["name"], color: l["color"]&.delete("#") }
    }
  )
end

#reopen_issue(number:) ⇒ Object



52
53
54
# File 'lib/rails_error_dashboard/services/linear_issue_client.rb', line 52

def reopen_issue(number:)
  update_issue_state(number, REOPEN_STATE_TYPES)
end