patchrelay 0.30.0 → 0.31.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.
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/IssueDetailView.js +1 -1
- package/dist/cli/watch/use-detail-stream.js +5 -0
- package/dist/cli/watch/watch-state.js +14 -0
- package/dist/db/migrations.js +9 -0
- package/dist/db.js +82 -14
- package/dist/github-failure-context.js +205 -0
- package/dist/github-webhook-handler.js +140 -22
- package/dist/issue-query-service.js +6 -0
- package/dist/linear-client.js +2 -0
- package/dist/run-orchestrator.js +155 -9
- package/dist/service.js +28 -3
- package/dist/webhook-handler.js +20 -14
- package/dist/webhooks.js +1 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -44,5 +44,5 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
|
|
|
44
44
|
const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
|
|
45
45
|
const graph = useMemo(() => buildPatchRelayStateGraph(history, issue.factoryState), [history, issue.factoryState]);
|
|
46
46
|
const queueObservations = useMemo(() => buildPatchRelayQueueObservations(issue, rawFeedEvents), [issue, rawFeedEvents]);
|
|
47
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.blockedByCount > 0 && _jsxs(Text, { color: "yellow", children: ["blocked by ", issue.blockedByKeys.join(", ")] }), issue.readyForExecution && !issue.activeRunType && issue.blockedByCount === 0 && _jsx(Text, { color: "blueBright", children: "ready" }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), detailTab === "timeline" && _jsx(Text, { dimColor: true, children: timelineMode }), follow && _jsx(Text, { color: "yellow", children: "follow" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), issue.title && _jsx(Text, { children: issue.title }), detailTab === "timeline" ? (_jsxs(_Fragment, { children: [plan && plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Plan" }), _jsx(Text, { children: progressBar(plan.filter((s) => s.status === "completed").length, plan.length, 16) }), _jsxs(Text, { dimColor: true, children: [plan.filter((s) => s.status === "completed").length, "/", plan.length] })] }), plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`)))] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow, mode: timelineMode }) })] })) : (_jsxs(_Fragment, { children: [_jsx(FactoryStateGraph, { main: graph.main, prLoops: graph.prLoops, queueLoop: graph.queueLoop, exits: graph.exits }), _jsx(QueueObservationView, { observations: queueObservations }), _jsx(Box, { marginTop: 1, children: _jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode }) })] }));
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.blockedByCount > 0 && _jsxs(Text, { color: "yellow", children: ["blocked by ", issue.blockedByKeys.join(", ")] }), issue.readyForExecution && !issue.activeRunType && issue.blockedByCount === 0 && _jsx(Text, { color: "blueBright", children: "ready" }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), detailTab === "timeline" && _jsx(Text, { dimColor: true, children: timelineMode }), follow && _jsx(Text, { color: "yellow", children: "follow" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), issue.title && _jsx(Text, { children: issue.title }), issueContext?.latestFailureSummary && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: issueContext.latestFailureSource === "queue_eviction" ? "yellow" : "red", children: ["Latest failure: ", issueContext.latestFailureSummary, issueContext.latestFailureHeadSha ? ` @ ${issueContext.latestFailureHeadSha.slice(0, 8)}` : ""] }) })), detailTab === "timeline" ? (_jsxs(_Fragment, { children: [plan && plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Plan" }), _jsx(Text, { children: progressBar(plan.filter((s) => s.status === "completed").length, plan.length, 16) }), _jsxs(Text, { dimColor: true, children: [plan.filter((s) => s.status === "completed").length, "/", plan.length] })] }), plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`)))] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow, mode: timelineMode }) })] })) : (_jsxs(_Fragment, { children: [_jsx(FactoryStateGraph, { main: graph.main, prLoops: graph.prLoops, queueLoop: graph.queueLoop, exits: graph.exits }), _jsx(QueueObservationView, { observations: queueObservations }), _jsx(Box, { marginTop: 1, children: _jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode }) })] }));
|
|
48
48
|
}
|
|
@@ -54,6 +54,11 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
|
54
54
|
ciRepairAttempts: typeof i.ciRepairAttempts === "number" ? i.ciRepairAttempts : 0,
|
|
55
55
|
queueRepairAttempts: typeof i.queueRepairAttempts === "number" ? i.queueRepairAttempts : 0,
|
|
56
56
|
reviewFixAttempts: typeof i.reviewFixAttempts === "number" ? i.reviewFixAttempts : 0,
|
|
57
|
+
latestFailureSource: typeof i.latestFailureSource === "string" ? i.latestFailureSource : undefined,
|
|
58
|
+
latestFailureHeadSha: typeof i.latestFailureHeadSha === "string" ? i.latestFailureHeadSha : undefined,
|
|
59
|
+
latestFailureCheckName: typeof i.latestFailureCheckName === "string" ? i.latestFailureCheckName : undefined,
|
|
60
|
+
latestFailureStepName: typeof i.latestFailureStepName === "string" ? i.latestFailureStepName : undefined,
|
|
61
|
+
latestFailureSummary: typeof i.latestFailureSummary === "string" ? i.latestFailureSummary : undefined,
|
|
57
62
|
runCount: runs.length,
|
|
58
63
|
};
|
|
59
64
|
}
|
|
@@ -171,6 +171,20 @@ function applyFeedEvent(state, event, receivedAt) {
|
|
|
171
171
|
if (event.status === "check_passed" || event.status === "check_failed") {
|
|
172
172
|
issue.prCheckStatus = event.status === "check_passed" ? "passed" : "failed";
|
|
173
173
|
}
|
|
174
|
+
if (event.status === "ci_repair_queued") {
|
|
175
|
+
issue.factoryState = "repairing_ci";
|
|
176
|
+
issue.statusNote = event.detail ?? event.summary;
|
|
177
|
+
}
|
|
178
|
+
if (event.status === "queue_repair_queued") {
|
|
179
|
+
issue.factoryState = "repairing_queue";
|
|
180
|
+
issue.statusNote = event.detail ?? event.summary;
|
|
181
|
+
}
|
|
182
|
+
if (event.status === "repair_deduped" || event.status === "branch_not_advanced") {
|
|
183
|
+
issue.statusNote = event.summary;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if ((event.kind === "turn" || event.kind === "github") && event.status === "branch_not_advanced") {
|
|
187
|
+
issue.statusNote = event.summary;
|
|
174
188
|
}
|
|
175
189
|
issue.updatedAt = event.at;
|
|
176
190
|
updated[index] = issue;
|
package/dist/db/migrations.js
CHANGED
|
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS issues (
|
|
|
7
7
|
title TEXT,
|
|
8
8
|
url TEXT,
|
|
9
9
|
current_linear_state TEXT,
|
|
10
|
+
current_linear_state_type TEXT,
|
|
10
11
|
factory_state TEXT NOT NULL DEFAULT 'delegated',
|
|
11
12
|
pending_run_type TEXT,
|
|
12
13
|
pending_run_context_json TEXT,
|
|
@@ -152,6 +153,7 @@ CREATE TABLE IF NOT EXISTS issue_dependencies (
|
|
|
152
153
|
blocker_issue_key TEXT,
|
|
153
154
|
blocker_title TEXT,
|
|
154
155
|
blocker_current_linear_state TEXT,
|
|
156
|
+
blocker_current_linear_state_type TEXT,
|
|
155
157
|
updated_at TEXT NOT NULL,
|
|
156
158
|
PRIMARY KEY (project_id, linear_issue_id, blocker_linear_issue_id)
|
|
157
159
|
);
|
|
@@ -188,17 +190,24 @@ export function runPatchRelayMigrations(connection) {
|
|
|
188
190
|
addColumnIfMissing(connection, "issues", "description", "TEXT");
|
|
189
191
|
addColumnIfMissing(connection, "issues", "priority", "INTEGER");
|
|
190
192
|
addColumnIfMissing(connection, "issues", "estimate", "REAL");
|
|
193
|
+
addColumnIfMissing(connection, "issues", "current_linear_state_type", "TEXT");
|
|
194
|
+
addColumnIfMissing(connection, "issue_dependencies", "blocker_current_linear_state_type", "TEXT");
|
|
191
195
|
// Zombie/stale recovery backoff
|
|
192
196
|
addColumnIfMissing(connection, "issues", "zombie_recovery_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
193
197
|
addColumnIfMissing(connection, "issues", "last_zombie_recovery_at", "TEXT");
|
|
194
198
|
// Preserve GitHub failure provenance so reconciliation can distinguish
|
|
195
199
|
// branch CI failures from merge-queue evictions after webhook delivery.
|
|
196
200
|
addColumnIfMissing(connection, "issues", "last_github_failure_source", "TEXT");
|
|
201
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_head_sha", "TEXT");
|
|
202
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_signature", "TEXT");
|
|
197
203
|
addColumnIfMissing(connection, "issues", "last_github_failure_check_name", "TEXT");
|
|
198
204
|
addColumnIfMissing(connection, "issues", "last_github_failure_check_url", "TEXT");
|
|
205
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_context_json", "TEXT");
|
|
199
206
|
addColumnIfMissing(connection, "issues", "last_github_failure_at", "TEXT");
|
|
200
207
|
addColumnIfMissing(connection, "issues", "last_queue_signal_at", "TEXT");
|
|
201
208
|
addColumnIfMissing(connection, "issues", "last_queue_incident_json", "TEXT");
|
|
209
|
+
addColumnIfMissing(connection, "issues", "last_attempted_failure_head_sha", "TEXT");
|
|
210
|
+
addColumnIfMissing(connection, "issues", "last_attempted_failure_signature", "TEXT");
|
|
202
211
|
}
|
|
203
212
|
function addColumnIfMissing(connection, table, column, definition) {
|
|
204
213
|
const cols = connection.prepare(`PRAGMA table_info(${table})`).all();
|
package/dist/db.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
1
2
|
import { LinearInstallationStore } from "./db/linear-installation-store.js";
|
|
2
3
|
import { OperatorFeedStore } from "./db/operator-feed-store.js";
|
|
3
4
|
import { RepositoryLinkStore } from "./db/repository-link-store.js";
|
|
@@ -104,6 +105,10 @@ export class PatchRelayDatabase {
|
|
|
104
105
|
sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
|
|
105
106
|
values.currentLinearState = params.currentLinearState;
|
|
106
107
|
}
|
|
108
|
+
if (params.currentLinearStateType !== undefined) {
|
|
109
|
+
sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
|
|
110
|
+
values.currentLinearStateType = params.currentLinearStateType;
|
|
111
|
+
}
|
|
107
112
|
if (params.factoryState !== undefined) {
|
|
108
113
|
sets.push("factory_state = @factoryState");
|
|
109
114
|
values.factoryState = params.factoryState;
|
|
@@ -160,6 +165,14 @@ export class PatchRelayDatabase {
|
|
|
160
165
|
sets.push("last_github_failure_source = @lastGitHubFailureSource");
|
|
161
166
|
values.lastGitHubFailureSource = params.lastGitHubFailureSource;
|
|
162
167
|
}
|
|
168
|
+
if (params.lastGitHubFailureHeadSha !== undefined) {
|
|
169
|
+
sets.push("last_github_failure_head_sha = @lastGitHubFailureHeadSha");
|
|
170
|
+
values.lastGitHubFailureHeadSha = params.lastGitHubFailureHeadSha;
|
|
171
|
+
}
|
|
172
|
+
if (params.lastGitHubFailureSignature !== undefined) {
|
|
173
|
+
sets.push("last_github_failure_signature = @lastGitHubFailureSignature");
|
|
174
|
+
values.lastGitHubFailureSignature = params.lastGitHubFailureSignature;
|
|
175
|
+
}
|
|
163
176
|
if (params.lastGitHubFailureCheckName !== undefined) {
|
|
164
177
|
sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
|
|
165
178
|
values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
|
|
@@ -168,6 +181,10 @@ export class PatchRelayDatabase {
|
|
|
168
181
|
sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
|
|
169
182
|
values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
|
|
170
183
|
}
|
|
184
|
+
if (params.lastGitHubFailureContextJson !== undefined) {
|
|
185
|
+
sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
|
|
186
|
+
values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
|
|
187
|
+
}
|
|
171
188
|
if (params.lastGitHubFailureAt !== undefined) {
|
|
172
189
|
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
173
190
|
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
@@ -180,6 +197,14 @@ export class PatchRelayDatabase {
|
|
|
180
197
|
sets.push("last_queue_incident_json = @lastQueueIncidentJson");
|
|
181
198
|
values.lastQueueIncidentJson = params.lastQueueIncidentJson;
|
|
182
199
|
}
|
|
200
|
+
if (params.lastAttemptedFailureHeadSha !== undefined) {
|
|
201
|
+
sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
|
|
202
|
+
values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
|
|
203
|
+
}
|
|
204
|
+
if (params.lastAttemptedFailureSignature !== undefined) {
|
|
205
|
+
sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
|
|
206
|
+
values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
|
|
207
|
+
}
|
|
183
208
|
if (params.ciRepairAttempts !== undefined) {
|
|
184
209
|
sets.push("ci_repair_attempts = @ciRepairAttempts");
|
|
185
210
|
values.ciRepairAttempts = params.ciRepairAttempts;
|
|
@@ -207,20 +232,22 @@ export class PatchRelayDatabase {
|
|
|
207
232
|
INSERT INTO issues (
|
|
208
233
|
project_id, linear_issue_id, issue_key, title, description, url,
|
|
209
234
|
priority, estimate,
|
|
210
|
-
current_linear_state, factory_state, pending_run_type, pending_run_context_json,
|
|
235
|
+
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
211
236
|
branch_name, worktree_path, thread_id, active_run_id,
|
|
212
237
|
agent_session_id,
|
|
213
238
|
pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
|
|
214
|
-
last_github_failure_source, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_at, last_queue_signal_at, last_queue_incident_json,
|
|
239
|
+
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at, last_queue_signal_at, last_queue_incident_json,
|
|
240
|
+
last_attempted_failure_head_sha, last_attempted_failure_signature,
|
|
215
241
|
updated_at
|
|
216
242
|
) VALUES (
|
|
217
243
|
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
218
244
|
@priority, @estimate,
|
|
219
|
-
@currentLinearState, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
245
|
+
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
220
246
|
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
221
247
|
@agentSessionId,
|
|
222
248
|
@prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
|
|
223
|
-
@lastGitHubFailureSource, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureAt, @lastQueueSignalAt, @lastQueueIncidentJson,
|
|
249
|
+
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt, @lastQueueSignalAt, @lastQueueIncidentJson,
|
|
250
|
+
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
|
|
224
251
|
@now
|
|
225
252
|
)
|
|
226
253
|
`).run({
|
|
@@ -233,6 +260,7 @@ export class PatchRelayDatabase {
|
|
|
233
260
|
priority: params.priority ?? null,
|
|
234
261
|
estimate: params.estimate ?? null,
|
|
235
262
|
currentLinearState: params.currentLinearState ?? null,
|
|
263
|
+
currentLinearStateType: params.currentLinearStateType ?? null,
|
|
236
264
|
factoryState: params.factoryState ?? "delegated",
|
|
237
265
|
pendingRunType: params.pendingRunType ?? null,
|
|
238
266
|
pendingRunContextJson: params.pendingRunContextJson ?? null,
|
|
@@ -247,11 +275,16 @@ export class PatchRelayDatabase {
|
|
|
247
275
|
prReviewState: params.prReviewState ?? null,
|
|
248
276
|
prCheckStatus: params.prCheckStatus ?? null,
|
|
249
277
|
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
278
|
+
lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
|
|
279
|
+
lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
|
|
250
280
|
lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
|
|
251
281
|
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
282
|
+
lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
|
|
252
283
|
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
253
284
|
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
254
285
|
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
286
|
+
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
287
|
+
lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
|
|
255
288
|
now,
|
|
256
289
|
});
|
|
257
290
|
}
|
|
@@ -295,11 +328,12 @@ export class PatchRelayDatabase {
|
|
|
295
328
|
blocker_issue_key,
|
|
296
329
|
blocker_title,
|
|
297
330
|
blocker_current_linear_state,
|
|
331
|
+
blocker_current_linear_state_type,
|
|
298
332
|
updated_at
|
|
299
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
333
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
300
334
|
`);
|
|
301
335
|
for (const blocker of params.blockers) {
|
|
302
|
-
insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, now);
|
|
336
|
+
insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, blocker.blockerCurrentLinearStateType ?? null, now);
|
|
303
337
|
}
|
|
304
338
|
}
|
|
305
339
|
listIssueDependencies(projectId, linearIssueId) {
|
|
@@ -311,6 +345,7 @@ export class PatchRelayDatabase {
|
|
|
311
345
|
COALESCE(blockers.issue_key, d.blocker_issue_key) AS blocker_issue_key,
|
|
312
346
|
COALESCE(blockers.title, d.blocker_title) AS blocker_title,
|
|
313
347
|
COALESCE(blockers.current_linear_state, d.blocker_current_linear_state) AS blocker_current_linear_state,
|
|
348
|
+
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type) AS blocker_current_linear_state_type,
|
|
314
349
|
d.updated_at
|
|
315
350
|
FROM issue_dependencies d
|
|
316
351
|
LEFT JOIN issues blockers
|
|
@@ -328,6 +363,9 @@ export class PatchRelayDatabase {
|
|
|
328
363
|
...(row.blocker_current_linear_state !== null && row.blocker_current_linear_state !== undefined
|
|
329
364
|
? { blockerCurrentLinearState: String(row.blocker_current_linear_state) }
|
|
330
365
|
: {}),
|
|
366
|
+
...(row.blocker_current_linear_state_type !== null && row.blocker_current_linear_state_type !== undefined
|
|
367
|
+
? { blockerCurrentLinearStateType: String(row.blocker_current_linear_state_type) }
|
|
368
|
+
: {}),
|
|
331
369
|
updatedAt: String(row.updated_at),
|
|
332
370
|
}));
|
|
333
371
|
}
|
|
@@ -351,7 +389,10 @@ export class PatchRelayDatabase {
|
|
|
351
389
|
ON blockers.project_id = d.project_id
|
|
352
390
|
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
353
391
|
WHERE d.project_id = ? AND d.linear_issue_id = ?
|
|
354
|
-
AND
|
|
392
|
+
AND (
|
|
393
|
+
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
394
|
+
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
395
|
+
)
|
|
355
396
|
`).get(projectId, linearIssueId);
|
|
356
397
|
return Number(row?.count ?? 0);
|
|
357
398
|
}
|
|
@@ -370,7 +411,10 @@ export class PatchRelayDatabase {
|
|
|
370
411
|
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
371
412
|
WHERE d.project_id = i.project_id
|
|
372
413
|
AND d.linear_issue_id = i.linear_issue_id
|
|
373
|
-
AND
|
|
414
|
+
AND (
|
|
415
|
+
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
416
|
+
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
417
|
+
)
|
|
374
418
|
)
|
|
375
419
|
`)
|
|
376
420
|
.all();
|
|
@@ -491,6 +535,8 @@ export class PatchRelayDatabase {
|
|
|
491
535
|
// ─── View builders ──────────────────────────────────────────────
|
|
492
536
|
issueToTrackedIssue(issue) {
|
|
493
537
|
const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
538
|
+
const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
|
|
539
|
+
const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
494
540
|
return {
|
|
495
541
|
id: issue.id,
|
|
496
542
|
projectId: issue.projectId,
|
|
@@ -500,11 +546,15 @@ export class PatchRelayDatabase {
|
|
|
500
546
|
...(issue.url ? { issueUrl: issue.url } : {}),
|
|
501
547
|
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
502
548
|
factoryState: issue.factoryState,
|
|
503
|
-
blockedByCount:
|
|
504
|
-
blockedByKeys:
|
|
505
|
-
.filter((entry) => !isDoneState(entry.blockerCurrentLinearState))
|
|
549
|
+
blockedByCount: unresolvedBlockedBy.length,
|
|
550
|
+
blockedByKeys: unresolvedBlockedBy
|
|
506
551
|
.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId),
|
|
507
|
-
readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined,
|
|
552
|
+
readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined && unresolvedBlockedBy.length === 0,
|
|
553
|
+
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
554
|
+
...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
555
|
+
...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
|
|
556
|
+
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
557
|
+
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
508
558
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
509
559
|
...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
|
|
510
560
|
updatedAt: issue.updatedAt,
|
|
@@ -544,6 +594,9 @@ function mapIssueRow(row) {
|
|
|
544
594
|
...(row.priority !== null && row.priority !== undefined ? { priority: Number(row.priority) } : {}),
|
|
545
595
|
...(row.estimate !== null && row.estimate !== undefined ? { estimate: Number(row.estimate) } : {}),
|
|
546
596
|
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
597
|
+
...(row.current_linear_state_type !== null && row.current_linear_state_type !== undefined
|
|
598
|
+
? { currentLinearStateType: String(row.current_linear_state_type) }
|
|
599
|
+
: {}),
|
|
547
600
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
548
601
|
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
549
602
|
...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
|
|
@@ -561,12 +614,21 @@ function mapIssueRow(row) {
|
|
|
561
614
|
...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
|
|
562
615
|
? { lastGitHubFailureSource: String(row.last_github_failure_source) }
|
|
563
616
|
: {}),
|
|
617
|
+
...(row.last_github_failure_head_sha !== null && row.last_github_failure_head_sha !== undefined
|
|
618
|
+
? { lastGitHubFailureHeadSha: String(row.last_github_failure_head_sha) }
|
|
619
|
+
: {}),
|
|
620
|
+
...(row.last_github_failure_signature !== null && row.last_github_failure_signature !== undefined
|
|
621
|
+
? { lastGitHubFailureSignature: String(row.last_github_failure_signature) }
|
|
622
|
+
: {}),
|
|
564
623
|
...(row.last_github_failure_check_name !== null && row.last_github_failure_check_name !== undefined
|
|
565
624
|
? { lastGitHubFailureCheckName: String(row.last_github_failure_check_name) }
|
|
566
625
|
: {}),
|
|
567
626
|
...(row.last_github_failure_check_url !== null && row.last_github_failure_check_url !== undefined
|
|
568
627
|
? { lastGitHubFailureCheckUrl: String(row.last_github_failure_check_url) }
|
|
569
628
|
: {}),
|
|
629
|
+
...(row.last_github_failure_context_json !== null && row.last_github_failure_context_json !== undefined
|
|
630
|
+
? { lastGitHubFailureContextJson: String(row.last_github_failure_context_json) }
|
|
631
|
+
: {}),
|
|
570
632
|
...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
|
|
571
633
|
? { lastGitHubFailureAt: String(row.last_github_failure_at) }
|
|
572
634
|
: {}),
|
|
@@ -576,6 +638,12 @@ function mapIssueRow(row) {
|
|
|
576
638
|
...(row.last_queue_incident_json !== null && row.last_queue_incident_json !== undefined
|
|
577
639
|
? { lastQueueIncidentJson: String(row.last_queue_incident_json) }
|
|
578
640
|
: {}),
|
|
641
|
+
...(row.last_attempted_failure_head_sha !== null && row.last_attempted_failure_head_sha !== undefined
|
|
642
|
+
? { lastAttemptedFailureHeadSha: String(row.last_attempted_failure_head_sha) }
|
|
643
|
+
: {}),
|
|
644
|
+
...(row.last_attempted_failure_signature !== null && row.last_attempted_failure_signature !== undefined
|
|
645
|
+
? { lastAttemptedFailureSignature: String(row.last_attempted_failure_signature) }
|
|
646
|
+
: {}),
|
|
579
647
|
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
580
648
|
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
581
649
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
@@ -602,6 +670,6 @@ function mapRunRow(row) {
|
|
|
602
670
|
...(row.ended_at !== null ? { endedAt: String(row.ended_at) } : {}),
|
|
603
671
|
};
|
|
604
672
|
}
|
|
605
|
-
function
|
|
606
|
-
return stateName?.trim().toLowerCase() === "done";
|
|
673
|
+
function isResolvedLinearState(stateType, stateName) {
|
|
674
|
+
return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
|
|
607
675
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execCommand, safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
2
|
+
const FAILED_CONCLUSIONS = new Set([
|
|
3
|
+
"failure",
|
|
4
|
+
"timed_out",
|
|
5
|
+
"cancelled",
|
|
6
|
+
"startup_failure",
|
|
7
|
+
"action_required",
|
|
8
|
+
"stale",
|
|
9
|
+
]);
|
|
10
|
+
export function createGitHubFailureContextResolver() {
|
|
11
|
+
return {
|
|
12
|
+
resolve: async ({ source, repoFullName, event }) => {
|
|
13
|
+
if (!repoFullName)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (source === "queue_eviction") {
|
|
16
|
+
const queueContext = buildFallbackFailureContext(source, repoFullName, event);
|
|
17
|
+
return {
|
|
18
|
+
...queueContext,
|
|
19
|
+
failureSignature: buildFailureSignature({
|
|
20
|
+
source,
|
|
21
|
+
headSha: queueContext.headSha,
|
|
22
|
+
checkName: queueContext.checkName,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const fallback = buildFallbackFailureContext(source, repoFullName, event);
|
|
27
|
+
try {
|
|
28
|
+
const failedCheck = await resolveFailedCheckRun(repoFullName, event);
|
|
29
|
+
const workflowRunId = parseWorkflowRunId(failedCheck?.detailsUrl ?? failedCheck?.htmlUrl ?? event.checkDetailsUrl ?? event.checkUrl);
|
|
30
|
+
const workflowJob = workflowRunId
|
|
31
|
+
? await resolveWorkflowJob(repoFullName, workflowRunId, failedCheck?.name ?? event.checkName)
|
|
32
|
+
: undefined;
|
|
33
|
+
const annotations = failedCheck?.id
|
|
34
|
+
? await resolveAnnotations(repoFullName, failedCheck.id)
|
|
35
|
+
: undefined;
|
|
36
|
+
const summary = firstNonEmpty(annotations?.[0], failedCheck?.outputTitle, failedCheck?.outputSummary, event.checkOutputTitle, event.checkOutputSummary, workflowJob?.stepName ? `Failed step: ${workflowJob.stepName}` : undefined);
|
|
37
|
+
const checkName = firstNonEmpty(failedCheck?.name, event.checkName);
|
|
38
|
+
const checkUrl = firstNonEmpty(failedCheck?.htmlUrl, event.checkUrl);
|
|
39
|
+
const checkDetailsUrl = firstNonEmpty(failedCheck?.detailsUrl, event.checkDetailsUrl);
|
|
40
|
+
const jobName = firstNonEmpty(workflowJob?.name, failedCheck?.name, event.checkName);
|
|
41
|
+
const stepName = workflowJob?.stepName;
|
|
42
|
+
return {
|
|
43
|
+
source,
|
|
44
|
+
repoFullName,
|
|
45
|
+
capturedAt: new Date().toISOString(),
|
|
46
|
+
...(event.headSha ? { headSha: event.headSha } : {}),
|
|
47
|
+
...(checkName ? { checkName } : {}),
|
|
48
|
+
...(checkUrl ? { checkUrl } : {}),
|
|
49
|
+
...(checkDetailsUrl ? { checkDetailsUrl } : {}),
|
|
50
|
+
...(workflowRunId !== undefined ? { workflowRunId } : {}),
|
|
51
|
+
...(jobName ? { jobName } : {}),
|
|
52
|
+
...(stepName ? { stepName } : {}),
|
|
53
|
+
...(summary ? { summary } : {}),
|
|
54
|
+
...(annotations && annotations.length > 0 ? { annotations } : {}),
|
|
55
|
+
failureSignature: buildFailureSignature({
|
|
56
|
+
source,
|
|
57
|
+
headSha: event.headSha,
|
|
58
|
+
checkName,
|
|
59
|
+
jobName,
|
|
60
|
+
stepName,
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return {
|
|
66
|
+
...fallback,
|
|
67
|
+
failureSignature: buildFailureSignature({
|
|
68
|
+
source,
|
|
69
|
+
headSha: fallback.headSha,
|
|
70
|
+
checkName: fallback.checkName,
|
|
71
|
+
stepName: fallback.stepName,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function parseGitHubFailureContext(value) {
|
|
79
|
+
if (!value)
|
|
80
|
+
return undefined;
|
|
81
|
+
return safeJsonParse(value);
|
|
82
|
+
}
|
|
83
|
+
export function summarizeGitHubFailureContext(context) {
|
|
84
|
+
if (!context)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (context.source === "queue_eviction") {
|
|
87
|
+
return firstNonEmpty(context.summary, context.checkName, "Queue eviction");
|
|
88
|
+
}
|
|
89
|
+
const lead = firstNonEmpty(context.jobName, context.checkName);
|
|
90
|
+
const step = context.stepName ? `${lead ?? "CI"} -> ${context.stepName}` : lead;
|
|
91
|
+
return firstNonEmpty(step && context.summary ? `${step}: ${context.summary}` : undefined, step, context.summary);
|
|
92
|
+
}
|
|
93
|
+
function buildFallbackFailureContext(source, repoFullName, event) {
|
|
94
|
+
const summary = firstNonEmpty(event.checkOutputTitle, event.checkOutputSummary, event.checkOutputText ? sanitizeDiagnosticText(event.checkOutputText, 240) : undefined);
|
|
95
|
+
return {
|
|
96
|
+
source,
|
|
97
|
+
repoFullName,
|
|
98
|
+
capturedAt: new Date().toISOString(),
|
|
99
|
+
...(event.headSha ? { headSha: event.headSha } : {}),
|
|
100
|
+
...(event.checkName ? { checkName: event.checkName } : {}),
|
|
101
|
+
...(event.checkUrl ? { checkUrl: event.checkUrl } : {}),
|
|
102
|
+
...(event.checkDetailsUrl ? { checkDetailsUrl: event.checkDetailsUrl } : {}),
|
|
103
|
+
...(event.checkName ? { jobName: event.checkName } : {}),
|
|
104
|
+
...(summary ? { summary } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function resolveFailedCheckRun(repoFullName, event) {
|
|
108
|
+
if (!event.headSha)
|
|
109
|
+
return undefined;
|
|
110
|
+
const response = await execCommand("gh", [
|
|
111
|
+
"api",
|
|
112
|
+
`repos/${repoFullName}/commits/${event.headSha}/check-runs`,
|
|
113
|
+
"--method", "GET",
|
|
114
|
+
], { timeoutMs: 15_000 });
|
|
115
|
+
if (response.exitCode !== 0) {
|
|
116
|
+
throw new Error(response.stderr || "gh api check-runs failed");
|
|
117
|
+
}
|
|
118
|
+
const payload = safeJsonParse(response.stdout);
|
|
119
|
+
const checks = (payload?.check_runs ?? [])
|
|
120
|
+
.map(mapCheckRunSummary)
|
|
121
|
+
.filter((entry) => entry.conclusion && FAILED_CONCLUSIONS.has(entry.conclusion.toLowerCase()));
|
|
122
|
+
return checks.find((entry) => entry.name === event.checkName)
|
|
123
|
+
?? checks.find((entry) => entry.name && event.checkName && entry.name.includes(event.checkName))
|
|
124
|
+
?? checks[0];
|
|
125
|
+
}
|
|
126
|
+
async function resolveWorkflowJob(repoFullName, workflowRunId, preferredName) {
|
|
127
|
+
const response = await execCommand("gh", [
|
|
128
|
+
"api",
|
|
129
|
+
`repos/${repoFullName}/actions/runs/${workflowRunId}/jobs`,
|
|
130
|
+
"--method", "GET",
|
|
131
|
+
], { timeoutMs: 15_000 });
|
|
132
|
+
if (response.exitCode !== 0) {
|
|
133
|
+
throw new Error(response.stderr || "gh api workflow jobs failed");
|
|
134
|
+
}
|
|
135
|
+
const payload = safeJsonParse(response.stdout);
|
|
136
|
+
const jobs = (payload?.jobs ?? []).map(mapWorkflowJobSummary);
|
|
137
|
+
return jobs.find((entry) => entry.name === preferredName)
|
|
138
|
+
?? jobs.find((entry) => entry.name && preferredName && entry.name.includes(preferredName))
|
|
139
|
+
?? jobs.find((entry) => entry.conclusion && FAILED_CONCLUSIONS.has(entry.conclusion.toLowerCase()))
|
|
140
|
+
?? jobs[0];
|
|
141
|
+
}
|
|
142
|
+
async function resolveAnnotations(repoFullName, checkRunId) {
|
|
143
|
+
const response = await execCommand("gh", [
|
|
144
|
+
"api",
|
|
145
|
+
`repos/${repoFullName}/check-runs/${checkRunId}/annotations`,
|
|
146
|
+
"--method", "GET",
|
|
147
|
+
"-F", "per_page=20",
|
|
148
|
+
], { timeoutMs: 15_000 });
|
|
149
|
+
if (response.exitCode !== 0) {
|
|
150
|
+
throw new Error(response.stderr || "gh api annotations failed");
|
|
151
|
+
}
|
|
152
|
+
const payload = safeJsonParse(response.stdout) ?? [];
|
|
153
|
+
return payload
|
|
154
|
+
.map((entry) => {
|
|
155
|
+
const title = typeof entry.title === "string" ? entry.title.trim() : "";
|
|
156
|
+
const message = typeof entry.message === "string" ? entry.message.trim() : "";
|
|
157
|
+
const path = typeof entry.path === "string" ? entry.path.trim() : "";
|
|
158
|
+
const rendered = [title, message, path ? `(${path})` : ""].filter(Boolean).join(": ");
|
|
159
|
+
return rendered ? sanitizeDiagnosticText(rendered, 240) : undefined;
|
|
160
|
+
})
|
|
161
|
+
.filter((entry) => Boolean(entry));
|
|
162
|
+
}
|
|
163
|
+
function mapCheckRunSummary(row) {
|
|
164
|
+
const output = row.output && typeof row.output === "object" ? row.output : undefined;
|
|
165
|
+
return {
|
|
166
|
+
...(typeof row.id === "number" ? { id: row.id } : {}),
|
|
167
|
+
...(typeof row.name === "string" ? { name: row.name } : {}),
|
|
168
|
+
...(typeof row.html_url === "string" ? { htmlUrl: row.html_url } : {}),
|
|
169
|
+
...(typeof row.details_url === "string" ? { detailsUrl: row.details_url } : {}),
|
|
170
|
+
...(typeof row.conclusion === "string" ? { conclusion: row.conclusion } : {}),
|
|
171
|
+
...(typeof output?.title === "string" ? { outputTitle: output.title } : {}),
|
|
172
|
+
...(typeof output?.summary === "string" ? { outputSummary: sanitizeDiagnosticText(output.summary, 240) } : {}),
|
|
173
|
+
...(typeof output?.text === "string" ? { outputText: sanitizeDiagnosticText(output.text, 240) } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function mapWorkflowJobSummary(row) {
|
|
177
|
+
const steps = Array.isArray(row.steps) ? row.steps.filter((entry) => Boolean(entry) && typeof entry === "object") : [];
|
|
178
|
+
const failedStep = steps.find((entry) => {
|
|
179
|
+
const conclusion = typeof entry.conclusion === "string" ? entry.conclusion.toLowerCase() : "";
|
|
180
|
+
return FAILED_CONCLUSIONS.has(conclusion);
|
|
181
|
+
});
|
|
182
|
+
const informativeStep = failedStep ?? steps.findLast((entry) => typeof entry.name === "string");
|
|
183
|
+
return {
|
|
184
|
+
...(typeof row.name === "string" ? { name: row.name } : {}),
|
|
185
|
+
...(typeof row.conclusion === "string" ? { conclusion: row.conclusion } : {}),
|
|
186
|
+
...(typeof informativeStep?.name === "string" ? { stepName: informativeStep.name } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function parseWorkflowRunId(url) {
|
|
190
|
+
if (!url)
|
|
191
|
+
return undefined;
|
|
192
|
+
const match = url.match(/\/actions\/runs\/(\d+)/);
|
|
193
|
+
return match ? Number(match[1]) : undefined;
|
|
194
|
+
}
|
|
195
|
+
function buildFailureSignature(parts) {
|
|
196
|
+
return [
|
|
197
|
+
parts.source,
|
|
198
|
+
parts.headSha ?? "unknown-sha",
|
|
199
|
+
parts.jobName ?? parts.checkName ?? "unknown-check",
|
|
200
|
+
parts.stepName ?? "unknown-step",
|
|
201
|
+
].join("::");
|
|
202
|
+
}
|
|
203
|
+
function firstNonEmpty(...values) {
|
|
204
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
|
205
|
+
}
|