linear_api 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +113 -11
- data/app/models/linear_api/synced_issue.rb +38 -4
- data/lib/linear_api/cache_sync.rb +101 -65
- data/lib/linear_api/client.rb +235 -228
- data/lib/linear_api/engine.rb +1 -0
- data/lib/linear_api/issue.rb +219 -11
- data/lib/linear_api/issue_tracker.rb +51 -17
- data/lib/linear_api/label.rb +14 -0
- data/lib/linear_api/project.rb +24 -2
- data/lib/linear_api/result.rb +22 -10
- data/lib/linear_api/team.rb +60 -0
- data/lib/linear_api/version.rb +1 -1
- data/lib/linear_api.rb +42 -5
- data/lib/tasks/linear_api.rake +29 -12
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b6fd5b3c475c49a2fa516dd0c0efeb160021fff64c92baeded00a2915e14cc1
|
|
4
|
+
data.tar.gz: 90feb73a98cd846639b42189bb67bf4ebf65e41976fef99b2405e21ecb4e5762
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5e959366dfb2e2fe68fb9f2cdaf64bb60e855597b5019d6363ba3ee83671fc2e082f2d022e7f295acf269d3beef321395369029b54075940254d5c1f75b8575
|
|
7
|
+
data.tar.gz: d26de938c9b5b89d33bb4361c66a5f2048f08301fc805fe13fe5061c8f451c5d3e98e227c8501cd4551bbb5c0b3b3b5959894654dcec76a41f10ac4c46e03a8c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Connection timeouts**: Faraday now has 5s open timeout and 15s read timeout (prevents hung Puma threads)
|
|
13
|
+
- **Automatic retry**: `faraday-retry` middleware retries on 500/502/503 and network errors (3 retries with exponential backoff)
|
|
14
|
+
- **Rate limit handling**: HTTP 429 responses now raise `RateLimitError` with `retry_after` attribute
|
|
15
|
+
- **Thread-safe client**: `LinearApi.client` and `reset_client!` are now protected by a `Mutex`
|
|
16
|
+
- **Configurable logger**: `LinearApi.logger` with automatic Rails.logger detection; all API calls, errors, and retries are logged
|
|
17
|
+
- **Pagination support**: `list_all_issues` method with cursor-based pagination and optional block yielding
|
|
18
|
+
- **`search_issues` method**: Explicit full-text search (separated from `get_issue` which now uses direct filter)
|
|
19
|
+
- **`batch_get_issues` method**: Fetch multiple issues by ID in a single API call (avoids N+1)
|
|
20
|
+
- **`fetch_all_metadata` method**: Single GraphQL query to fetch labels, projects, states, and users at once
|
|
21
|
+
- **`due_date` and `estimate` fields**: Added to `Issue` model and all queries
|
|
22
|
+
- **`value!` method on `Result`**: Returns data on success, raises `LinearApi::Error` on failure
|
|
23
|
+
- **Health check**: `LinearApi.healthy?` method and `linear:health` rake task
|
|
24
|
+
- **Configurable workspace slug**: `LinearApi.workspace_slug` / `LINEAR_WORKSPACE_SLUG` env var for `SyncedIssue#linear_url`
|
|
25
|
+
- **Sensitive key redaction**: `IssueTracker` now redacts keys matching password/secret/token/api_key patterns from issue descriptions
|
|
26
|
+
- **CacheSync spec**: Full test coverage for `CacheSync` class
|
|
27
|
+
- **Network failure tests**: Tests for HTTP 429, timeouts, malformed JSON responses
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **BREAKING**: `Result` no longer delegates methods via `method_missing`. Use `result.data.method` or `result.value!.method` instead
|
|
32
|
+
- **BREAKING**: `get_issue('TOS-123')` now uses a direct filter query (`issues(filter: { identifier: { eq: ... } })`) instead of full-text search. This prevents false positives (e.g., "TOS-1" matching "TOS-10"). Use `search_issues(term:)` for fuzzy search.
|
|
33
|
+
- `CacheSync.sync_all` now fetches all metadata in a **single batched API call** (was 4 separate calls)
|
|
34
|
+
- `SyncedIssue.refresh_stale_issues!` now uses **batch fetch** (was N+1 individual API calls)
|
|
35
|
+
- `IssueTracker` now memoizes auto-tracked project/label IDs in memory after first resolution
|
|
36
|
+
- `IssueTracker.track_error` and `track_issue` now rescue all errors and return failure Result (never lets tracking failures propagate)
|
|
37
|
+
- All inline GraphQL queries moved from `Client` to their respective model classes as constants
|
|
38
|
+
- Rake task `export_cache` now fetches team key from API instead of hardcoding `'TOS'`
|
|
39
|
+
- Rake task helper methods moved to `self.` class methods to avoid polluting global scope
|
|
40
|
+
- `faraday-retry` added as a runtime dependency
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `SyncedIssue#linear_url` no longer hardcodes `theownerstack` workspace slug
|
|
45
|
+
- `RateLimitError` now includes `retry_after` attribute (was defined but never raised)
|
|
46
|
+
|
|
47
|
+
## [0.4.0] - 2026-02-06
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **BREAKING (dependency)**: Rails moved from runtime to development dependency. The core client (`Client`, `Result`, `Issue`, `Project`, `Label`, `Team`) now only requires `faraday`. Rails Engine, `CacheSync`, and `IssueTracker` are still loaded automatically when Rails is present via `defined?(Rails::Engine)` guard.
|
|
52
|
+
- This allows non-Rails projects (e.g., standalone gems, CLI tools) to use `linear_api` without pulling in all of Rails.
|
|
53
|
+
|
|
8
54
|
## [0.3.1] - 2026-02-06
|
|
9
55
|
|
|
10
56
|
### Added
|
data/README.md
CHANGED
|
@@ -33,6 +33,8 @@ require 'linear_api'
|
|
|
33
33
|
LinearApi.configure do |config|
|
|
34
34
|
config.api_key = ENV['LINEAR_API_KEY']
|
|
35
35
|
config.team_id = 'your-team-id' # e.g., 'cfe09c0a-dcae-4cf9-be66-621d00798fcf'
|
|
36
|
+
config.workspace_slug = 'your-workspace' # Used for building issue URLs (default: 'theownerstack')
|
|
37
|
+
config.logger = Rails.logger # Optional: defaults to Rails.logger or stdout
|
|
36
38
|
end
|
|
37
39
|
```
|
|
38
40
|
|
|
@@ -45,7 +47,9 @@ result = LinearApi.client.create_issue(
|
|
|
45
47
|
title: 'Bug: Login fails with special characters',
|
|
46
48
|
description: '## Problem\n\nUsers cannot login when password contains @',
|
|
47
49
|
priority: 2, # 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
48
|
-
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f']
|
|
50
|
+
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f'],
|
|
51
|
+
due_date: '2026-03-01', # ISO 8601 date
|
|
52
|
+
estimate: 3 # Story points
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
if result.success?
|
|
@@ -54,11 +58,16 @@ if result.success?
|
|
|
54
58
|
else
|
|
55
59
|
puts "Error: #{result.error}"
|
|
56
60
|
end
|
|
61
|
+
|
|
62
|
+
# Or use value! to raise on failure:
|
|
63
|
+
issue = LinearApi.client.create_issue(title: 'Bug').value!
|
|
64
|
+
puts issue.identifier
|
|
57
65
|
```
|
|
58
66
|
|
|
59
67
|
### Get an Issue
|
|
60
68
|
|
|
61
69
|
```ruby
|
|
70
|
+
# Direct lookup by identifier (exact match)
|
|
62
71
|
result = LinearApi.client.get_issue('TOS-123')
|
|
63
72
|
|
|
64
73
|
if result.success?
|
|
@@ -66,6 +75,18 @@ if result.success?
|
|
|
66
75
|
puts "Title: #{issue.title}"
|
|
67
76
|
puts "State: #{issue.state_name}"
|
|
68
77
|
puts "Labels: #{issue.label_names.join(', ')}"
|
|
78
|
+
puts "Due: #{issue.due_date}"
|
|
79
|
+
puts "Estimate: #{issue.estimate}"
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Search Issues (Full-Text)
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
result = LinearApi.client.search_issues(term: 'login bug', limit: 10)
|
|
87
|
+
|
|
88
|
+
result.data.each do |issue|
|
|
89
|
+
puts "#{issue.identifier}: #{issue.title}"
|
|
69
90
|
end
|
|
70
91
|
```
|
|
71
92
|
|
|
@@ -97,11 +118,31 @@ result = LinearApi.client.add_comment(
|
|
|
97
118
|
### List Issues
|
|
98
119
|
|
|
99
120
|
```ruby
|
|
121
|
+
# Simple list (up to 50)
|
|
100
122
|
result = LinearApi.client.list_issues(limit: 50)
|
|
101
123
|
|
|
102
124
|
result.data.each do |issue|
|
|
103
125
|
puts "#{issue.identifier}: #{issue.title} [#{issue.state_name}]"
|
|
104
126
|
end
|
|
127
|
+
|
|
128
|
+
# List ALL issues with automatic pagination
|
|
129
|
+
result = LinearApi.client.list_all_issues(batch_size: 50) do |issue|
|
|
130
|
+
# Optional: process each issue as it's fetched
|
|
131
|
+
puts "Fetched: #{issue.identifier}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
puts "Total: #{result.data.length}"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Batch Fetch Issues
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Fetch multiple issues in a single API call (avoids N+1)
|
|
141
|
+
result = LinearApi.client.batch_get_issues(ids: ['uuid-1', 'uuid-2', 'uuid-3'])
|
|
142
|
+
|
|
143
|
+
result.data.each do |issue|
|
|
144
|
+
puts "#{issue.identifier}: #{issue.state_name}"
|
|
145
|
+
end
|
|
105
146
|
```
|
|
106
147
|
|
|
107
148
|
### List Labels
|
|
@@ -168,6 +209,20 @@ end
|
|
|
168
209
|
LinearApi.client.assign(id: issue.id, assignee_id: 'user-uuid')
|
|
169
210
|
```
|
|
170
211
|
|
|
212
|
+
### Fetch All Metadata (Single API Call)
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
# Fetch labels, projects, states, and users in one batched call
|
|
216
|
+
result = LinearApi.client.fetch_all_metadata
|
|
217
|
+
|
|
218
|
+
if result.success?
|
|
219
|
+
puts "Labels: #{result.data[:labels].length}"
|
|
220
|
+
puts "Projects: #{result.data[:projects].length}"
|
|
221
|
+
puts "States: #{result.data[:states].length}"
|
|
222
|
+
puts "Users: #{result.data[:users].length}"
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
171
226
|
### Archive/Unarchive Issues
|
|
172
227
|
|
|
173
228
|
```ruby
|
|
@@ -230,14 +285,31 @@ puts result.data['viewer']['name']
|
|
|
230
285
|
```ruby
|
|
231
286
|
result = LinearApi.client.create_issue(title: 'Test')
|
|
232
287
|
|
|
233
|
-
|
|
234
|
-
when ->(r) { r.success? }
|
|
235
|
-
# Handle success
|
|
288
|
+
if result.success?
|
|
236
289
|
puts result.data.identifier
|
|
237
|
-
|
|
238
|
-
# Handle error
|
|
290
|
+
else
|
|
239
291
|
puts "Error: #{result.error}"
|
|
240
292
|
end
|
|
293
|
+
|
|
294
|
+
# Or use value! to raise on failure (good for scripts)
|
|
295
|
+
begin
|
|
296
|
+
issue = LinearApi.client.create_issue(title: 'Test').value!
|
|
297
|
+
puts issue.identifier
|
|
298
|
+
rescue LinearApi::Error => e
|
|
299
|
+
puts "Failed: #{e.message}"
|
|
300
|
+
rescue LinearApi::RateLimitError => e
|
|
301
|
+
puts "Rate limited, retry after #{e.retry_after}s"
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Health Check
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
if LinearApi.healthy?
|
|
309
|
+
puts "Linear API is reachable"
|
|
310
|
+
else
|
|
311
|
+
puts "Cannot connect to Linear"
|
|
312
|
+
end
|
|
241
313
|
```
|
|
242
314
|
|
|
243
315
|
## Rails Engine (Auto-Tracking)
|
|
@@ -247,10 +319,13 @@ The gem includes a Rails Engine for automatic error tracking with deduplication.
|
|
|
247
319
|
### Setup
|
|
248
320
|
|
|
249
321
|
```bash
|
|
322
|
+
# Verify Linear API connectivity
|
|
323
|
+
bin/rails linear:health
|
|
324
|
+
|
|
250
325
|
# Create the auto-tracking project and label in Linear
|
|
251
326
|
bin/rails linear:setup
|
|
252
327
|
|
|
253
|
-
# Sync labels, projects, and states from Linear
|
|
328
|
+
# Sync labels, projects, and states from Linear (single batched API call)
|
|
254
329
|
bin/rails linear:sync_cache
|
|
255
330
|
```
|
|
256
331
|
|
|
@@ -262,7 +337,7 @@ class DailySalesReportJob < ApplicationJob
|
|
|
262
337
|
def perform(account)
|
|
263
338
|
# ... generate report ...
|
|
264
339
|
rescue StandardError => e
|
|
265
|
-
# Track the error in Linear
|
|
340
|
+
# Track the error in Linear (never raises, returns Result)
|
|
266
341
|
LinearApi::IssueTracker.track_error(
|
|
267
342
|
exception: e,
|
|
268
343
|
context: { date: Date.current, account_id: account.id },
|
|
@@ -287,6 +362,21 @@ LinearApi::IssueTracker.track_issue(
|
|
|
287
362
|
)
|
|
288
363
|
```
|
|
289
364
|
|
|
365
|
+
### Security: Sensitive Data Redaction
|
|
366
|
+
|
|
367
|
+
`IssueTracker` automatically redacts context values whose keys match sensitive patterns (password, secret, token, api_key, credential, authorization):
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
LinearApi::IssueTracker.track_error(
|
|
371
|
+
exception: e,
|
|
372
|
+
context: {
|
|
373
|
+
account_id: 123, # Included as-is
|
|
374
|
+
api_key: 'sk_live_xxx', # Redacted to '[REDACTED]'
|
|
375
|
+
user_email: 'user@test.com' # Included as-is
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
```
|
|
379
|
+
|
|
290
380
|
### How Deduplication Works
|
|
291
381
|
|
|
292
382
|
1. Each error generates a fingerprint based on exception class, message, and backtrace
|
|
@@ -296,13 +386,16 @@ LinearApi::IssueTracker.track_issue(
|
|
|
296
386
|
### Rake Tasks
|
|
297
387
|
|
|
298
388
|
```bash
|
|
389
|
+
# Verify API connectivity
|
|
390
|
+
bin/rails linear:health
|
|
391
|
+
|
|
299
392
|
# Setup auto-tracking project and label
|
|
300
393
|
bin/rails linear:setup
|
|
301
394
|
|
|
302
|
-
# Sync metadata from Linear API
|
|
395
|
+
# Sync metadata from Linear API (single batched call)
|
|
303
396
|
bin/rails linear:sync_cache
|
|
304
397
|
|
|
305
|
-
# Refresh local issue states (
|
|
398
|
+
# Refresh local issue states (batched, checks if resolved)
|
|
306
399
|
bin/rails linear:refresh_issues
|
|
307
400
|
|
|
308
401
|
# Show cache statistics
|
|
@@ -344,9 +437,18 @@ account_issues = LinearApi::SyncedIssue.where(
|
|
|
344
437
|
# Check if an issue is still open
|
|
345
438
|
issue = LinearApi::SyncedIssue.find_by(identifier: 'TOS-123')
|
|
346
439
|
puts issue.open? # => true/false
|
|
347
|
-
puts issue.linear_url # => "https://linear.app/
|
|
440
|
+
puts issue.linear_url # => "https://linear.app/your-workspace/issue/TOS-123"
|
|
348
441
|
```
|
|
349
442
|
|
|
443
|
+
## Resilience Features
|
|
444
|
+
|
|
445
|
+
- **Connection timeouts**: 5s connect, 15s read (configurable per-client)
|
|
446
|
+
- **Automatic retries**: 3 retries with exponential backoff on 500/502/503 and network errors
|
|
447
|
+
- **Rate limit detection**: HTTP 429 raises `RateLimitError` with `retry_after`
|
|
448
|
+
- **Thread safety**: Module-level client access is protected by `Mutex`
|
|
449
|
+
- **Logging**: All API calls, retries, and errors logged via configurable `LinearApi.logger`
|
|
450
|
+
- **Silent error tracking**: `IssueTracker.track_error` never raises -- returns failure `Result` and logs
|
|
451
|
+
|
|
350
452
|
## Development
|
|
351
453
|
|
|
352
454
|
```bash
|
|
@@ -42,9 +42,10 @@ module LinearApi
|
|
|
42
42
|
true
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# Build Linear URL
|
|
45
|
+
# Build Linear URL using configurable workspace slug
|
|
46
46
|
def linear_url
|
|
47
|
-
|
|
47
|
+
slug = LinearApi.workspace_slug || 'theownerstack'
|
|
48
|
+
"https://linear.app/#{slug}/issue/#{identifier}"
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# Parse metadata JSON
|
|
@@ -67,9 +68,42 @@ module LinearApi
|
|
|
67
68
|
find_open_for_fingerprint(fingerprint).nil?
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
# Refresh all stale issues
|
|
71
|
+
# Refresh all stale issues using batch API call (avoids N+1)
|
|
71
72
|
def refresh_stale_issues!
|
|
72
|
-
stale.
|
|
73
|
+
stale_issues = stale.to_a
|
|
74
|
+
return if stale_issues.empty?
|
|
75
|
+
|
|
76
|
+
LinearApi.logger.info { "LinearApi: refreshing #{stale_issues.size} stale issues" }
|
|
77
|
+
|
|
78
|
+
# Batch fetch from Linear API in groups of 50
|
|
79
|
+
stale_issues.each_slice(50) do |batch|
|
|
80
|
+
ids = batch.map(&:linear_id)
|
|
81
|
+
result = LinearApi.client.batch_get_issues(ids: ids)
|
|
82
|
+
|
|
83
|
+
unless result.success?
|
|
84
|
+
LinearApi.logger.error { "LinearApi: batch refresh failed: #{result.error}" }
|
|
85
|
+
# Fall back to individual refresh for this batch
|
|
86
|
+
batch.each(&:refresh_from_linear!)
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Index fetched issues by ID for O(1) lookup
|
|
91
|
+
fetched = result.data.index_by(&:id)
|
|
92
|
+
|
|
93
|
+
batch.each do |synced|
|
|
94
|
+
linear_issue = fetched[synced.linear_id]
|
|
95
|
+
if linear_issue
|
|
96
|
+
synced.update!(
|
|
97
|
+
state_name: linear_issue.state_name,
|
|
98
|
+
title: linear_issue.title,
|
|
99
|
+
priority: linear_issue.priority,
|
|
100
|
+
synced_at: Time.current
|
|
101
|
+
)
|
|
102
|
+
else
|
|
103
|
+
LinearApi.logger.warn { "LinearApi: issue #{synced.identifier} not found in Linear (may be deleted)" }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
73
107
|
end
|
|
74
108
|
end
|
|
75
109
|
end
|
|
@@ -12,98 +12,57 @@ module LinearApi
|
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Sync all metadata in a single API call (instead of 4 separate calls)
|
|
15
16
|
def sync_all
|
|
17
|
+
LinearApi.logger.info { 'LinearApi: syncing all metadata from Linear API' }
|
|
18
|
+
|
|
19
|
+
result = LinearApi.client.fetch_all_metadata
|
|
20
|
+
unless result.success?
|
|
21
|
+
LinearApi.logger.error { "LinearApi: metadata sync failed: #{result.error}" }
|
|
22
|
+
return {
|
|
23
|
+
labels: { success: false, error: result.error },
|
|
24
|
+
projects: { success: false, error: result.error },
|
|
25
|
+
states: { success: false, error: result.error },
|
|
26
|
+
users: { success: false, error: result.error }
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
metadata = result.data
|
|
16
31
|
{
|
|
17
|
-
labels:
|
|
18
|
-
projects:
|
|
19
|
-
states:
|
|
20
|
-
users:
|
|
32
|
+
labels: sync_labels_from_data(metadata[:labels]),
|
|
33
|
+
projects: sync_projects_from_data(metadata[:projects]),
|
|
34
|
+
states: sync_states_from_data(metadata[:states]),
|
|
35
|
+
users: sync_users_from_data(metadata[:users])
|
|
21
36
|
}
|
|
22
37
|
end
|
|
23
38
|
|
|
39
|
+
# Individual sync methods (for targeted refresh)
|
|
24
40
|
def sync_labels
|
|
25
41
|
result = LinearApi.client.list_labels
|
|
26
42
|
return { success: false, error: result.error } unless result.success?
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
result.data.each do |label|
|
|
30
|
-
category = extract_category(label.name)
|
|
31
|
-
CachedMetadata.upsert_from_api(
|
|
32
|
-
type: 'label',
|
|
33
|
-
linear_id: label.id,
|
|
34
|
-
name: label.name,
|
|
35
|
-
key: label.name,
|
|
36
|
-
color: label.color,
|
|
37
|
-
category: category,
|
|
38
|
-
raw_data: label.to_h
|
|
39
|
-
)
|
|
40
|
-
count += 1
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{ success: true, count: count }
|
|
44
|
+
sync_labels_from_data(result.data)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def sync_projects
|
|
47
48
|
result = LinearApi.client.list_projects
|
|
48
49
|
return { success: false, error: result.error } unless result.success?
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
result.data.each do |project|
|
|
52
|
-
key = project.name.parameterize.underscore
|
|
53
|
-
CachedMetadata.upsert_from_api(
|
|
54
|
-
type: 'project',
|
|
55
|
-
linear_id: project.id,
|
|
56
|
-
name: project.name,
|
|
57
|
-
key: key,
|
|
58
|
-
state_type: project.state,
|
|
59
|
-
raw_data: project.to_h
|
|
60
|
-
)
|
|
61
|
-
count += 1
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
{ success: true, count: count }
|
|
51
|
+
sync_projects_from_data(result.data)
|
|
65
52
|
end
|
|
66
53
|
|
|
67
54
|
def sync_states
|
|
68
55
|
result = LinearApi.client.list_states
|
|
69
56
|
return { success: false, error: result.error } unless result.success?
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
result.data.each do |state|
|
|
73
|
-
key = state['name'].parameterize.underscore
|
|
74
|
-
CachedMetadata.upsert_from_api(
|
|
75
|
-
type: 'state',
|
|
76
|
-
linear_id: state['id'],
|
|
77
|
-
name: state['name'],
|
|
78
|
-
key: key,
|
|
79
|
-
color: state['color'],
|
|
80
|
-
state_type: state['type'],
|
|
81
|
-
raw_data: state
|
|
82
|
-
)
|
|
83
|
-
count += 1
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
{ success: true, count: count }
|
|
58
|
+
sync_states_from_data(result.data)
|
|
87
59
|
end
|
|
88
60
|
|
|
89
61
|
def sync_users
|
|
90
62
|
result = LinearApi.client.list_users
|
|
91
63
|
return { success: false, error: result.error } unless result.success?
|
|
92
64
|
|
|
93
|
-
|
|
94
|
-
result.data.each do |user|
|
|
95
|
-
key = user['email']&.split('@')&.first || user['name'].parameterize.underscore
|
|
96
|
-
CachedMetadata.upsert_from_api(
|
|
97
|
-
type: 'user',
|
|
98
|
-
linear_id: user['id'],
|
|
99
|
-
name: user['name'],
|
|
100
|
-
key: key,
|
|
101
|
-
raw_data: user
|
|
102
|
-
)
|
|
103
|
-
count += 1
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
{ success: true, count: count }
|
|
65
|
+
sync_users_from_data(result.data)
|
|
107
66
|
end
|
|
108
67
|
|
|
109
68
|
# Import from linear.json file
|
|
@@ -157,6 +116,83 @@ module LinearApi
|
|
|
157
116
|
|
|
158
117
|
private
|
|
159
118
|
|
|
119
|
+
def sync_labels_from_data(labels)
|
|
120
|
+
count = 0
|
|
121
|
+
labels.each do |label|
|
|
122
|
+
category = extract_category(label.is_a?(Label) ? label.name : label['name'])
|
|
123
|
+
CachedMetadata.upsert_from_api(
|
|
124
|
+
type: 'label',
|
|
125
|
+
linear_id: label.is_a?(Label) ? label.id : label['id'],
|
|
126
|
+
name: label.is_a?(Label) ? label.name : label['name'],
|
|
127
|
+
key: label.is_a?(Label) ? label.name : label['name'],
|
|
128
|
+
color: label.is_a?(Label) ? label.color : label['color'],
|
|
129
|
+
category: category,
|
|
130
|
+
raw_data: label.is_a?(Label) ? label.to_h : label
|
|
131
|
+
)
|
|
132
|
+
count += 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
{ success: true, count: count }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def sync_projects_from_data(projects)
|
|
139
|
+
count = 0
|
|
140
|
+
projects.each do |project|
|
|
141
|
+
name = project.is_a?(Project) ? project.name : project['name']
|
|
142
|
+
key = name.parameterize.underscore
|
|
143
|
+
CachedMetadata.upsert_from_api(
|
|
144
|
+
type: 'project',
|
|
145
|
+
linear_id: project.is_a?(Project) ? project.id : project['id'],
|
|
146
|
+
name: name,
|
|
147
|
+
key: key,
|
|
148
|
+
state_type: project.is_a?(Project) ? project.state : project['state'],
|
|
149
|
+
raw_data: project.is_a?(Project) ? project.to_h : project
|
|
150
|
+
)
|
|
151
|
+
count += 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
{ success: true, count: count }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def sync_states_from_data(states)
|
|
158
|
+
count = 0
|
|
159
|
+
states.each do |state|
|
|
160
|
+
name = state.is_a?(Hash) ? state['name'] : state.name
|
|
161
|
+
key = name.parameterize.underscore
|
|
162
|
+
CachedMetadata.upsert_from_api(
|
|
163
|
+
type: 'state',
|
|
164
|
+
linear_id: state.is_a?(Hash) ? state['id'] : state.id,
|
|
165
|
+
name: name,
|
|
166
|
+
key: key,
|
|
167
|
+
color: state.is_a?(Hash) ? state['color'] : state.color,
|
|
168
|
+
state_type: state.is_a?(Hash) ? state['type'] : state.type,
|
|
169
|
+
raw_data: state.is_a?(Hash) ? state : state.to_h
|
|
170
|
+
)
|
|
171
|
+
count += 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
{ success: true, count: count }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def sync_users_from_data(users)
|
|
178
|
+
count = 0
|
|
179
|
+
users.each do |user|
|
|
180
|
+
name = user.is_a?(Hash) ? user['name'] : user.name
|
|
181
|
+
email = user.is_a?(Hash) ? user['email'] : user.email
|
|
182
|
+
key = email&.split('@')&.first || name.parameterize.underscore
|
|
183
|
+
CachedMetadata.upsert_from_api(
|
|
184
|
+
type: 'user',
|
|
185
|
+
linear_id: user.is_a?(Hash) ? user['id'] : user.id,
|
|
186
|
+
name: name,
|
|
187
|
+
key: key,
|
|
188
|
+
raw_data: user.is_a?(Hash) ? user : user.to_h
|
|
189
|
+
)
|
|
190
|
+
count += 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
{ success: true, count: count }
|
|
194
|
+
end
|
|
195
|
+
|
|
160
196
|
def extract_category(label_name)
|
|
161
197
|
return nil unless label_name.include?(':')
|
|
162
198
|
|