Skip to content

Commit 9aeae63

Browse files
feat(errors): adopt IntegrationError in Linear integration (4 files)
- client.ts: 8 errors updated to IntegrationError with LINEAR_* codes - auth.ts: 8 errors updated for OAuth/token flows - sync-service.ts: 5 errors updated for sync operations - unified-sync.ts: 4 errors updated for unified sync STA-186: Error Handling Infrastructure (Linear integration progress)
1 parent c7e1382 commit 9aeae63

4 files changed

Lines changed: 171 additions & 91 deletions

File tree

src/integrations/linear/auth.ts

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,7 @@ import { createHash, randomBytes } from 'crypto';
77
import { readFileSync, writeFileSync, existsSync } from 'fs';
88
import { join } from 'path';
99
import { logger } from '../../core/monitoring/logger.js';
10-
// Type-safe environment variable access
11-
function getEnv(key: string, defaultValue?: string): string {
12-
const value = process.env[key];
13-
if (value === undefined) {
14-
if (defaultValue !== undefined) return defaultValue;
15-
throw new Error(`Environment variable ${key} is required`);
16-
}
17-
return value;
18-
}
19-
20-
function getOptionalEnv(key: string): string | undefined {
21-
return process.env[key];
22-
}
23-
10+
import { IntegrationError, ErrorCode } from '../../core/errors/index.js';
2411

2512
export interface LinearAuthConfig {
2613
clientId: string;
@@ -94,7 +81,10 @@ export class LinearAuthManager {
9481
*/
9582
generateAuthUrl(state?: string): { url: string; codeVerifier: string } {
9683
if (!this.config) {
97-
throw new Error('Linear OAuth configuration not loaded');
84+
throw new IntegrationError(
85+
'Linear OAuth configuration not loaded',
86+
ErrorCode.LINEAR_AUTH_FAILED
87+
);
9888
}
9989

10090
// Generate PKCE parameters
@@ -130,7 +120,10 @@ export class LinearAuthManager {
130120
codeVerifier: string
131121
): Promise<LinearTokens> {
132122
if (!this.config) {
133-
throw new Error('Linear OAuth configuration not loaded');
123+
throw new IntegrationError(
124+
'Linear OAuth configuration not loaded',
125+
ErrorCode.LINEAR_AUTH_FAILED
126+
);
134127
}
135128

136129
const tokenUrl = 'https://api.linear.app/oauth/token';
@@ -155,7 +148,11 @@ export class LinearAuthManager {
155148

156149
if (!response.ok) {
157150
const errorText = await response.text();
158-
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
151+
throw new IntegrationError(
152+
`Token exchange failed: ${response.status}`,
153+
ErrorCode.LINEAR_AUTH_FAILED,
154+
{ status: response.status, body: errorText }
155+
);
159156
}
160157

161158
const result = (await response.json()) as LinearAuthResult;
@@ -179,12 +176,18 @@ export class LinearAuthManager {
179176
*/
180177
async refreshAccessToken(): Promise<LinearTokens> {
181178
if (!this.config) {
182-
throw new Error('Linear OAuth configuration not loaded');
179+
throw new IntegrationError(
180+
'Linear OAuth configuration not loaded',
181+
ErrorCode.LINEAR_AUTH_FAILED
182+
);
183183
}
184184

185185
const currentTokens = this.loadTokens();
186186
if (!currentTokens?.refreshToken) {
187-
throw new Error('No refresh token available');
187+
throw new IntegrationError(
188+
'No refresh token available',
189+
ErrorCode.LINEAR_AUTH_FAILED
190+
);
188191
}
189192

190193
const tokenUrl = 'https://api.linear.app/oauth/token';
@@ -207,7 +210,11 @@ export class LinearAuthManager {
207210

208211
if (!response.ok) {
209212
const errorText = await response.text();
210-
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
213+
throw new IntegrationError(
214+
`Token refresh failed: ${response.status}`,
215+
ErrorCode.LINEAR_AUTH_FAILED,
216+
{ status: response.status, body: errorText }
217+
);
211218
}
212219

213220
const result = (await response.json()) as LinearAuthResult;
@@ -229,7 +236,10 @@ export class LinearAuthManager {
229236
async getValidToken(): Promise<string> {
230237
const tokens = this.loadTokens();
231238
if (!tokens) {
232-
throw new Error('No Linear tokens found. Please complete OAuth setup.');
239+
throw new IntegrationError(
240+
'No Linear tokens found. Please complete OAuth setup.',
241+
ErrorCode.LINEAR_AUTH_FAILED
242+
);
233243
}
234244

235245
// Check if token expires in next 5 minutes
@@ -362,8 +372,9 @@ export class LinearOAuthSetup {
362372
try {
363373
const codeVerifier = process.env['_LINEAR_CODE_VERIFIER'];
364374
if (!codeVerifier) {
365-
throw new Error(
366-
'Code verifier not found. Please restart the setup process.'
375+
throw new IntegrationError(
376+
'Code verifier not found. Please restart the setup process.',
377+
ErrorCode.LINEAR_AUTH_FAILED
367378
);
368379
}
369380

src/integrations/linear/client.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { logger } from '../../core/monitoring/logger.js';
7+
import { IntegrationError, ErrorCode } from '../../core/errors/index.js';
78

89
export interface LinearConfig {
910
apiKey: string;
@@ -75,7 +76,10 @@ export class LinearClient {
7576
this.baseUrl = config.baseUrl || 'https://api.linear.app';
7677

7778
if (!config.apiKey) {
78-
throw new Error('Linear API key is required');
79+
throw new IntegrationError(
80+
'Linear API key is required',
81+
ErrorCode.LINEAR_AUTH_FAILED
82+
);
7983
}
8084
}
8185

@@ -206,7 +210,11 @@ export class LinearClient {
206210
await this.sleep(waitTime);
207211
return this.graphql<T>(query, variables, retries - 1, allowAuthRefresh);
208212
}
209-
throw new Error('Linear API rate limit exceeded after retries');
213+
throw new IntegrationError(
214+
'Linear API rate limit exceeded after retries',
215+
ErrorCode.LINEAR_API_ERROR,
216+
{ retries: 0 }
217+
);
210218
}
211219

212220
if (!response.ok) {
@@ -215,8 +223,14 @@ export class LinearClient {
215223
'Linear API error response:',
216224
new Error(`${response.status}: ${errorText}`)
217225
);
218-
throw new Error(
219-
`Linear API error: ${response.status} ${response.statusText} - ${errorText}`
226+
throw new IntegrationError(
227+
`Linear API error: ${response.status} ${response.statusText}`,
228+
ErrorCode.LINEAR_API_ERROR,
229+
{
230+
status: response.status,
231+
statusText: response.statusText,
232+
body: errorText,
233+
}
220234
);
221235
}
222236

@@ -244,7 +258,11 @@ export class LinearClient {
244258
}
245259

246260
logger.error('Linear GraphQL errors:', { errors: result.errors });
247-
throw new Error(`Linear GraphQL error: ${result.errors[0].message}`);
261+
throw new IntegrationError(
262+
`Linear GraphQL error: ${result.errors[0].message}`,
263+
ErrorCode.LINEAR_API_ERROR,
264+
{ errors: result.errors }
265+
);
248266
}
249267

250268
return result.data as T;
@@ -297,7 +315,11 @@ export class LinearClient {
297315
}>(mutation, { input });
298316

299317
if (!result.issueCreate.success) {
300-
throw new Error('Failed to create Linear issue');
318+
throw new IntegrationError(
319+
'Failed to create Linear issue',
320+
ErrorCode.LINEAR_API_ERROR,
321+
{ input }
322+
);
301323
}
302324

303325
return result.issueCreate.issue;
@@ -353,7 +375,11 @@ export class LinearClient {
353375
}>(mutation, { id: issueId, input: updates });
354376

355377
if (!result.issueUpdate.success) {
356-
throw new Error(`Failed to update Linear issue ${issueId}`);
378+
throw new IntegrationError(
379+
`Failed to update Linear issue ${issueId}`,
380+
ErrorCode.LINEAR_API_ERROR,
381+
{ issueId, updates }
382+
);
357383
}
358384

359385
return result.issueUpdate.issue;
@@ -488,7 +514,11 @@ export class LinearClient {
488514
team: { id: string; name: string; key: string };
489515
}>(query, { id: teamId });
490516
if (!result.team) {
491-
throw new Error(`Team ${teamId} not found`);
517+
throw new IntegrationError(
518+
`Team ${teamId} not found`,
519+
ErrorCode.LINEAR_API_ERROR,
520+
{ teamId }
521+
);
492522
}
493523
return result.team;
494524
} else {
@@ -499,7 +529,10 @@ export class LinearClient {
499529
}>(query);
500530

501531
if (result.teams.nodes.length === 0) {
502-
throw new Error('No teams found');
532+
throw new IntegrationError(
533+
'No teams found',
534+
ErrorCode.LINEAR_API_ERROR
535+
);
503536
}
504537

505538
return result.teams.nodes[0]!;

src/integrations/linear/sync-service.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,9 @@
1-
import { LinearClient, LinearIssue, LinearCreateIssueInput } from './client.js';
1+
import { LinearClient, LinearCreateIssueInput } from './client.js';
22
import { ContextService } from '../../services/context-service.js';
33
import { ConfigService } from '../../services/config-service.js';
44
import { logger } from '../../core/monitoring/logger.js';
55
import { Task, TaskStatus, TaskPriority } from '../../types/task.js';
6-
// Type-safe environment variable access
7-
function getEnv(key: string, defaultValue?: string): string {
8-
const value = process.env[key];
9-
if (value === undefined) {
10-
if (defaultValue !== undefined) return defaultValue;
11-
throw new Error(`Environment variable ${key} is required`);
12-
}
13-
return value;
14-
}
15-
16-
function getOptionalEnv(key: string): string | undefined {
17-
return process.env[key];
18-
}
19-
6+
import { IntegrationError, ErrorCode } from '../../core/errors/index.js';
207

218
// Minimal issue data needed for sync (webhook payloads may have fewer fields)
229
export interface LinearIssueData {
@@ -53,7 +40,10 @@ export class LinearSyncService {
5340

5441
const apiKey = process.env['LINEAR_API_KEY'];
5542
if (!apiKey) {
56-
throw new Error('LINEAR_API_KEY environment variable not set');
43+
throw new IntegrationError(
44+
'LINEAR_API_KEY environment variable not set',
45+
ErrorCode.LINEAR_AUTH_FAILED
46+
);
5747
}
5848

5949
this.linearClient = new LinearClient({ apiKey });
@@ -73,7 +63,10 @@ export class LinearSyncService {
7363
const teamId = config.integrations?.linear?.teamId;
7464

7565
if (!teamId) {
76-
throw new Error('Linear team ID not configured');
66+
throw new IntegrationError(
67+
'Linear team ID not configured',
68+
ErrorCode.LINEAR_SYNC_FAILED
69+
);
7770
}
7871

7972
const issues = await this.linearClient.getIssues({ teamId });
@@ -133,7 +126,11 @@ export class LinearSyncService {
133126
try {
134127
const task = await this.contextService.getTask(taskId);
135128
if (!task) {
136-
throw new Error(`Task ${taskId} not found`);
129+
throw new IntegrationError(
130+
`Task ${taskId} not found`,
131+
ErrorCode.LINEAR_SYNC_FAILED,
132+
{ taskId }
133+
);
137134
}
138135

139136
if (task.externalId) {
@@ -148,7 +145,10 @@ export class LinearSyncService {
148145
const config = await this.configService.getConfig();
149146
const teamId = config.integrations?.linear?.teamId;
150147
if (!teamId) {
151-
throw new Error('Linear team ID not configured');
148+
throw new IntegrationError(
149+
'Linear team ID not configured',
150+
ErrorCode.LINEAR_SYNC_FAILED
151+
);
152152
}
153153
const createData: LinearCreateIssueInput = {
154154
title: task.title,

0 commit comments

Comments
 (0)