linear_api 0.3.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 +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +371 -0
- data/app/models/linear_api/application_record.rb +8 -0
- data/app/models/linear_api/cached_metadata.rb +102 -0
- data/app/models/linear_api/synced_issue.rb +76 -0
- data/db/migrate/20260204000001_create_linear_api_cached_metadata.rb +22 -0
- data/db/migrate/20260204000002_create_linear_api_synced_issues.rb +26 -0
- data/lib/linear_api/cache_sync.rb +185 -0
- data/lib/linear_api/client.rb +519 -0
- data/lib/linear_api/engine.rb +27 -0
- data/lib/linear_api/issue.rb +169 -0
- data/lib/linear_api/issue_tracker.rb +291 -0
- data/lib/linear_api/label.rb +40 -0
- data/lib/linear_api/project.rb +43 -0
- data/lib/linear_api/result.rb +36 -0
- data/lib/linear_api/team.rb +51 -0
- data/lib/linear_api/version.rb +5 -0
- data/lib/linear_api.rb +41 -0
- data/lib/tasks/linear_api.rake +164 -0
- metadata +268 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6a4003a10fb0279c739bb5cadef86f1d953ada25452b33bc274cc50c1ac14df9
|
|
4
|
+
data.tar.gz: 5b1e90306471473e8983634c34ea0d4543ab9fd987be9e9917da10d88ae68731
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cfec50c4d35241ed7923ae427a55702e970aaa734d5d2cfd3d51ca7a07cf9667908a5084a997418faff6d56f64b3196f6908bd28296003d53cad0ccdfcfd07d9
|
|
7
|
+
data.tar.gz: 58171286e1925c8b665b1be6f09c5525761a7708abe8390c53b3897bff2bae12f206df8a7ca117fa7da8deb389bad906a70ec8bcad0027120b3e1b1d8484fbdf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-02-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `list_users` - List team members for assignment
|
|
13
|
+
- `archive_issue` - Archive (soft delete) an issue
|
|
14
|
+
- `unarchive_issue` - Restore an archived issue
|
|
15
|
+
- `create_label` - Create new labels programmatically
|
|
16
|
+
- `create_sub_issue` - Create child issues
|
|
17
|
+
- `list_sub_issues` - List children of an issue
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2026-02-01
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Initial release
|
|
24
|
+
- `LinearApi::Client` for GraphQL API communication
|
|
25
|
+
- Issue management: create, update, get, list
|
|
26
|
+
- Comment support for issues
|
|
27
|
+
- Project and label listing
|
|
28
|
+
- Workflow state querying
|
|
29
|
+
- `LinearApi::Result` for consistent response handling
|
|
30
|
+
- RSpec test suite with WebMock
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheOwnerStack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# LinearApi
|
|
2
|
+
|
|
3
|
+
A Ruby gem for interacting with the [Linear](https://linear.app) GraphQL API. Create, update, and query issues, projects, and labels programmatically. Includes a Rails Engine for automatic error tracking with deduplication.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'linear_api', path: '../linear'
|
|
11
|
+
# Or from GitHub:
|
|
12
|
+
gem 'linear_api', git: 'https://github.com/dan1d/linear_api.git'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For Rails applications, run the migrations:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bin/rails linear_api:install:migrations
|
|
25
|
+
bin/rails db:migrate
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require 'linear_api'
|
|
32
|
+
|
|
33
|
+
LinearApi.configure do |config|
|
|
34
|
+
config.api_key = ENV['LINEAR_API_KEY']
|
|
35
|
+
config.team_id = 'your-team-id' # e.g., 'cfe09c0a-dcae-4cf9-be66-621d00798fcf'
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Create an Issue
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
result = LinearApi.client.create_issue(
|
|
45
|
+
title: 'Bug: Login fails with special characters',
|
|
46
|
+
description: '## Problem\n\nUsers cannot login when password contains @',
|
|
47
|
+
priority: 2, # 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
48
|
+
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f'] # type:bug
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if result.success?
|
|
52
|
+
puts "Created: #{result.data.identifier}" # => "TOS-123"
|
|
53
|
+
puts "URL: #{result.data.url}"
|
|
54
|
+
else
|
|
55
|
+
puts "Error: #{result.error}"
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Get an Issue
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
result = LinearApi.client.get_issue('TOS-123')
|
|
63
|
+
|
|
64
|
+
if result.success?
|
|
65
|
+
issue = result.data
|
|
66
|
+
puts "Title: #{issue.title}"
|
|
67
|
+
puts "State: #{issue.state_name}"
|
|
68
|
+
puts "Labels: #{issue.label_names.join(', ')}"
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Update an Issue
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Update state
|
|
76
|
+
result = LinearApi.client.update_issue(
|
|
77
|
+
id: 'issue-uuid',
|
|
78
|
+
state_id: 'state-uuid'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Add labels
|
|
82
|
+
result = LinearApi.client.update_issue(
|
|
83
|
+
id: 'issue-uuid',
|
|
84
|
+
label_ids: ['label-1', 'label-2']
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Add a Comment
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
result = LinearApi.client.add_comment(
|
|
92
|
+
issue_id: 'issue-uuid',
|
|
93
|
+
body: '## QA Results\n\n✅ All tests passed'
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### List Issues
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
result = LinearApi.client.list_issues(limit: 50)
|
|
101
|
+
|
|
102
|
+
result.data.each do |issue|
|
|
103
|
+
puts "#{issue.identifier}: #{issue.title} [#{issue.state_name}]"
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### List Labels
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
result = LinearApi.client.list_labels
|
|
111
|
+
|
|
112
|
+
result.data.each do |label|
|
|
113
|
+
puts "#{label.name}: #{label.id}"
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Projects
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# List all projects
|
|
121
|
+
result = LinearApi.client.list_projects
|
|
122
|
+
|
|
123
|
+
result.data.each do |project|
|
|
124
|
+
puts "#{project.name}: #{project.id}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Create a project
|
|
128
|
+
result = LinearApi.client.create_project(
|
|
129
|
+
name: 'Test Coverage and Automation',
|
|
130
|
+
description: 'Track QA automation and test coverage improvements',
|
|
131
|
+
color: '#4338ca'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if result.success?
|
|
135
|
+
puts "Created project: #{result.data.name}"
|
|
136
|
+
puts "URL: #{result.data.url}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Add an issue to a project
|
|
140
|
+
result = LinearApi.client.create_issue(
|
|
141
|
+
title: 'Add integration tests for payment flow',
|
|
142
|
+
description: '## Objective\n\nImprove test coverage for payment processing',
|
|
143
|
+
project_id: 'project-uuid',
|
|
144
|
+
priority: 2
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Get Workflow States
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
result = LinearApi.client.list_states
|
|
152
|
+
|
|
153
|
+
result.data.each do |state|
|
|
154
|
+
puts "#{state['name']} (#{state['type']}): #{state['id']}"
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### List Team Members (for Assignment)
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
result = LinearApi.client.list_users
|
|
162
|
+
|
|
163
|
+
result.data.each do |user|
|
|
164
|
+
puts "#{user['name']} (#{user['email']}): #{user['id']}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Assign to a user
|
|
168
|
+
LinearApi.client.assign(id: issue.id, assignee_id: 'user-uuid')
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Archive/Unarchive Issues
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Archive (soft delete)
|
|
175
|
+
LinearApi.client.archive_issue(id: 'issue-uuid')
|
|
176
|
+
|
|
177
|
+
# Restore
|
|
178
|
+
LinearApi.client.unarchive_issue(id: 'issue-uuid')
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Create Labels
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
result = LinearApi.client.create_label(
|
|
185
|
+
name: 'priority:critical',
|
|
186
|
+
color: '#ff0000',
|
|
187
|
+
description: 'Critical priority issues'
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
puts "Created label: #{result.data.id}"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Sub-Issues
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Create a sub-issue
|
|
197
|
+
result = LinearApi.client.create_sub_issue(
|
|
198
|
+
parent_id: 'parent-issue-uuid',
|
|
199
|
+
title: 'Sub-task: Write tests',
|
|
200
|
+
priority: 3
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# List sub-issues
|
|
204
|
+
children = LinearApi.client.list_sub_issues(parent_id: 'parent-issue-uuid')
|
|
205
|
+
children.data.each do |child|
|
|
206
|
+
puts "#{child.identifier}: #{child.title}"
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Direct GraphQL Queries
|
|
211
|
+
|
|
212
|
+
For advanced use cases, execute raw GraphQL:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
query = <<~GRAPHQL
|
|
216
|
+
query {
|
|
217
|
+
viewer {
|
|
218
|
+
name
|
|
219
|
+
email
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
GRAPHQL
|
|
223
|
+
|
|
224
|
+
result = LinearApi.client.query(query)
|
|
225
|
+
puts result.data['viewer']['name']
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error Handling
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
result = LinearApi.client.create_issue(title: 'Test')
|
|
232
|
+
|
|
233
|
+
case result
|
|
234
|
+
when ->(r) { r.success? }
|
|
235
|
+
# Handle success
|
|
236
|
+
puts result.data.identifier
|
|
237
|
+
when ->(r) { r.failure? }
|
|
238
|
+
# Handle error
|
|
239
|
+
puts "Error: #{result.error}"
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Rails Engine (Auto-Tracking)
|
|
244
|
+
|
|
245
|
+
The gem includes a Rails Engine for automatic error tracking with deduplication. Errors with the same fingerprint are grouped together instead of creating duplicate issues.
|
|
246
|
+
|
|
247
|
+
### Setup
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Create the auto-tracking project and label in Linear
|
|
251
|
+
bin/rails linear:setup
|
|
252
|
+
|
|
253
|
+
# Sync labels, projects, and states from Linear
|
|
254
|
+
bin/rails linear:sync_cache
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Track Errors Automatically
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# In your service or job
|
|
261
|
+
class DailySalesReportJob < ApplicationJob
|
|
262
|
+
def perform(account)
|
|
263
|
+
# ... generate report ...
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
# Track the error in Linear
|
|
266
|
+
LinearApi::IssueTracker.track_error(
|
|
267
|
+
exception: e,
|
|
268
|
+
context: { date: Date.current, account_id: account.id },
|
|
269
|
+
source: account,
|
|
270
|
+
labels: [:bug, :sync]
|
|
271
|
+
)
|
|
272
|
+
raise # Re-raise after tracking
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Track Custom Issues
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Create a tracked issue (not from an exception)
|
|
281
|
+
LinearApi::IssueTracker.track_issue(
|
|
282
|
+
title: 'Missing API credentials for merchant',
|
|
283
|
+
description: "Merchant #{merchant.name} has invalid Clover credentials",
|
|
284
|
+
context: { merchant_id: merchant.id },
|
|
285
|
+
source: merchant,
|
|
286
|
+
labels: [:bug, :area_sync]
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### How Deduplication Works
|
|
291
|
+
|
|
292
|
+
1. Each error generates a fingerprint based on exception class, message, and backtrace
|
|
293
|
+
2. If an open issue exists with the same fingerprint, a comment is added instead
|
|
294
|
+
3. Once an issue is marked "Done" or "Canceled", new occurrences create a fresh issue
|
|
295
|
+
|
|
296
|
+
### Rake Tasks
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Setup auto-tracking project and label
|
|
300
|
+
bin/rails linear:setup
|
|
301
|
+
|
|
302
|
+
# Sync metadata from Linear API
|
|
303
|
+
bin/rails linear:sync_cache
|
|
304
|
+
|
|
305
|
+
# Refresh local issue states (check if resolved)
|
|
306
|
+
bin/rails linear:refresh_issues
|
|
307
|
+
|
|
308
|
+
# Show cache statistics
|
|
309
|
+
bin/rails linear:stats
|
|
310
|
+
|
|
311
|
+
# Export cached metadata to JSON
|
|
312
|
+
bin/rails linear:export_cache
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Cached Metadata
|
|
316
|
+
|
|
317
|
+
The engine caches Linear metadata locally for fast lookups:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# Find a label by name
|
|
321
|
+
label = LinearApi::CachedMetadata.labels.find_by(name: 'type:bug')
|
|
322
|
+
|
|
323
|
+
# Get all projects
|
|
324
|
+
projects = LinearApi::CachedMetadata.projects.pluck(:name, :linear_id)
|
|
325
|
+
|
|
326
|
+
# Resolve label ID from key
|
|
327
|
+
bug_id = LinearApi::CachedMetadata.resolve_label_id('type:bug')
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Synced Issues
|
|
331
|
+
|
|
332
|
+
Track which issues have been created:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# Find all unresolved tracked issues
|
|
336
|
+
open_issues = LinearApi::SyncedIssue.unresolved
|
|
337
|
+
|
|
338
|
+
# Find issues for a specific source
|
|
339
|
+
account_issues = LinearApi::SyncedIssue.where(
|
|
340
|
+
source_type: 'CloverAccount',
|
|
341
|
+
source_id: account.id
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Check if an issue is still open
|
|
345
|
+
issue = LinearApi::SyncedIssue.find_by(identifier: 'TOS-123')
|
|
346
|
+
puts issue.open? # => true/false
|
|
347
|
+
puts issue.linear_url # => "https://linear.app/theownerstack/issue/TOS-123"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Development
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Install dependencies
|
|
354
|
+
bundle install
|
|
355
|
+
|
|
356
|
+
# Run tests
|
|
357
|
+
bundle exec rspec
|
|
358
|
+
|
|
359
|
+
# Run tests with VCR recording (hits live API)
|
|
360
|
+
VCR_RECORD=1 bundle exec rspec
|
|
361
|
+
|
|
362
|
+
# Run linter
|
|
363
|
+
bundle exec rubocop
|
|
364
|
+
|
|
365
|
+
# Run all checks
|
|
366
|
+
bundle exec rake
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT License. See LICENSE.txt.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinearApi
|
|
4
|
+
class CachedMetadata < ApplicationRecord
|
|
5
|
+
TYPES = %w[label project state user].freeze
|
|
6
|
+
|
|
7
|
+
validates :metadata_type, presence: true, inclusion: { in: TYPES }
|
|
8
|
+
validates :linear_id, presence: true, uniqueness: { scope: :metadata_type }
|
|
9
|
+
|
|
10
|
+
# Type scopes
|
|
11
|
+
scope :labels, -> { where(metadata_type: 'label') }
|
|
12
|
+
scope :projects, -> { where(metadata_type: 'project') }
|
|
13
|
+
scope :states, -> { where(metadata_type: 'state') }
|
|
14
|
+
scope :users, -> { where(metadata_type: 'user') }
|
|
15
|
+
|
|
16
|
+
# Category scopes for labels
|
|
17
|
+
scope :by_category, ->(cat) { where(category: cat) }
|
|
18
|
+
scope :type_labels, -> { labels.by_category('type') }
|
|
19
|
+
scope :area_labels, -> { labels.by_category('area') }
|
|
20
|
+
scope :priority_labels, -> { labels.by_category('priority') }
|
|
21
|
+
|
|
22
|
+
# State type scopes
|
|
23
|
+
scope :completed_states, -> { states.where(state_type: 'completed') }
|
|
24
|
+
scope :active_states, -> { states.where(state_type: %w[unstarted started]) }
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Common label lookups
|
|
28
|
+
def bug_label_id
|
|
29
|
+
labels.find_by(key: 'type:bug')&.linear_id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def feature_label_id
|
|
33
|
+
labels.find_by(key: 'type:feature')&.linear_id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sync_label_id
|
|
37
|
+
labels.find_by(key: 'area:sync')&.linear_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def infrastructure_label_id
|
|
41
|
+
labels.find_by(key: 'area:infrastructure')&.linear_id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def critical_priority_id
|
|
45
|
+
labels.find_by(key: 'priority:critical')&.linear_id
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def high_priority_id
|
|
49
|
+
labels.find_by(key: 'priority:high')&.linear_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# State lookups
|
|
53
|
+
def backlog_state_id
|
|
54
|
+
states.find_by(key: 'backlog')&.linear_id
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def todo_state_id
|
|
58
|
+
states.find_by(key: 'todo')&.linear_id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def in_progress_state_id
|
|
62
|
+
states.find_by(key: 'in_progress')&.linear_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def done_state_id
|
|
66
|
+
states.find_by(key: 'done')&.linear_id
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Project lookups
|
|
70
|
+
def daily_operations_project_id
|
|
71
|
+
projects.find_by(key: 'daily_operations')&.linear_id
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tech_debt_project_id
|
|
75
|
+
projects.find_by(key: 'tech_debt')&.linear_id
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolve symbolic label to ID
|
|
79
|
+
def resolve_label_id(label)
|
|
80
|
+
case label
|
|
81
|
+
when :bug then bug_label_id
|
|
82
|
+
when :feature then feature_label_id
|
|
83
|
+
when :sync then sync_label_id
|
|
84
|
+
when :infrastructure then infrastructure_label_id
|
|
85
|
+
when :critical then critical_priority_id
|
|
86
|
+
when :high then high_priority_id
|
|
87
|
+
when String then label # Already an ID
|
|
88
|
+
else
|
|
89
|
+
labels.find_by(key: label.to_s)&.linear_id
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Upsert from API data
|
|
94
|
+
def upsert_from_api(type:, linear_id:, name:, **attrs)
|
|
95
|
+
find_or_initialize_by(metadata_type: type, linear_id: linear_id).tap do |record|
|
|
96
|
+
record.assign_attributes(name: name, **attrs)
|
|
97
|
+
record.save!
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinearApi
|
|
4
|
+
class SyncedIssue < ApplicationRecord
|
|
5
|
+
validates :linear_id, presence: true, uniqueness: true
|
|
6
|
+
validates :fingerprint, presence: true
|
|
7
|
+
|
|
8
|
+
# Source polymorphism (without actual belongs_to for flexibility)
|
|
9
|
+
scope :for_source, ->(type, id) { where(source_type: type.to_s, source_id: id.to_s) }
|
|
10
|
+
scope :by_fingerprint, ->(fp) { where(fingerprint: fp) }
|
|
11
|
+
|
|
12
|
+
# State scopes
|
|
13
|
+
scope :unresolved, -> { where.not(state_name: %w[Done Canceled Duplicate]) }
|
|
14
|
+
scope :resolved, -> { where(state_name: 'Done') }
|
|
15
|
+
scope :open, -> { where(state_name: %w[Backlog Todo]) }
|
|
16
|
+
scope :in_progress, -> { where(state_name: ['In Progress', 'In Review', 'QA']) }
|
|
17
|
+
|
|
18
|
+
# Time scopes
|
|
19
|
+
scope :recent, -> { where('created_at > ?', 7.days.ago) }
|
|
20
|
+
scope :stale, -> { unresolved.where('synced_at < ?', 1.day.ago) }
|
|
21
|
+
|
|
22
|
+
# Check if this issue is still open in Linear
|
|
23
|
+
def open?
|
|
24
|
+
!%w[Done Canceled Duplicate].include?(state_name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolved?
|
|
28
|
+
state_name == 'Done'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sync state from Linear
|
|
32
|
+
def refresh_from_linear!
|
|
33
|
+
result = LinearApi.client.get_issue_by_id(linear_id)
|
|
34
|
+
return false unless result.success?
|
|
35
|
+
|
|
36
|
+
update!(
|
|
37
|
+
state_name: result.data.state_name,
|
|
38
|
+
title: result.data.title,
|
|
39
|
+
priority: result.data.priority,
|
|
40
|
+
synced_at: Time.current
|
|
41
|
+
)
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build Linear URL
|
|
46
|
+
def linear_url
|
|
47
|
+
"https://linear.app/theownerstack/issue/#{identifier}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parse metadata JSON
|
|
51
|
+
def parsed_metadata
|
|
52
|
+
return {} unless metadata
|
|
53
|
+
|
|
54
|
+
metadata.is_a?(String) ? JSON.parse(metadata) : metadata
|
|
55
|
+
rescue JSON::ParserError
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class << self
|
|
60
|
+
# Find existing open issue for this fingerprint
|
|
61
|
+
def find_open_for_fingerprint(fingerprint)
|
|
62
|
+
by_fingerprint(fingerprint).unresolved.order(created_at: :desc).first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if we should create a new issue or add to existing
|
|
66
|
+
def should_create_new?(fingerprint)
|
|
67
|
+
find_open_for_fingerprint(fingerprint).nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Refresh all stale issues
|
|
71
|
+
def refresh_stale_issues!
|
|
72
|
+
stale.find_each(&:refresh_from_linear!)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLinearApiCachedMetadata < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :linear_api_cached_metadata do |t|
|
|
6
|
+
t.string :metadata_type, null: false # "label", "project", "state", "user"
|
|
7
|
+
t.string :linear_id, null: false
|
|
8
|
+
t.string :name
|
|
9
|
+
t.string :key # For lookup (e.g., "type:bug", "area:sync", "backlog")
|
|
10
|
+
t.string :color
|
|
11
|
+
t.string :category # For labels: "type", "area", "priority", "qa"
|
|
12
|
+
t.string :state_type # For states: "backlog", "unstarted", "started", "completed", "canceled"
|
|
13
|
+
t.json :raw_data
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :linear_api_cached_metadata, %i[metadata_type linear_id], unique: true, name: 'idx_linear_cached_metadata_type_id'
|
|
19
|
+
add_index :linear_api_cached_metadata, %i[metadata_type key], name: 'idx_linear_cached_metadata_type_key'
|
|
20
|
+
add_index :linear_api_cached_metadata, :category
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLinearApiSyncedIssues < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :linear_api_synced_issues do |t|
|
|
6
|
+
t.string :linear_id, null: false
|
|
7
|
+
t.string :identifier # TOS-123
|
|
8
|
+
t.string :title
|
|
9
|
+
t.string :state_name
|
|
10
|
+
t.integer :priority
|
|
11
|
+
t.string :source_type # "AccountLinking", "DailySalesReport", etc.
|
|
12
|
+
t.string :source_id # ID of the related record
|
|
13
|
+
t.string :fingerprint, null: false # For deduplication
|
|
14
|
+
t.json :metadata # Extra context (date, error details, etc.)
|
|
15
|
+
t.datetime :synced_at
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :linear_api_synced_issues, :linear_id, unique: true
|
|
21
|
+
add_index :linear_api_synced_issues, :identifier
|
|
22
|
+
add_index :linear_api_synced_issues, :fingerprint
|
|
23
|
+
add_index :linear_api_synced_issues, %i[source_type source_id], name: 'idx_linear_synced_issues_source'
|
|
24
|
+
add_index :linear_api_synced_issues, :state_name
|
|
25
|
+
end
|
|
26
|
+
end
|