linear_api 0.3.2 → 0.5.1
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 +120 -14
- 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 +261 -229
- 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: bfd85fc80770e0982a5791d46f49fb4406b356b9e9fa43be79a9af61d21e791b
|
|
4
|
+
data.tar.gz: 30b5161cf27bdb0a43d8f15a22f32693db5b75c018a07aa5860329b2898a01b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6cd9e24591ae9ce4a807f7fb5758d8b759aa9a8f0b3e0c47fcdc600c55aba984781119c6319c58573370a69837761fe1bac307f052e019147007968b90080a8
|
|
7
|
+
data.tar.gz: be806327bb48559330ace30e6981146cbb7fcc27ad060027644951da7caa846dd9ee284c413625e684c74522a0bbaed6c3bba60594094f1144230903bcf0ae27
|
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
|
@@ -7,9 +7,7 @@ A Ruby gem for interacting with the [Linear](https://linear.app) GraphQL API. Cr
|
|
|
7
7
|
Add this line to your application's Gemfile:
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
|
-
gem 'linear_api'
|
|
11
|
-
# Or from GitHub:
|
|
12
|
-
gem 'linear_api', git: 'https://github.com/dan1d/linear_api.git'
|
|
10
|
+
gem 'linear_api'
|
|
13
11
|
```
|
|
14
12
|
|
|
15
13
|
Then run:
|
|
@@ -18,6 +16,12 @@ Then run:
|
|
|
18
16
|
bundle install
|
|
19
17
|
```
|
|
20
18
|
|
|
19
|
+
Or install it directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install linear_api
|
|
23
|
+
```
|
|
24
|
+
|
|
21
25
|
For Rails applications, run the migrations:
|
|
22
26
|
|
|
23
27
|
```bash
|
|
@@ -33,6 +37,8 @@ require 'linear_api'
|
|
|
33
37
|
LinearApi.configure do |config|
|
|
34
38
|
config.api_key = ENV['LINEAR_API_KEY']
|
|
35
39
|
config.team_id = 'your-team-id' # e.g., 'cfe09c0a-dcae-4cf9-be66-621d00798fcf'
|
|
40
|
+
config.workspace_slug = 'your-workspace' # Used for building issue URLs (default: 'theownerstack')
|
|
41
|
+
config.logger = Rails.logger # Optional: defaults to Rails.logger or stdout
|
|
36
42
|
end
|
|
37
43
|
```
|
|
38
44
|
|
|
@@ -45,7 +51,9 @@ result = LinearApi.client.create_issue(
|
|
|
45
51
|
title: 'Bug: Login fails with special characters',
|
|
46
52
|
description: '## Problem\n\nUsers cannot login when password contains @',
|
|
47
53
|
priority: 2, # 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
48
|
-
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f']
|
|
54
|
+
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f'],
|
|
55
|
+
due_date: '2026-03-01', # ISO 8601 date
|
|
56
|
+
estimate: 3 # Story points
|
|
49
57
|
)
|
|
50
58
|
|
|
51
59
|
if result.success?
|
|
@@ -54,11 +62,16 @@ if result.success?
|
|
|
54
62
|
else
|
|
55
63
|
puts "Error: #{result.error}"
|
|
56
64
|
end
|
|
65
|
+
|
|
66
|
+
# Or use value! to raise on failure:
|
|
67
|
+
issue = LinearApi.client.create_issue(title: 'Bug').value!
|
|
68
|
+
puts issue.identifier
|
|
57
69
|
```
|
|
58
70
|
|
|
59
71
|
### Get an Issue
|
|
60
72
|
|
|
61
73
|
```ruby
|
|
74
|
+
# Direct lookup by identifier (exact match)
|
|
62
75
|
result = LinearApi.client.get_issue('TOS-123')
|
|
63
76
|
|
|
64
77
|
if result.success?
|
|
@@ -66,6 +79,18 @@ if result.success?
|
|
|
66
79
|
puts "Title: #{issue.title}"
|
|
67
80
|
puts "State: #{issue.state_name}"
|
|
68
81
|
puts "Labels: #{issue.label_names.join(', ')}"
|
|
82
|
+
puts "Due: #{issue.due_date}"
|
|
83
|
+
puts "Estimate: #{issue.estimate}"
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Search Issues (Full-Text)
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
result = LinearApi.client.search_issues(term: 'login bug', limit: 10)
|
|
91
|
+
|
|
92
|
+
result.data.each do |issue|
|
|
93
|
+
puts "#{issue.identifier}: #{issue.title}"
|
|
69
94
|
end
|
|
70
95
|
```
|
|
71
96
|
|
|
@@ -97,11 +122,31 @@ result = LinearApi.client.add_comment(
|
|
|
97
122
|
### List Issues
|
|
98
123
|
|
|
99
124
|
```ruby
|
|
125
|
+
# Simple list (up to 50)
|
|
100
126
|
result = LinearApi.client.list_issues(limit: 50)
|
|
101
127
|
|
|
102
128
|
result.data.each do |issue|
|
|
103
129
|
puts "#{issue.identifier}: #{issue.title} [#{issue.state_name}]"
|
|
104
130
|
end
|
|
131
|
+
|
|
132
|
+
# List ALL issues with automatic pagination
|
|
133
|
+
result = LinearApi.client.list_all_issues(batch_size: 50) do |issue|
|
|
134
|
+
# Optional: process each issue as it's fetched
|
|
135
|
+
puts "Fetched: #{issue.identifier}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
puts "Total: #{result.data.length}"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Batch Fetch Issues
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# Fetch multiple issues in a single API call (avoids N+1)
|
|
145
|
+
result = LinearApi.client.batch_get_issues(ids: ['uuid-1', 'uuid-2', 'uuid-3'])
|
|
146
|
+
|
|
147
|
+
result.data.each do |issue|
|
|
148
|
+
puts "#{issue.identifier}: #{issue.state_name}"
|
|
149
|
+
end
|
|
105
150
|
```
|
|
106
151
|
|
|
107
152
|
### List Labels
|
|
@@ -168,6 +213,20 @@ end
|
|
|
168
213
|
LinearApi.client.assign(id: issue.id, assignee_id: 'user-uuid')
|
|
169
214
|
```
|
|
170
215
|
|
|
216
|
+
### Fetch All Metadata (Single API Call)
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# Fetch labels, projects, states, and users in one batched call
|
|
220
|
+
result = LinearApi.client.fetch_all_metadata
|
|
221
|
+
|
|
222
|
+
if result.success?
|
|
223
|
+
puts "Labels: #{result.data[:labels].length}"
|
|
224
|
+
puts "Projects: #{result.data[:projects].length}"
|
|
225
|
+
puts "States: #{result.data[:states].length}"
|
|
226
|
+
puts "Users: #{result.data[:users].length}"
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
171
230
|
### Archive/Unarchive Issues
|
|
172
231
|
|
|
173
232
|
```ruby
|
|
@@ -230,14 +289,31 @@ puts result.data['viewer']['name']
|
|
|
230
289
|
```ruby
|
|
231
290
|
result = LinearApi.client.create_issue(title: 'Test')
|
|
232
291
|
|
|
233
|
-
|
|
234
|
-
when ->(r) { r.success? }
|
|
235
|
-
# Handle success
|
|
292
|
+
if result.success?
|
|
236
293
|
puts result.data.identifier
|
|
237
|
-
|
|
238
|
-
# Handle error
|
|
294
|
+
else
|
|
239
295
|
puts "Error: #{result.error}"
|
|
240
296
|
end
|
|
297
|
+
|
|
298
|
+
# Or use value! to raise on failure (good for scripts)
|
|
299
|
+
begin
|
|
300
|
+
issue = LinearApi.client.create_issue(title: 'Test').value!
|
|
301
|
+
puts issue.identifier
|
|
302
|
+
rescue LinearApi::Error => e
|
|
303
|
+
puts "Failed: #{e.message}"
|
|
304
|
+
rescue LinearApi::RateLimitError => e
|
|
305
|
+
puts "Rate limited, retry after #{e.retry_after}s"
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Health Check
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
if LinearApi.healthy?
|
|
313
|
+
puts "Linear API is reachable"
|
|
314
|
+
else
|
|
315
|
+
puts "Cannot connect to Linear"
|
|
316
|
+
end
|
|
241
317
|
```
|
|
242
318
|
|
|
243
319
|
## Rails Engine (Auto-Tracking)
|
|
@@ -247,10 +323,13 @@ The gem includes a Rails Engine for automatic error tracking with deduplication.
|
|
|
247
323
|
### Setup
|
|
248
324
|
|
|
249
325
|
```bash
|
|
326
|
+
# Verify Linear API connectivity
|
|
327
|
+
bin/rails linear:health
|
|
328
|
+
|
|
250
329
|
# Create the auto-tracking project and label in Linear
|
|
251
330
|
bin/rails linear:setup
|
|
252
331
|
|
|
253
|
-
# Sync labels, projects, and states from Linear
|
|
332
|
+
# Sync labels, projects, and states from Linear (single batched API call)
|
|
254
333
|
bin/rails linear:sync_cache
|
|
255
334
|
```
|
|
256
335
|
|
|
@@ -262,7 +341,7 @@ class DailySalesReportJob < ApplicationJob
|
|
|
262
341
|
def perform(account)
|
|
263
342
|
# ... generate report ...
|
|
264
343
|
rescue StandardError => e
|
|
265
|
-
# Track the error in Linear
|
|
344
|
+
# Track the error in Linear (never raises, returns Result)
|
|
266
345
|
LinearApi::IssueTracker.track_error(
|
|
267
346
|
exception: e,
|
|
268
347
|
context: { date: Date.current, account_id: account.id },
|
|
@@ -287,6 +366,21 @@ LinearApi::IssueTracker.track_issue(
|
|
|
287
366
|
)
|
|
288
367
|
```
|
|
289
368
|
|
|
369
|
+
### Security: Sensitive Data Redaction
|
|
370
|
+
|
|
371
|
+
`IssueTracker` automatically redacts context values whose keys match sensitive patterns (password, secret, token, api_key, credential, authorization):
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
LinearApi::IssueTracker.track_error(
|
|
375
|
+
exception: e,
|
|
376
|
+
context: {
|
|
377
|
+
account_id: 123, # Included as-is
|
|
378
|
+
api_key: 'sk_live_xxx', # Redacted to '[REDACTED]'
|
|
379
|
+
user_email: 'user@test.com' # Included as-is
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
```
|
|
383
|
+
|
|
290
384
|
### How Deduplication Works
|
|
291
385
|
|
|
292
386
|
1. Each error generates a fingerprint based on exception class, message, and backtrace
|
|
@@ -296,13 +390,16 @@ LinearApi::IssueTracker.track_issue(
|
|
|
296
390
|
### Rake Tasks
|
|
297
391
|
|
|
298
392
|
```bash
|
|
393
|
+
# Verify API connectivity
|
|
394
|
+
bin/rails linear:health
|
|
395
|
+
|
|
299
396
|
# Setup auto-tracking project and label
|
|
300
397
|
bin/rails linear:setup
|
|
301
398
|
|
|
302
|
-
# Sync metadata from Linear API
|
|
399
|
+
# Sync metadata from Linear API (single batched call)
|
|
303
400
|
bin/rails linear:sync_cache
|
|
304
401
|
|
|
305
|
-
# Refresh local issue states (
|
|
402
|
+
# Refresh local issue states (batched, checks if resolved)
|
|
306
403
|
bin/rails linear:refresh_issues
|
|
307
404
|
|
|
308
405
|
# Show cache statistics
|
|
@@ -344,9 +441,18 @@ account_issues = LinearApi::SyncedIssue.where(
|
|
|
344
441
|
# Check if an issue is still open
|
|
345
442
|
issue = LinearApi::SyncedIssue.find_by(identifier: 'TOS-123')
|
|
346
443
|
puts issue.open? # => true/false
|
|
347
|
-
puts issue.linear_url # => "https://linear.app/
|
|
444
|
+
puts issue.linear_url # => "https://linear.app/your-workspace/issue/TOS-123"
|
|
348
445
|
```
|
|
349
446
|
|
|
447
|
+
## Resilience Features
|
|
448
|
+
|
|
449
|
+
- **Connection timeouts**: 5s connect, 15s read (configurable per-client)
|
|
450
|
+
- **Automatic retries**: 3 retries with exponential backoff on 500/502/503 and network errors
|
|
451
|
+
- **Rate limit detection**: HTTP 429 raises `RateLimitError` with `retry_after`
|
|
452
|
+
- **Thread safety**: Module-level client access is protected by `Mutex`
|
|
453
|
+
- **Logging**: All API calls, retries, and errors logged via configurable `LinearApi.logger`
|
|
454
|
+
- **Silent error tracking**: `IssueTracker.track_error` never raises -- returns failure `Result` and logs
|
|
455
|
+
|
|
350
456
|
## Development
|
|
351
457
|
|
|
352
458
|
```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
|
|