lorenz 0.1.10 → 0.1.11
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.
- package/README.md +1 -1
- package/RELEASE-MANIFEST.json +1 -1
- package/node_modules/@agentclientprotocol/claude-agent-acp/dist/bundle.js +1 -1
- package/node_modules/@lorenz/config/dist/parse.d.ts.map +1 -1
- package/node_modules/@lorenz/config/dist/parse.js +88 -21
- package/node_modules/@lorenz/config/dist/parse.js.map +1 -1
- package/node_modules/@lorenz/dashboard/dist/assets/index-CInuwNxv.js +227 -0
- package/node_modules/@lorenz/dashboard/dist/index.html +1 -1
- package/node_modules/@lorenz/domain/dist/index.d.ts +6 -0
- package/node_modules/@lorenz/domain/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/domain/dist/index.js +67 -0
- package/node_modules/@lorenz/domain/dist/index.js.map +1 -1
- package/node_modules/@lorenz/jira-tracker/dist/client.d.ts +13 -2
- package/node_modules/@lorenz/jira-tracker/dist/client.d.ts.map +1 -1
- package/node_modules/@lorenz/jira-tracker/dist/client.js +90 -34
- package/node_modules/@lorenz/jira-tracker/dist/client.js.map +1 -1
- package/node_modules/@lorenz/jira-tracker/dist/tools.d.ts.map +1 -1
- package/node_modules/@lorenz/jira-tracker/dist/tools.js +2 -2
- package/node_modules/@lorenz/jira-tracker/dist/tools.js.map +1 -1
- package/node_modules/@lorenz/linear-tracker/dist/client.d.ts +22 -0
- package/node_modules/@lorenz/linear-tracker/dist/client.d.ts.map +1 -1
- package/node_modules/@lorenz/linear-tracker/dist/client.js +357 -107
- package/node_modules/@lorenz/linear-tracker/dist/client.js.map +1 -1
- package/node_modules/@lorenz/linear-tracker/dist/diagnostics.d.ts +3 -0
- package/node_modules/@lorenz/linear-tracker/dist/diagnostics.d.ts.map +1 -0
- package/node_modules/@lorenz/linear-tracker/dist/diagnostics.js +30 -0
- package/node_modules/@lorenz/linear-tracker/dist/diagnostics.js.map +1 -0
- package/node_modules/@lorenz/linear-tracker/dist/tools.d.ts.map +1 -1
- package/node_modules/@lorenz/linear-tracker/dist/tools.js +8 -33
- package/node_modules/@lorenz/linear-tracker/dist/tools.js.map +1 -1
- package/node_modules/@lorenz/orchestrator/dist/codec.js +2 -0
- package/node_modules/@lorenz/orchestrator/dist/codec.js.map +1 -1
- package/node_modules/@lorenz/orchestrator/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/orchestrator/dist/index.js +15 -0
- package/node_modules/@lorenz/orchestrator/dist/index.js.map +1 -1
- package/node_modules/@lorenz/presenter/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/presenter/dist/index.js +7 -7
- package/node_modules/@lorenz/presenter/dist/index.js.map +1 -1
- package/node_modules/@lorenz/projections/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/projections/dist/index.js +1 -0
- package/node_modules/@lorenz/projections/dist/index.js.map +1 -1
- package/node_modules/@lorenz/runtime/dist/index.d.ts +1 -0
- package/node_modules/@lorenz/runtime/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/runtime/dist/index.js +28 -15
- package/node_modules/@lorenz/runtime/dist/index.js.map +1 -1
- package/node_modules/@lorenz/runtime-events/dist/index.d.ts +3 -0
- package/node_modules/@lorenz/runtime-events/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/traceviz-emitter/dist/index.d.ts +1 -1
- package/node_modules/@lorenz/traceviz-emitter/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/traceviz-emitter/dist/index.js +3 -2
- package/node_modules/@lorenz/traceviz-emitter/dist/index.js.map +1 -1
- package/node_modules/@lorenz/traceviz-server/dist/parser.js +2 -2
- package/node_modules/@lorenz/traceviz-server/dist/parser.js.map +1 -1
- package/node_modules/@lorenz/tracker-sdk/dist/index.d.ts +1 -0
- package/node_modules/@lorenz/tracker-sdk/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/tracker-sdk/dist/index.js +1 -0
- package/node_modules/@lorenz/tracker-sdk/dist/index.js.map +1 -1
- package/node_modules/@lorenz/tracker-sdk/dist/pagination.d.ts +27 -0
- package/node_modules/@lorenz/tracker-sdk/dist/pagination.d.ts.map +1 -0
- package/node_modules/@lorenz/tracker-sdk/dist/pagination.js +58 -0
- package/node_modules/@lorenz/tracker-sdk/dist/pagination.js.map +1 -0
- package/node_modules/@lorenz/tui/dist/index.d.ts +3 -1
- package/node_modules/@lorenz/tui/dist/index.d.ts.map +1 -1
- package/node_modules/@lorenz/tui/dist/index.js +21 -24
- package/node_modules/@lorenz/tui/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/node_modules/@lorenz/dashboard/dist/assets/index-DD4yVBDF.js +0 -227
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { LinearGraphQLClient } from "@linear/sdk";
|
|
2
2
|
export { LinearGraphQLClient } from "@linear/sdk";
|
|
3
3
|
import { normalizeIssue } from "@lorenz/issue";
|
|
4
|
-
import {
|
|
4
|
+
import { createTrackerPaginationGuard } from "@lorenz/tracker-sdk";
|
|
5
|
+
import { errorMessage, isRecord, normalizeStateType, redactDiagnosticText, } from "@lorenz/domain";
|
|
6
|
+
import { linearErrorContext } from "./diagnostics.js";
|
|
5
7
|
import { linearEndpoint, linearTrackerOptions } from "./options.js";
|
|
6
8
|
const LINEAR_REQUEST_TIMEOUT_MS = 30_000;
|
|
7
|
-
const
|
|
9
|
+
const LINEAR_CONNECTION_PAGE_SIZE = 50;
|
|
8
10
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
11
|
const issueFields = `
|
|
10
12
|
id
|
|
@@ -42,6 +44,7 @@ export class LinearClient {
|
|
|
42
44
|
settings;
|
|
43
45
|
fetchImpl;
|
|
44
46
|
logger;
|
|
47
|
+
paginationLimits;
|
|
45
48
|
constructor(settings, fetchImplOrDeps, retryOptions = {}) {
|
|
46
49
|
this.settings = settings;
|
|
47
50
|
this.retryOptions = {
|
|
@@ -58,6 +61,7 @@ export class LinearClient {
|
|
|
58
61
|
warn: (message) => console.warn(message),
|
|
59
62
|
error: (message) => console.error(message),
|
|
60
63
|
};
|
|
64
|
+
this.paginationLimits = deps.paginationLimits;
|
|
61
65
|
if (deps.graphqlClient) {
|
|
62
66
|
this.gqlClient = deps.graphqlClient;
|
|
63
67
|
}
|
|
@@ -106,9 +110,11 @@ export class LinearClient {
|
|
|
106
110
|
catch (error) {
|
|
107
111
|
if (!response.ok) {
|
|
108
112
|
this.logStatusError(query, response.status, errorBodyText ?? errorMessage(error));
|
|
109
|
-
|
|
113
|
+
// eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
|
|
114
|
+
throw new Error(`linear api status ${response.status}`);
|
|
110
115
|
}
|
|
111
|
-
|
|
116
|
+
// eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
|
|
117
|
+
throw new Error(`linear_invalid_json: ${redactDiagnosticText(errorMessage(error))}`);
|
|
112
118
|
}
|
|
113
119
|
if (response.status === 429) {
|
|
114
120
|
this.logStatusError(query, response.status, body);
|
|
@@ -116,7 +122,7 @@ export class LinearClient {
|
|
|
116
122
|
}
|
|
117
123
|
if (isRecord(body) && Array.isArray(body.errors) && body.errors.length > 0) {
|
|
118
124
|
this.logStatusError(query, response.status, body);
|
|
119
|
-
throw new Error(`linear_graphql_errors: ${JSON.stringify(body.errors)}`);
|
|
125
|
+
throw new Error(`linear_graphql_errors: ${redactDiagnosticText(JSON.stringify(body.errors))}`);
|
|
120
126
|
}
|
|
121
127
|
if (!response.ok) {
|
|
122
128
|
this.logStatusError(query, response.status, body);
|
|
@@ -144,7 +150,8 @@ export class LinearClient {
|
|
|
144
150
|
}
|
|
145
151
|
catch (error) {
|
|
146
152
|
this.logRequestError(query, error);
|
|
147
|
-
|
|
153
|
+
// eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
|
|
154
|
+
throw new Error(redactDiagnosticText(errorMessage(error)));
|
|
148
155
|
}
|
|
149
156
|
if (response.status !== 429 || retryCount >= this.retryOptions.maxRetries)
|
|
150
157
|
return response;
|
|
@@ -182,7 +189,7 @@ export class LinearClient {
|
|
|
182
189
|
const project = data.projects.nodes[0];
|
|
183
190
|
if (!project)
|
|
184
191
|
throw new Error(`linear project not found: ${projectSlug}`);
|
|
185
|
-
return parseProject(project);
|
|
192
|
+
return this.parseProject(project);
|
|
186
193
|
}
|
|
187
194
|
async fetchCandidateIssues() {
|
|
188
195
|
return this.fetchIssuesByStates(this.settings.tracker.activeStates);
|
|
@@ -195,7 +202,13 @@ export class LinearClient {
|
|
|
195
202
|
const issues = [];
|
|
196
203
|
const assignee = await this.assigneeFilterValue();
|
|
197
204
|
const projectSlugs = await this.resolveProjectSlugs();
|
|
205
|
+
const pagination = createTrackerPaginationGuard({
|
|
206
|
+
tracker: "linear",
|
|
207
|
+
resource: "issues",
|
|
208
|
+
limits: this.paginationLimits,
|
|
209
|
+
});
|
|
198
210
|
for (;;) {
|
|
211
|
+
pagination.recordPage();
|
|
199
212
|
const data = await this.graphql(`query LorenzTsPoll($projectSlugs: [String!]!, $stateNames: [String!]!, $first: Int!, $after: String) {
|
|
200
213
|
issues(filter: {project: {slugId: {in: $projectSlugs}}, state: {name: {in: $stateNames}}}, first: $first, after: $after) {
|
|
201
214
|
nodes { ${issueFields} }
|
|
@@ -207,18 +220,22 @@ export class LinearClient {
|
|
|
207
220
|
first: 50,
|
|
208
221
|
after,
|
|
209
222
|
});
|
|
210
|
-
|
|
223
|
+
pagination.recordItems(data.issues.nodes.length);
|
|
224
|
+
await this.appendNormalizedIssues(issues, data.issues.nodes, assignee);
|
|
211
225
|
if (!data.issues.pageInfo.hasNextPage)
|
|
212
226
|
return issues;
|
|
213
|
-
|
|
214
|
-
throw new Error("linear_missing_end_cursor");
|
|
215
|
-
after = data.issues.pageInfo.endCursor;
|
|
227
|
+
after = pagination.nextCursor(data.issues.pageInfo.endCursor, "endCursor");
|
|
216
228
|
}
|
|
217
229
|
}
|
|
218
230
|
async fetchIssuesByIds(ids) {
|
|
219
231
|
const uniqueIds = [...new Set(ids)];
|
|
220
232
|
if (uniqueIds.length === 0)
|
|
221
233
|
return [];
|
|
234
|
+
createTrackerPaginationGuard({
|
|
235
|
+
tracker: "linear",
|
|
236
|
+
resource: "issuesByIds",
|
|
237
|
+
limits: this.paginationLimits,
|
|
238
|
+
}).recordItems(uniqueIds.length);
|
|
222
239
|
const assignee = await this.assigneeFilterValue();
|
|
223
240
|
const issueOrder = new Map(uniqueIds.map((id, index) => [id, index]));
|
|
224
241
|
const issues = [];
|
|
@@ -230,7 +247,7 @@ export class LinearClient {
|
|
|
230
247
|
nodes { ${issueFields} }
|
|
231
248
|
}
|
|
232
249
|
}`, { ids: batchIds, first: batchIds.length });
|
|
233
|
-
appendNormalizedIssues(issues, data.issues.nodes, assignee);
|
|
250
|
+
await this.appendNormalizedIssues(issues, data.issues.nodes, assignee);
|
|
234
251
|
}
|
|
235
252
|
catch (error) {
|
|
236
253
|
// Identifier-shaped inputs ("MT-32" from workspace directory names) can make
|
|
@@ -255,7 +272,7 @@ export class LinearClient {
|
|
|
255
272
|
}
|
|
256
273
|
}`, { id });
|
|
257
274
|
if (data.issue)
|
|
258
|
-
appendNormalizedIssues(issues, [data.issue], assignee);
|
|
275
|
+
await this.appendNormalizedIssues(issues, [data.issue], assignee);
|
|
259
276
|
}
|
|
260
277
|
catch {
|
|
261
278
|
// Best-effort identifier resolution.
|
|
@@ -286,7 +303,10 @@ export class LinearClient {
|
|
|
286
303
|
});
|
|
287
304
|
if (!data.issueCreate.success || !data.issueCreate.issue)
|
|
288
305
|
throw new Error("linear issueCreate failed");
|
|
289
|
-
|
|
306
|
+
const normalized = await this.normalizeLinearIssue(data.issueCreate.issue, await this.assigneeFilterValue());
|
|
307
|
+
if (!normalized)
|
|
308
|
+
throw new Error("linear issueCreate returned malformed issue");
|
|
309
|
+
return normalized;
|
|
290
310
|
}
|
|
291
311
|
async updateIssueState(issueId, stateId) {
|
|
292
312
|
const data = await this.graphql(`mutation LorenzTsUpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
@@ -297,7 +317,10 @@ export class LinearClient {
|
|
|
297
317
|
}`, { id: issueId, input: { stateId } });
|
|
298
318
|
if (!data.issueUpdate.success || !data.issueUpdate.issue)
|
|
299
319
|
throw new Error("linear issueUpdate failed");
|
|
300
|
-
|
|
320
|
+
const normalized = await this.normalizeLinearIssue(data.issueUpdate.issue, await this.assigneeFilterValue());
|
|
321
|
+
if (!normalized)
|
|
322
|
+
throw new Error("linear issueUpdate returned malformed issue");
|
|
323
|
+
return normalized;
|
|
301
324
|
}
|
|
302
325
|
async archiveIssue(issueId) {
|
|
303
326
|
const data = await this.graphql(`mutation LorenzTsArchiveIssue($id: String!) {
|
|
@@ -335,24 +358,276 @@ export class LinearClient {
|
|
|
335
358
|
async resolveProjectSlugsByLabels(labels) {
|
|
336
359
|
let after = null;
|
|
337
360
|
const slugs = [];
|
|
361
|
+
const pagination = createTrackerPaginationGuard({
|
|
362
|
+
tracker: "linear",
|
|
363
|
+
resource: "projectsByLabels",
|
|
364
|
+
limits: this.paginationLimits,
|
|
365
|
+
});
|
|
338
366
|
for (;;) {
|
|
367
|
+
pagination.recordPage();
|
|
339
368
|
const data = await this.graphql(`query LorenzTsProjectsByLabels($labels: [String!]!, $first: Int!, $after: String) {
|
|
340
369
|
projects(filter: {labels: {name: {in: $labels}}}, first: $first, after: $after) {
|
|
341
370
|
nodes { slugId }
|
|
342
371
|
pageInfo { hasNextPage endCursor }
|
|
343
372
|
}
|
|
344
373
|
}`, { labels, first: 100, after });
|
|
374
|
+
pagination.recordItems(data.projects.nodes.length);
|
|
345
375
|
slugs.push(...data.projects.nodes.map((p) => p.slugId));
|
|
346
376
|
if (!data.projects.pageInfo?.hasNextPage)
|
|
347
377
|
break;
|
|
348
|
-
|
|
349
|
-
throw new Error("linear_missing_end_cursor: projectsByLabels");
|
|
350
|
-
after = data.projects.pageInfo.endCursor;
|
|
378
|
+
after = pagination.nextCursor(data.projects.pageInfo.endCursor, "endCursor");
|
|
351
379
|
}
|
|
352
380
|
if (slugs.length === 0)
|
|
353
381
|
throw new Error(`no linear projects found for labels: ${labels.join(", ")}`);
|
|
354
382
|
return slugs;
|
|
355
383
|
}
|
|
384
|
+
async appendNormalizedIssues(target, nodes, assignee) {
|
|
385
|
+
for (const issue of nodes) {
|
|
386
|
+
const normalized = await this.normalizeLinearIssue(issue, assignee);
|
|
387
|
+
if (normalized)
|
|
388
|
+
target.push(normalized);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async normalizeLinearIssue(issue, assignee) {
|
|
392
|
+
if (!isRecord(issue))
|
|
393
|
+
return null;
|
|
394
|
+
try {
|
|
395
|
+
return normalizeIssue(await this.linearIssuePayload(issue), assignee);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async linearIssuePayload(issue) {
|
|
402
|
+
const source = issueSource(issue);
|
|
403
|
+
const issueId = stringField(issue, "id");
|
|
404
|
+
const [labels, inverseRelations] = await Promise.all([
|
|
405
|
+
this.completePagedConnection({
|
|
406
|
+
source,
|
|
407
|
+
connection: "issue.labels",
|
|
408
|
+
initial: issue.labels,
|
|
409
|
+
fetchPage: async (after) => this.fetchIssueLabelsPage(issueId, after),
|
|
410
|
+
}),
|
|
411
|
+
this.completePagedConnection({
|
|
412
|
+
source,
|
|
413
|
+
connection: "issue.inverseRelations",
|
|
414
|
+
initial: issue.inverseRelations,
|
|
415
|
+
fetchPage: async (after) => this.fetchIssueInverseRelationsPage(issueId, after),
|
|
416
|
+
}),
|
|
417
|
+
]);
|
|
418
|
+
const degradedConnections = [
|
|
419
|
+
...labels.degradedConnections,
|
|
420
|
+
...inverseRelations.degradedConnections,
|
|
421
|
+
];
|
|
422
|
+
if (degradedConnections.length > 0)
|
|
423
|
+
this.logDegradedConnections(degradedConnections);
|
|
424
|
+
return linearIssuePayload({
|
|
425
|
+
...issue,
|
|
426
|
+
labels: connectionFromNodes(labels.nodes),
|
|
427
|
+
inverseRelations: connectionFromNodes(inverseRelations.nodes),
|
|
428
|
+
}, degradedConnections);
|
|
429
|
+
}
|
|
430
|
+
async fetchIssueLabelsPage(issueId, after) {
|
|
431
|
+
const data = await this.graphql(`query LorenzTsIssueLabels($id: String!, $first: Int!, $after: String) {
|
|
432
|
+
issue(id: $id) {
|
|
433
|
+
labels(first: $first, after: $after) {
|
|
434
|
+
nodes { name }
|
|
435
|
+
pageInfo { hasNextPage endCursor }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}`, { id: issueId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
|
|
439
|
+
if (!data.issue)
|
|
440
|
+
throw new Error(`linear issue not found: ${issueId}`);
|
|
441
|
+
return data.issue.labels;
|
|
442
|
+
}
|
|
443
|
+
async fetchIssueInverseRelationsPage(issueId, after) {
|
|
444
|
+
const data = await this.graphql(`query LorenzTsIssueInverseRelations($id: String!, $first: Int!, $after: String) {
|
|
445
|
+
issue(id: $id) {
|
|
446
|
+
inverseRelations(first: $first, after: $after) {
|
|
447
|
+
nodes {
|
|
448
|
+
type
|
|
449
|
+
issue {
|
|
450
|
+
id
|
|
451
|
+
identifier
|
|
452
|
+
state { name type }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
pageInfo { hasNextPage endCursor }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}`, { id: issueId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
|
|
459
|
+
if (!data.issue)
|
|
460
|
+
throw new Error(`linear issue not found: ${issueId}`);
|
|
461
|
+
return data.issue.inverseRelations;
|
|
462
|
+
}
|
|
463
|
+
async parseProject(project) {
|
|
464
|
+
const id = stringField(project, "id");
|
|
465
|
+
const name = stringField(project, "name");
|
|
466
|
+
const slugId = stringField(project, "slugId");
|
|
467
|
+
const teams = await this.completePagedConnection({
|
|
468
|
+
source: projectSource(project),
|
|
469
|
+
connection: "project.teams",
|
|
470
|
+
initial: project.teams,
|
|
471
|
+
fetchPage: async (after) => this.fetchProjectTeamsPage(id, after),
|
|
472
|
+
});
|
|
473
|
+
const parsedTeams = await Promise.all(teams.nodes.map(async (team) => this.parseTeam(asRecord(team))));
|
|
474
|
+
const degradedConnections = [
|
|
475
|
+
...teams.degradedConnections,
|
|
476
|
+
...parsedTeams.flatMap((team) => team.degradedConnections ?? []),
|
|
477
|
+
];
|
|
478
|
+
if (degradedConnections.length > 0)
|
|
479
|
+
this.logDegradedConnections(degradedConnections);
|
|
480
|
+
return {
|
|
481
|
+
id,
|
|
482
|
+
name,
|
|
483
|
+
slugId,
|
|
484
|
+
teams: parsedTeams,
|
|
485
|
+
...(degradedConnections.length > 0 ? { degradedConnections } : {}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async fetchProjectTeamsPage(projectId, after) {
|
|
489
|
+
const data = await this.graphql(`query LorenzTsProjectTeams($id: String!, $first: Int!, $after: String) {
|
|
490
|
+
project(id: $id) {
|
|
491
|
+
teams(first: $first, after: $after) {
|
|
492
|
+
nodes {
|
|
493
|
+
id
|
|
494
|
+
key
|
|
495
|
+
name
|
|
496
|
+
states(first: $first) {
|
|
497
|
+
nodes { id name type }
|
|
498
|
+
pageInfo { hasNextPage endCursor }
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
pageInfo { hasNextPage endCursor }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}`, { id: projectId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
|
|
505
|
+
if (!data.project)
|
|
506
|
+
throw new Error(`linear project not found: ${projectId}`);
|
|
507
|
+
return data.project.teams;
|
|
508
|
+
}
|
|
509
|
+
async parseTeam(team) {
|
|
510
|
+
const id = stringField(team, "id");
|
|
511
|
+
const key = stringField(team, "key");
|
|
512
|
+
const name = stringField(team, "name");
|
|
513
|
+
const states = await this.completePagedConnection({
|
|
514
|
+
source: teamSource(team),
|
|
515
|
+
connection: "project.team.states",
|
|
516
|
+
initial: team.states,
|
|
517
|
+
fetchPage: async (after) => this.fetchTeamStatesPage(id, after),
|
|
518
|
+
});
|
|
519
|
+
const degradedConnections = states.degradedConnections;
|
|
520
|
+
return {
|
|
521
|
+
id,
|
|
522
|
+
key,
|
|
523
|
+
name,
|
|
524
|
+
states: states.nodes.map((state) => {
|
|
525
|
+
const stateRecord = asRecord(state);
|
|
526
|
+
return {
|
|
527
|
+
id: stringField(stateRecord, "id"),
|
|
528
|
+
name: stringField(stateRecord, "name"),
|
|
529
|
+
type: normalizeStateType(stringField(stateRecord, "type")),
|
|
530
|
+
};
|
|
531
|
+
}),
|
|
532
|
+
...(degradedConnections.length > 0 ? { degradedConnections } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async fetchTeamStatesPage(teamId, after) {
|
|
536
|
+
const data = await this.graphql(`query LorenzTsTeamStates($id: String!, $first: Int!, $after: String) {
|
|
537
|
+
team(id: $id) {
|
|
538
|
+
states(first: $first, after: $after) {
|
|
539
|
+
nodes { id name type }
|
|
540
|
+
pageInfo { hasNextPage endCursor }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}`, { id: teamId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
|
|
544
|
+
if (!data.team)
|
|
545
|
+
throw new Error(`linear team not found: ${teamId}`);
|
|
546
|
+
return data.team.states;
|
|
547
|
+
}
|
|
548
|
+
async completePagedConnection(input) {
|
|
549
|
+
const pagination = createTrackerPaginationGuard({
|
|
550
|
+
tracker: "linear",
|
|
551
|
+
resource: input.connection,
|
|
552
|
+
limits: this.paginationLimits,
|
|
553
|
+
});
|
|
554
|
+
const initial = connectionSnapshot(input.initial);
|
|
555
|
+
const nodes = [...initial.nodes];
|
|
556
|
+
try {
|
|
557
|
+
pagination.recordPage();
|
|
558
|
+
pagination.recordItems(initial.nodes.length);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
return {
|
|
562
|
+
nodes: [],
|
|
563
|
+
degradedConnections: [degradedConnection(input, errorMessage(error))],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (!initial.pageInfo?.hasNextPage)
|
|
567
|
+
return { nodes, degradedConnections: [] };
|
|
568
|
+
let after;
|
|
569
|
+
try {
|
|
570
|
+
after = pagination.nextCursor(initial.pageInfo.endCursor, "endCursor");
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
return {
|
|
574
|
+
nodes,
|
|
575
|
+
degradedConnections: [
|
|
576
|
+
degradedConnection(input, errorMessage(error), initial.pageInfo.endCursor),
|
|
577
|
+
],
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
for (;;) {
|
|
581
|
+
let page;
|
|
582
|
+
try {
|
|
583
|
+
pagination.recordPage();
|
|
584
|
+
page = await input.fetchPage(after);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
return {
|
|
588
|
+
nodes,
|
|
589
|
+
degradedConnections: [degradedConnection(input, errorMessage(error), after)],
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const next = connectionSnapshot(page);
|
|
593
|
+
if (!next.isConnection) {
|
|
594
|
+
return {
|
|
595
|
+
nodes,
|
|
596
|
+
degradedConnections: [
|
|
597
|
+
degradedConnection(input, "linear_invalid_connection_payload", after),
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
pagination.recordItems(next.nodes.length);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
return {
|
|
606
|
+
nodes,
|
|
607
|
+
degradedConnections: [degradedConnection(input, errorMessage(error), after)],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
nodes.push(...next.nodes);
|
|
611
|
+
if (!next.pageInfo?.hasNextPage)
|
|
612
|
+
return { nodes, degradedConnections: [] };
|
|
613
|
+
try {
|
|
614
|
+
after = pagination.nextCursor(next.pageInfo.endCursor, "endCursor");
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
return {
|
|
618
|
+
nodes,
|
|
619
|
+
degradedConnections: [
|
|
620
|
+
degradedConnection(input, errorMessage(error), next.pageInfo.endCursor),
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
logDegradedConnections(degradedConnections) {
|
|
627
|
+
for (const degraded of degradedConnections) {
|
|
628
|
+
this.logger.warn(`linear tracker degraded connection source=${degraded.source} connection=${degraded.connection} reason=${degraded.reason}${degraded.cursor ? ` cursor=${degraded.cursor}` : ""}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
356
631
|
requiredProjectSlug() {
|
|
357
632
|
const { projectSlug } = linearTrackerOptions(this.settings);
|
|
358
633
|
if (!projectSlug)
|
|
@@ -388,80 +663,48 @@ export class LinearClient {
|
|
|
388
663
|
this.logger.error(`Linear GraphQL request failed status=${status}${linearErrorContext(query, body)}`);
|
|
389
664
|
}
|
|
390
665
|
logRequestError(query, error) {
|
|
391
|
-
this.logger.error(`Linear GraphQL request failed: ${errorMessage(error)}${linearErrorContext(query)}`);
|
|
666
|
+
this.logger.error(`Linear GraphQL request failed: ${redactDiagnosticText(errorMessage(error))}${linearErrorContext(query)}`);
|
|
392
667
|
}
|
|
393
668
|
}
|
|
394
669
|
function resolveDeps(fetchImplOrDeps) {
|
|
395
670
|
if (!fetchImplOrDeps)
|
|
396
|
-
return {
|
|
671
|
+
return {
|
|
672
|
+
fetchImpl: undefined,
|
|
673
|
+
graphqlClient: undefined,
|
|
674
|
+
logger: undefined,
|
|
675
|
+
paginationLimits: undefined,
|
|
676
|
+
};
|
|
397
677
|
if (typeof fetchImplOrDeps === "function")
|
|
398
|
-
return {
|
|
678
|
+
return {
|
|
679
|
+
fetchImpl: fetchImplOrDeps,
|
|
680
|
+
graphqlClient: undefined,
|
|
681
|
+
logger: undefined,
|
|
682
|
+
paginationLimits: undefined,
|
|
683
|
+
};
|
|
399
684
|
return {
|
|
400
685
|
fetchImpl: fetchImplOrDeps.fetchImpl ?? undefined,
|
|
401
686
|
graphqlClient: fetchImplOrDeps.graphqlClient ?? undefined,
|
|
402
687
|
logger: fetchImplOrDeps.logger ?? undefined,
|
|
688
|
+
paginationLimits: fetchImplOrDeps.paginationLimits ?? undefined,
|
|
403
689
|
};
|
|
404
690
|
}
|
|
405
|
-
function linearIssuePayload(issue) {
|
|
691
|
+
function linearIssuePayload(issue, degradedConnections = []) {
|
|
406
692
|
return {
|
|
407
693
|
...issue,
|
|
408
694
|
state: issue.state,
|
|
409
695
|
state_type: isRecord(issue.state) ? issue.state.type : null,
|
|
410
696
|
branch_name: issue.branchName,
|
|
411
697
|
assignee_id: isRecord(issue.assignee) ? issue.assignee.id : null,
|
|
412
|
-
labels:
|
|
413
|
-
relations:
|
|
698
|
+
labels: connectionSnapshot(issue.labels).nodes,
|
|
699
|
+
relations: connectionSnapshot(issue.inverseRelations).nodes,
|
|
414
700
|
created_at: issue.createdAt,
|
|
415
701
|
updated_at: issue.updatedAt,
|
|
702
|
+
...(degradedConnections.length > 0 ? { linear_degraded_connections: degradedConnections } : {}),
|
|
416
703
|
};
|
|
417
704
|
}
|
|
418
|
-
function appendNormalizedIssues(target, nodes, assignee) {
|
|
419
|
-
for (const issue of nodes) {
|
|
420
|
-
const normalized = normalizeLinearIssue(issue, assignee);
|
|
421
|
-
if (normalized)
|
|
422
|
-
target.push(normalized);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
function normalizeLinearIssue(issue, assignee) {
|
|
426
|
-
if (!isRecord(issue))
|
|
427
|
-
return null;
|
|
428
|
-
try {
|
|
429
|
-
return normalizeIssue(linearIssuePayload(issue), assignee);
|
|
430
|
-
}
|
|
431
|
-
catch (error) {
|
|
432
|
-
if (isLinearConnectionTruncatedError(error))
|
|
433
|
-
throw error;
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
705
|
function normalizeStateNames(stateNames) {
|
|
438
706
|
return [...new Set(stateNames.map((stateName) => String(stateName)))];
|
|
439
707
|
}
|
|
440
|
-
function parseProject(project) {
|
|
441
|
-
const teams = nodesFromConnection(project.teams, "project.teams");
|
|
442
|
-
return {
|
|
443
|
-
id: stringField(project, "id"),
|
|
444
|
-
name: stringField(project, "name"),
|
|
445
|
-
slugId: stringField(project, "slugId"),
|
|
446
|
-
teams: teams.map((team) => {
|
|
447
|
-
const teamRecord = asRecord(team);
|
|
448
|
-
const states = nodesFromConnection(teamRecord.states, "project.team.states");
|
|
449
|
-
return {
|
|
450
|
-
id: stringField(teamRecord, "id"),
|
|
451
|
-
key: stringField(teamRecord, "key"),
|
|
452
|
-
name: stringField(teamRecord, "name"),
|
|
453
|
-
states: states.map((state) => {
|
|
454
|
-
const stateRecord = asRecord(state);
|
|
455
|
-
return {
|
|
456
|
-
id: stringField(stateRecord, "id"),
|
|
457
|
-
name: stringField(stateRecord, "name"),
|
|
458
|
-
type: normalizeStateType(stringField(stateRecord, "type")),
|
|
459
|
-
};
|
|
460
|
-
}),
|
|
461
|
-
};
|
|
462
|
-
}),
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
708
|
function isRateLimitError(error) {
|
|
466
709
|
if (!error || typeof error !== "object")
|
|
467
710
|
return false;
|
|
@@ -484,35 +727,61 @@ function retryDelayFromError(error, options, retryCount) {
|
|
|
484
727
|
}
|
|
485
728
|
function reclassifyError(error) {
|
|
486
729
|
if (error instanceof Error) {
|
|
487
|
-
const msg = error.message;
|
|
730
|
+
const msg = redactDiagnosticText(error.message);
|
|
731
|
+
const cause = redactedLinearCause(error);
|
|
488
732
|
if (msg.includes("429"))
|
|
489
|
-
return new Error("linear api status 429", { cause
|
|
733
|
+
return new Error("linear api status 429", { cause });
|
|
490
734
|
if (msg.toLowerCase().includes("graphql")) {
|
|
491
|
-
return new Error(`linear_graphql_errors: ${msg}`, { cause
|
|
735
|
+
return new Error(`linear_graphql_errors: ${msg}`, { cause });
|
|
492
736
|
}
|
|
493
|
-
return
|
|
737
|
+
return new Error(msg, { cause });
|
|
494
738
|
}
|
|
495
|
-
return new Error(String(error));
|
|
739
|
+
return new Error(redactDiagnosticText(String(error)));
|
|
496
740
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
super(`linear_truncated_connection: ${connectionName}`);
|
|
500
|
-
this.name = "LinearConnectionTruncatedError";
|
|
501
|
-
}
|
|
741
|
+
function redactedLinearCause(error) {
|
|
742
|
+
return new Error(redactDiagnosticText(errorMessage(error)));
|
|
502
743
|
}
|
|
503
|
-
function
|
|
504
|
-
return
|
|
744
|
+
function isConnection(value) {
|
|
745
|
+
return isRecord(value) && Array.isArray(value.nodes);
|
|
505
746
|
}
|
|
506
|
-
function
|
|
747
|
+
function connectionSnapshot(value) {
|
|
507
748
|
if (!isConnection(value))
|
|
508
|
-
return [];
|
|
509
|
-
|
|
510
|
-
throw new LinearConnectionTruncatedError(connectionName);
|
|
511
|
-
}
|
|
512
|
-
return value.nodes;
|
|
749
|
+
return { nodes: [], pageInfo: null, isConnection: false };
|
|
750
|
+
return { nodes: value.nodes, pageInfo: connectionPageInfo(value.pageInfo), isConnection: true };
|
|
513
751
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
752
|
+
function connectionPageInfo(value) {
|
|
753
|
+
if (!isRecord(value) || typeof value.hasNextPage !== "boolean")
|
|
754
|
+
return null;
|
|
755
|
+
return {
|
|
756
|
+
hasNextPage: value.hasNextPage,
|
|
757
|
+
endCursor: typeof value.endCursor === "string" || value.endCursor === null ? value.endCursor : null,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function connectionFromNodes(nodes) {
|
|
761
|
+
return { nodes, pageInfo: { hasNextPage: false, endCursor: null } };
|
|
762
|
+
}
|
|
763
|
+
function degradedConnection(input, reason, cursor) {
|
|
764
|
+
return {
|
|
765
|
+
source: input.source,
|
|
766
|
+
connection: input.connection,
|
|
767
|
+
reason,
|
|
768
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function issueSource(issue) {
|
|
772
|
+
const identifier = typeof issue.identifier === "string" ? issue.identifier : "unknown";
|
|
773
|
+
const id = typeof issue.id === "string" ? issue.id : "unknown";
|
|
774
|
+
return `issue ${identifier} (${id})`;
|
|
775
|
+
}
|
|
776
|
+
function projectSource(project) {
|
|
777
|
+
const slug = typeof project.slugId === "string" ? project.slugId : "unknown";
|
|
778
|
+
const id = typeof project.id === "string" ? project.id : "unknown";
|
|
779
|
+
return `project ${slug} (${id})`;
|
|
780
|
+
}
|
|
781
|
+
function teamSource(team) {
|
|
782
|
+
const key = typeof team.key === "string" ? team.key : "unknown";
|
|
783
|
+
const id = typeof team.id === "string" ? team.id : "unknown";
|
|
784
|
+
return `team ${key} (${id})`;
|
|
516
785
|
}
|
|
517
786
|
function stringField(record, key) {
|
|
518
787
|
const value = record[key];
|
|
@@ -525,25 +794,6 @@ function asRecord(value) {
|
|
|
525
794
|
throw new Error("expected Linear object");
|
|
526
795
|
return value;
|
|
527
796
|
}
|
|
528
|
-
function linearErrorContext(query, body) {
|
|
529
|
-
const parts = [];
|
|
530
|
-
const operation = operationName(query);
|
|
531
|
-
if (operation)
|
|
532
|
-
parts.push(`operation=${operation}`);
|
|
533
|
-
if (body !== undefined)
|
|
534
|
-
parts.push(`body=${summarizeErrorBody(body)}`);
|
|
535
|
-
return parts.length === 0 ? "" : ` ${parts.join(" ")}`;
|
|
536
|
-
}
|
|
537
|
-
function operationName(query) {
|
|
538
|
-
return /\b(?:query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(query)?.[1] ?? null;
|
|
539
|
-
}
|
|
540
|
-
function summarizeErrorBody(body) {
|
|
541
|
-
const text = typeof body === "string" ? body : (JSON.stringify(body) ?? String(body));
|
|
542
|
-
const compact = text.replace(/\s+/g, " ").trim();
|
|
543
|
-
if (compact.length <= MAX_ERROR_BODY_LOG_BYTES)
|
|
544
|
-
return compact;
|
|
545
|
-
return `${compact.slice(0, MAX_ERROR_BODY_LOG_BYTES)}...<truncated>`;
|
|
546
|
-
}
|
|
547
797
|
function retryAfterHeaderValueFromError(error) {
|
|
548
798
|
if (!isRecord(error))
|
|
549
799
|
return null;
|