patchrelay 0.30.1 → 0.32.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 +10 -0
- package/dist/db.js +106 -2
- package/dist/github-failure-context.js +298 -0
- package/dist/github-webhook-handler.js +318 -38
- package/dist/issue-query-service.js +6 -0
- package/dist/run-orchestrator.js +162 -10
- package/dist/service.js +21 -2
- 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
|
@@ -198,11 +198,21 @@ export function runPatchRelayMigrations(connection) {
|
|
|
198
198
|
// Preserve GitHub failure provenance so reconciliation can distinguish
|
|
199
199
|
// branch CI failures from merge-queue evictions after webhook delivery.
|
|
200
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");
|
|
201
203
|
addColumnIfMissing(connection, "issues", "last_github_failure_check_name", "TEXT");
|
|
202
204
|
addColumnIfMissing(connection, "issues", "last_github_failure_check_url", "TEXT");
|
|
205
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_context_json", "TEXT");
|
|
203
206
|
addColumnIfMissing(connection, "issues", "last_github_failure_at", "TEXT");
|
|
207
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_head_sha", "TEXT");
|
|
208
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_gate_check_name", "TEXT");
|
|
209
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_gate_check_status", "TEXT");
|
|
210
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_json", "TEXT");
|
|
211
|
+
addColumnIfMissing(connection, "issues", "last_github_ci_snapshot_settled_at", "TEXT");
|
|
204
212
|
addColumnIfMissing(connection, "issues", "last_queue_signal_at", "TEXT");
|
|
205
213
|
addColumnIfMissing(connection, "issues", "last_queue_incident_json", "TEXT");
|
|
214
|
+
addColumnIfMissing(connection, "issues", "last_attempted_failure_head_sha", "TEXT");
|
|
215
|
+
addColumnIfMissing(connection, "issues", "last_attempted_failure_signature", "TEXT");
|
|
206
216
|
}
|
|
207
217
|
function addColumnIfMissing(connection, table, column, definition) {
|
|
208
218
|
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";
|
|
@@ -164,6 +165,14 @@ export class PatchRelayDatabase {
|
|
|
164
165
|
sets.push("last_github_failure_source = @lastGitHubFailureSource");
|
|
165
166
|
values.lastGitHubFailureSource = params.lastGitHubFailureSource;
|
|
166
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
|
+
}
|
|
167
176
|
if (params.lastGitHubFailureCheckName !== undefined) {
|
|
168
177
|
sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
|
|
169
178
|
values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
|
|
@@ -172,10 +181,34 @@ export class PatchRelayDatabase {
|
|
|
172
181
|
sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
|
|
173
182
|
values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
|
|
174
183
|
}
|
|
184
|
+
if (params.lastGitHubFailureContextJson !== undefined) {
|
|
185
|
+
sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
|
|
186
|
+
values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
|
|
187
|
+
}
|
|
175
188
|
if (params.lastGitHubFailureAt !== undefined) {
|
|
176
189
|
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
177
190
|
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
178
191
|
}
|
|
192
|
+
if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
|
|
193
|
+
sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
|
|
194
|
+
values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
|
|
195
|
+
}
|
|
196
|
+
if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
|
|
197
|
+
sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
|
|
198
|
+
values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
|
|
199
|
+
}
|
|
200
|
+
if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
|
|
201
|
+
sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
|
|
202
|
+
values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
|
|
203
|
+
}
|
|
204
|
+
if (params.lastGitHubCiSnapshotJson !== undefined) {
|
|
205
|
+
sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
|
|
206
|
+
values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
|
|
207
|
+
}
|
|
208
|
+
if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
|
|
209
|
+
sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
|
|
210
|
+
values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
|
|
211
|
+
}
|
|
179
212
|
if (params.lastQueueSignalAt !== undefined) {
|
|
180
213
|
sets.push("last_queue_signal_at = @lastQueueSignalAt");
|
|
181
214
|
values.lastQueueSignalAt = params.lastQueueSignalAt;
|
|
@@ -184,6 +217,14 @@ export class PatchRelayDatabase {
|
|
|
184
217
|
sets.push("last_queue_incident_json = @lastQueueIncidentJson");
|
|
185
218
|
values.lastQueueIncidentJson = params.lastQueueIncidentJson;
|
|
186
219
|
}
|
|
220
|
+
if (params.lastAttemptedFailureHeadSha !== undefined) {
|
|
221
|
+
sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
|
|
222
|
+
values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
|
|
223
|
+
}
|
|
224
|
+
if (params.lastAttemptedFailureSignature !== undefined) {
|
|
225
|
+
sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
|
|
226
|
+
values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
|
|
227
|
+
}
|
|
187
228
|
if (params.ciRepairAttempts !== undefined) {
|
|
188
229
|
sets.push("ci_repair_attempts = @ciRepairAttempts");
|
|
189
230
|
values.ciRepairAttempts = params.ciRepairAttempts;
|
|
@@ -215,7 +256,10 @@ export class PatchRelayDatabase {
|
|
|
215
256
|
branch_name, worktree_path, thread_id, active_run_id,
|
|
216
257
|
agent_session_id,
|
|
217
258
|
pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
|
|
218
|
-
last_github_failure_source, last_github_failure_check_name, last_github_failure_check_url,
|
|
259
|
+
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,
|
|
260
|
+
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
261
|
+
last_queue_signal_at, last_queue_incident_json,
|
|
262
|
+
last_attempted_failure_head_sha, last_attempted_failure_signature,
|
|
219
263
|
updated_at
|
|
220
264
|
) VALUES (
|
|
221
265
|
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
@@ -224,7 +268,10 @@ export class PatchRelayDatabase {
|
|
|
224
268
|
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
225
269
|
@agentSessionId,
|
|
226
270
|
@prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
|
|
227
|
-
@lastGitHubFailureSource, @
|
|
271
|
+
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
272
|
+
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
273
|
+
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
274
|
+
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
|
|
228
275
|
@now
|
|
229
276
|
)
|
|
230
277
|
`).run({
|
|
@@ -252,11 +299,21 @@ export class PatchRelayDatabase {
|
|
|
252
299
|
prReviewState: params.prReviewState ?? null,
|
|
253
300
|
prCheckStatus: params.prCheckStatus ?? null,
|
|
254
301
|
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
302
|
+
lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
|
|
303
|
+
lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
|
|
255
304
|
lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
|
|
256
305
|
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
306
|
+
lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
|
|
257
307
|
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
308
|
+
lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
|
|
309
|
+
lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
|
|
310
|
+
lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
|
|
311
|
+
lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
|
|
312
|
+
lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
|
|
258
313
|
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
259
314
|
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
315
|
+
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
316
|
+
lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
|
|
260
317
|
now,
|
|
261
318
|
});
|
|
262
319
|
}
|
|
@@ -353,6 +410,17 @@ export class PatchRelayDatabase {
|
|
|
353
410
|
linearIssueId: String(row.linear_issue_id),
|
|
354
411
|
}));
|
|
355
412
|
}
|
|
413
|
+
getLatestGitHubCiSnapshot(projectId, linearIssueId) {
|
|
414
|
+
const issue = this.getIssue(projectId, linearIssueId);
|
|
415
|
+
if (!issue?.lastGitHubCiSnapshotJson)
|
|
416
|
+
return undefined;
|
|
417
|
+
try {
|
|
418
|
+
return JSON.parse(issue.lastGitHubCiSnapshotJson);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
356
424
|
countUnresolvedBlockers(projectId, linearIssueId) {
|
|
357
425
|
const row = this.connection.prepare(`
|
|
358
426
|
SELECT COUNT(*) AS count
|
|
@@ -508,6 +576,7 @@ export class PatchRelayDatabase {
|
|
|
508
576
|
issueToTrackedIssue(issue) {
|
|
509
577
|
const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
510
578
|
const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
|
|
579
|
+
const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
511
580
|
return {
|
|
512
581
|
id: issue.id,
|
|
513
582
|
projectId: issue.projectId,
|
|
@@ -521,6 +590,11 @@ export class PatchRelayDatabase {
|
|
|
521
590
|
blockedByKeys: unresolvedBlockedBy
|
|
522
591
|
.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId),
|
|
523
592
|
readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined && unresolvedBlockedBy.length === 0,
|
|
593
|
+
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
594
|
+
...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
595
|
+
...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
|
|
596
|
+
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
597
|
+
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
524
598
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
525
599
|
...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
|
|
526
600
|
updatedAt: issue.updatedAt,
|
|
@@ -580,21 +654,51 @@ function mapIssueRow(row) {
|
|
|
580
654
|
...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
|
|
581
655
|
? { lastGitHubFailureSource: String(row.last_github_failure_source) }
|
|
582
656
|
: {}),
|
|
657
|
+
...(row.last_github_failure_head_sha !== null && row.last_github_failure_head_sha !== undefined
|
|
658
|
+
? { lastGitHubFailureHeadSha: String(row.last_github_failure_head_sha) }
|
|
659
|
+
: {}),
|
|
660
|
+
...(row.last_github_failure_signature !== null && row.last_github_failure_signature !== undefined
|
|
661
|
+
? { lastGitHubFailureSignature: String(row.last_github_failure_signature) }
|
|
662
|
+
: {}),
|
|
583
663
|
...(row.last_github_failure_check_name !== null && row.last_github_failure_check_name !== undefined
|
|
584
664
|
? { lastGitHubFailureCheckName: String(row.last_github_failure_check_name) }
|
|
585
665
|
: {}),
|
|
586
666
|
...(row.last_github_failure_check_url !== null && row.last_github_failure_check_url !== undefined
|
|
587
667
|
? { lastGitHubFailureCheckUrl: String(row.last_github_failure_check_url) }
|
|
588
668
|
: {}),
|
|
669
|
+
...(row.last_github_failure_context_json !== null && row.last_github_failure_context_json !== undefined
|
|
670
|
+
? { lastGitHubFailureContextJson: String(row.last_github_failure_context_json) }
|
|
671
|
+
: {}),
|
|
589
672
|
...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
|
|
590
673
|
? { lastGitHubFailureAt: String(row.last_github_failure_at) }
|
|
591
674
|
: {}),
|
|
675
|
+
...(row.last_github_ci_snapshot_head_sha !== null && row.last_github_ci_snapshot_head_sha !== undefined
|
|
676
|
+
? { lastGitHubCiSnapshotHeadSha: String(row.last_github_ci_snapshot_head_sha) }
|
|
677
|
+
: {}),
|
|
678
|
+
...(row.last_github_ci_snapshot_gate_check_name !== null && row.last_github_ci_snapshot_gate_check_name !== undefined
|
|
679
|
+
? { lastGitHubCiSnapshotGateCheckName: String(row.last_github_ci_snapshot_gate_check_name) }
|
|
680
|
+
: {}),
|
|
681
|
+
...(row.last_github_ci_snapshot_gate_check_status !== null && row.last_github_ci_snapshot_gate_check_status !== undefined
|
|
682
|
+
? { lastGitHubCiSnapshotGateCheckStatus: String(row.last_github_ci_snapshot_gate_check_status) }
|
|
683
|
+
: {}),
|
|
684
|
+
...(row.last_github_ci_snapshot_json !== null && row.last_github_ci_snapshot_json !== undefined
|
|
685
|
+
? { lastGitHubCiSnapshotJson: String(row.last_github_ci_snapshot_json) }
|
|
686
|
+
: {}),
|
|
687
|
+
...(row.last_github_ci_snapshot_settled_at !== null && row.last_github_ci_snapshot_settled_at !== undefined
|
|
688
|
+
? { lastGitHubCiSnapshotSettledAt: String(row.last_github_ci_snapshot_settled_at) }
|
|
689
|
+
: {}),
|
|
592
690
|
...(row.last_queue_signal_at !== null && row.last_queue_signal_at !== undefined
|
|
593
691
|
? { lastQueueSignalAt: String(row.last_queue_signal_at) }
|
|
594
692
|
: {}),
|
|
595
693
|
...(row.last_queue_incident_json !== null && row.last_queue_incident_json !== undefined
|
|
596
694
|
? { lastQueueIncidentJson: String(row.last_queue_incident_json) }
|
|
597
695
|
: {}),
|
|
696
|
+
...(row.last_attempted_failure_head_sha !== null && row.last_attempted_failure_head_sha !== undefined
|
|
697
|
+
? { lastAttemptedFailureHeadSha: String(row.last_attempted_failure_head_sha) }
|
|
698
|
+
: {}),
|
|
699
|
+
...(row.last_attempted_failure_signature !== null && row.last_attempted_failure_signature !== undefined
|
|
700
|
+
? { lastAttemptedFailureSignature: String(row.last_attempted_failure_signature) }
|
|
701
|
+
: {}),
|
|
598
702
|
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
599
703
|
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
600
704
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
@@ -0,0 +1,298 @@
|
|
|
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 createGitHubCiSnapshotResolver() {
|
|
79
|
+
return {
|
|
80
|
+
resolve: async ({ repoFullName, event, gateCheckNames }) => {
|
|
81
|
+
if (!repoFullName || !event.headSha)
|
|
82
|
+
return undefined;
|
|
83
|
+
try {
|
|
84
|
+
const checks = await resolveCheckSnapshotChecks(repoFullName, event.headSha);
|
|
85
|
+
return buildCiSnapshotFromChecks(checks, event, gateCheckNames);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function parseGitHubFailureContext(value) {
|
|
94
|
+
if (!value)
|
|
95
|
+
return undefined;
|
|
96
|
+
return safeJsonParse(value);
|
|
97
|
+
}
|
|
98
|
+
export function summarizeGitHubFailureContext(context) {
|
|
99
|
+
if (!context)
|
|
100
|
+
return undefined;
|
|
101
|
+
if (context.source === "queue_eviction") {
|
|
102
|
+
return firstNonEmpty(context.summary, context.checkName, "Queue eviction");
|
|
103
|
+
}
|
|
104
|
+
const lead = firstNonEmpty(context.jobName, context.checkName);
|
|
105
|
+
const step = context.stepName ? `${lead ?? "CI"} -> ${context.stepName}` : lead;
|
|
106
|
+
return firstNonEmpty(step && context.summary ? `${step}: ${context.summary}` : undefined, step, context.summary);
|
|
107
|
+
}
|
|
108
|
+
function buildFallbackFailureContext(source, repoFullName, event) {
|
|
109
|
+
const summary = firstNonEmpty(event.checkOutputTitle, event.checkOutputSummary, event.checkOutputText ? sanitizeDiagnosticText(event.checkOutputText, 240) : undefined);
|
|
110
|
+
return {
|
|
111
|
+
source,
|
|
112
|
+
repoFullName,
|
|
113
|
+
capturedAt: new Date().toISOString(),
|
|
114
|
+
...(event.headSha ? { headSha: event.headSha } : {}),
|
|
115
|
+
...(event.checkName ? { checkName: event.checkName } : {}),
|
|
116
|
+
...(event.checkUrl ? { checkUrl: event.checkUrl } : {}),
|
|
117
|
+
...(event.checkDetailsUrl ? { checkDetailsUrl: event.checkDetailsUrl } : {}),
|
|
118
|
+
...(event.checkName ? { jobName: event.checkName } : {}),
|
|
119
|
+
...(summary ? { summary } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function resolveFailedCheckRun(repoFullName, event) {
|
|
123
|
+
if (!event.headSha)
|
|
124
|
+
return undefined;
|
|
125
|
+
const response = await execCommand("gh", [
|
|
126
|
+
"api",
|
|
127
|
+
`repos/${repoFullName}/commits/${event.headSha}/check-runs`,
|
|
128
|
+
"--method", "GET",
|
|
129
|
+
], { timeoutMs: 15_000 });
|
|
130
|
+
if (response.exitCode !== 0) {
|
|
131
|
+
throw new Error(response.stderr || "gh api check-runs failed");
|
|
132
|
+
}
|
|
133
|
+
const payload = safeJsonParse(response.stdout);
|
|
134
|
+
const checks = (payload?.check_runs ?? [])
|
|
135
|
+
.map(mapCheckRunSummary)
|
|
136
|
+
.filter((entry) => entry.conclusion && FAILED_CONCLUSIONS.has(entry.conclusion.toLowerCase()));
|
|
137
|
+
return checks.find((entry) => entry.name === event.checkName)
|
|
138
|
+
?? checks.find((entry) => entry.name && event.checkName && entry.name.includes(event.checkName))
|
|
139
|
+
?? checks[0];
|
|
140
|
+
}
|
|
141
|
+
async function resolveCheckSnapshotChecks(repoFullName, headSha) {
|
|
142
|
+
const response = await execCommand("gh", [
|
|
143
|
+
"api",
|
|
144
|
+
`repos/${repoFullName}/commits/${headSha}/check-runs`,
|
|
145
|
+
"--method", "GET",
|
|
146
|
+
], { timeoutMs: 15_000 });
|
|
147
|
+
if (response.exitCode !== 0) {
|
|
148
|
+
throw new Error(response.stderr || "gh api check-runs failed");
|
|
149
|
+
}
|
|
150
|
+
const payload = safeJsonParse(response.stdout);
|
|
151
|
+
return (payload?.check_runs ?? []).map(mapCiSnapshotCheck).filter((entry) => Boolean(entry));
|
|
152
|
+
}
|
|
153
|
+
async function resolveWorkflowJob(repoFullName, workflowRunId, preferredName) {
|
|
154
|
+
const response = await execCommand("gh", [
|
|
155
|
+
"api",
|
|
156
|
+
`repos/${repoFullName}/actions/runs/${workflowRunId}/jobs`,
|
|
157
|
+
"--method", "GET",
|
|
158
|
+
], { timeoutMs: 15_000 });
|
|
159
|
+
if (response.exitCode !== 0) {
|
|
160
|
+
throw new Error(response.stderr || "gh api workflow jobs failed");
|
|
161
|
+
}
|
|
162
|
+
const payload = safeJsonParse(response.stdout);
|
|
163
|
+
const jobs = (payload?.jobs ?? []).map(mapWorkflowJobSummary);
|
|
164
|
+
return jobs.find((entry) => entry.name === preferredName)
|
|
165
|
+
?? jobs.find((entry) => entry.name && preferredName && entry.name.includes(preferredName))
|
|
166
|
+
?? jobs.find((entry) => entry.conclusion && FAILED_CONCLUSIONS.has(entry.conclusion.toLowerCase()))
|
|
167
|
+
?? jobs[0];
|
|
168
|
+
}
|
|
169
|
+
async function resolveAnnotations(repoFullName, checkRunId) {
|
|
170
|
+
const response = await execCommand("gh", [
|
|
171
|
+
"api",
|
|
172
|
+
`repos/${repoFullName}/check-runs/${checkRunId}/annotations`,
|
|
173
|
+
"--method", "GET",
|
|
174
|
+
"-F", "per_page=20",
|
|
175
|
+
], { timeoutMs: 15_000 });
|
|
176
|
+
if (response.exitCode !== 0) {
|
|
177
|
+
throw new Error(response.stderr || "gh api annotations failed");
|
|
178
|
+
}
|
|
179
|
+
const payload = safeJsonParse(response.stdout) ?? [];
|
|
180
|
+
return payload
|
|
181
|
+
.map((entry) => {
|
|
182
|
+
const title = typeof entry.title === "string" ? entry.title.trim() : "";
|
|
183
|
+
const message = typeof entry.message === "string" ? entry.message.trim() : "";
|
|
184
|
+
const path = typeof entry.path === "string" ? entry.path.trim() : "";
|
|
185
|
+
const rendered = [title, message, path ? `(${path})` : ""].filter(Boolean).join(": ");
|
|
186
|
+
return rendered ? sanitizeDiagnosticText(rendered, 240) : undefined;
|
|
187
|
+
})
|
|
188
|
+
.filter((entry) => Boolean(entry));
|
|
189
|
+
}
|
|
190
|
+
function mapCheckRunSummary(row) {
|
|
191
|
+
const output = row.output && typeof row.output === "object" ? row.output : undefined;
|
|
192
|
+
return {
|
|
193
|
+
...(typeof row.id === "number" ? { id: row.id } : {}),
|
|
194
|
+
...(typeof row.name === "string" ? { name: row.name } : {}),
|
|
195
|
+
...(typeof row.html_url === "string" ? { htmlUrl: row.html_url } : {}),
|
|
196
|
+
...(typeof row.details_url === "string" ? { detailsUrl: row.details_url } : {}),
|
|
197
|
+
...(typeof row.conclusion === "string" ? { conclusion: row.conclusion } : {}),
|
|
198
|
+
...(typeof output?.title === "string" ? { outputTitle: output.title } : {}),
|
|
199
|
+
...(typeof output?.summary === "string" ? { outputSummary: sanitizeDiagnosticText(output.summary, 240) } : {}),
|
|
200
|
+
...(typeof output?.text === "string" ? { outputText: sanitizeDiagnosticText(output.text, 240) } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function mapCiSnapshotCheck(row) {
|
|
204
|
+
if (typeof row.name !== "string" || !row.name.trim())
|
|
205
|
+
return undefined;
|
|
206
|
+
const output = row.output && typeof row.output === "object" ? row.output : undefined;
|
|
207
|
+
const status = deriveCheckStatus({
|
|
208
|
+
apiStatus: typeof row.status === "string" ? row.status : undefined,
|
|
209
|
+
apiConclusion: typeof row.conclusion === "string" ? row.conclusion : undefined,
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
name: row.name.trim(),
|
|
213
|
+
status,
|
|
214
|
+
...(typeof row.conclusion === "string" && row.conclusion.trim() ? { conclusion: row.conclusion.trim().toLowerCase() } : {}),
|
|
215
|
+
...(typeof row.details_url === "string" && row.details_url.trim() ? { detailsUrl: row.details_url.trim() } : {}),
|
|
216
|
+
...(firstNonEmpty(typeof output?.title === "string" ? output.title : undefined, typeof output?.summary === "string" ? sanitizeDiagnosticText(output.summary, 240) : undefined) ? { summary: firstNonEmpty(typeof output?.title === "string" ? output.title : undefined, typeof output?.summary === "string" ? sanitizeDiagnosticText(output.summary, 240) : undefined) } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function mapWorkflowJobSummary(row) {
|
|
220
|
+
const steps = Array.isArray(row.steps) ? row.steps.filter((entry) => Boolean(entry) && typeof entry === "object") : [];
|
|
221
|
+
const failedStep = steps.find((entry) => {
|
|
222
|
+
const conclusion = typeof entry.conclusion === "string" ? entry.conclusion.toLowerCase() : "";
|
|
223
|
+
return FAILED_CONCLUSIONS.has(conclusion);
|
|
224
|
+
});
|
|
225
|
+
const informativeStep = failedStep ?? steps.findLast((entry) => typeof entry.name === "string");
|
|
226
|
+
return {
|
|
227
|
+
...(typeof row.name === "string" ? { name: row.name } : {}),
|
|
228
|
+
...(typeof row.conclusion === "string" ? { conclusion: row.conclusion } : {}),
|
|
229
|
+
...(typeof informativeStep?.name === "string" ? { stepName: informativeStep.name } : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function parseWorkflowRunId(url) {
|
|
233
|
+
if (!url)
|
|
234
|
+
return undefined;
|
|
235
|
+
const match = url.match(/\/actions\/runs\/(\d+)/);
|
|
236
|
+
return match ? Number(match[1]) : undefined;
|
|
237
|
+
}
|
|
238
|
+
function buildCiSnapshotFromChecks(checks, event, gateCheckNames) {
|
|
239
|
+
const gateCheck = findGateCheck(checks, gateCheckNames, event.checkName);
|
|
240
|
+
const gateCheckName = gateCheck?.name ?? pickGateCheckName(gateCheckNames, event.checkName) ?? event.checkName;
|
|
241
|
+
const gateCheckStatus = gateCheck?.status ?? deriveCheckStatus({
|
|
242
|
+
eventStatus: event.checkStatus,
|
|
243
|
+
eventConclusion: event.triggerEvent === "check_passed" ? "success" : "failure",
|
|
244
|
+
});
|
|
245
|
+
const failedChecks = checks.filter((entry) => entry.status === "failure");
|
|
246
|
+
return {
|
|
247
|
+
headSha: event.headSha,
|
|
248
|
+
...(gateCheckName ? { gateCheckName } : {}),
|
|
249
|
+
gateCheckStatus,
|
|
250
|
+
failedChecks,
|
|
251
|
+
checks,
|
|
252
|
+
...(gateCheckStatus !== "pending" ? { settledAt: new Date().toISOString() } : {}),
|
|
253
|
+
capturedAt: new Date().toISOString(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function findGateCheck(checks, gateCheckNames, fallbackCheckName) {
|
|
257
|
+
const exactNames = gateCheckNames.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
258
|
+
if (exactNames.length > 0) {
|
|
259
|
+
const exact = checks.find((entry) => exactNames.includes(entry.name.trim().toLowerCase()));
|
|
260
|
+
if (exact)
|
|
261
|
+
return exact;
|
|
262
|
+
}
|
|
263
|
+
if (!fallbackCheckName)
|
|
264
|
+
return undefined;
|
|
265
|
+
const fallback = fallbackCheckName.trim().toLowerCase();
|
|
266
|
+
return checks.find((entry) => entry.name.trim().toLowerCase() === fallback);
|
|
267
|
+
}
|
|
268
|
+
function pickGateCheckName(gateCheckNames, fallbackCheckName) {
|
|
269
|
+
return gateCheckNames.find((entry) => entry.trim().length > 0)?.trim()
|
|
270
|
+
?? fallbackCheckName?.trim();
|
|
271
|
+
}
|
|
272
|
+
function deriveCheckStatus(params) {
|
|
273
|
+
const status = params.apiStatus?.trim().toLowerCase();
|
|
274
|
+
if (status === "queued" || status === "in_progress" || status === "requested" || status === "waiting" || status === "pending") {
|
|
275
|
+
return "pending";
|
|
276
|
+
}
|
|
277
|
+
const conclusion = params.apiConclusion?.trim().toLowerCase()
|
|
278
|
+
?? params.eventConclusion?.trim().toLowerCase()
|
|
279
|
+
?? params.eventStatus?.trim().toLowerCase();
|
|
280
|
+
if (conclusion === "success" || conclusion === "neutral" || conclusion === "skipped") {
|
|
281
|
+
return "success";
|
|
282
|
+
}
|
|
283
|
+
if (conclusion && FAILED_CONCLUSIONS.has(conclusion)) {
|
|
284
|
+
return "failure";
|
|
285
|
+
}
|
|
286
|
+
return status === "completed" ? "failure" : "pending";
|
|
287
|
+
}
|
|
288
|
+
function buildFailureSignature(parts) {
|
|
289
|
+
return [
|
|
290
|
+
parts.source,
|
|
291
|
+
parts.headSha ?? "unknown-sha",
|
|
292
|
+
parts.jobName ?? parts.checkName ?? "unknown-check",
|
|
293
|
+
parts.stepName ?? "unknown-step",
|
|
294
|
+
].join("::");
|
|
295
|
+
}
|
|
296
|
+
function firstNonEmpty(...values) {
|
|
297
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
|
298
|
+
}
|