@straiffi/archon 1.7.1 → 1.7.3
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/client/assets/ActivityView-DTEenQas.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
- package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
- package/dist/client/assets/channel-Cym17waa.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
- package/dist/client/assets/index-CEMaJCPS.js +273 -0
- package/dist/client/assets/index-lWftq65U.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
- package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
- package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +471 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1267 -213
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +281 -76
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/agentRuns.js +205 -38
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/automationProgression.js +464 -0
- package/dist/server/lib/automationProgression.js.map +1 -0
- package/dist/server/lib/automationReconciliation.js +498 -0
- package/dist/server/lib/automationReconciliation.js.map +1 -0
- package/dist/server/lib/automationStageDetails.js +232 -0
- package/dist/server/lib/automationStageDetails.js.map +1 -0
- package/dist/server/lib/buildOpenQuestionOperations.js +223 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +52 -0
- package/dist/server/lib/buildOpenQuestions.js.map +1 -1
- package/dist/server/lib/buildSequences.js +130 -46
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +82 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +31 -0
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionReconciliation.js +518 -0
- package/dist/server/lib/executionReconciliation.js.map +1 -0
- package/dist/server/lib/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1264 -0
- package/dist/server/lib/executionRuns.js.map +1 -0
- package/dist/server/lib/git.js +385 -6
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/integrations/github.js +43 -0
- package/dist/server/lib/integrations/github.js.map +1 -1
- package/dist/server/lib/integrations/githubActions.js +1 -0
- package/dist/server/lib/integrations/githubActions.js.map +1 -1
- package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
- package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +7 -3
- package/dist/server/lib/projectMemorySelections.js.map +1 -1
- package/dist/server/lib/shutdown.js +2 -2
- package/dist/server/lib/shutdown.js.map +1 -1
- package/dist/server/lib/specArtifact.js +3 -0
- package/dist/server/lib/specArtifact.js.map +1 -1
- package/dist/server/lib/specFormatting.js +1 -1
- package/dist/server/lib/specFormatting.js.map +1 -1
- package/dist/server/lib/specOperations.js +31 -0
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +44 -14
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewDraft.js +3 -2
- package/dist/server/lib/specReviewDraft.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +44 -7
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/specReviewValidation.js +217 -0
- package/dist/server/lib/specReviewValidation.js.map +1 -0
- package/dist/server/lib/specReviews.js +30 -2
- package/dist/server/lib/specReviews.js.map +1 -1
- package/dist/server/lib/specs.js +18 -1
- package/dist/server/lib/specs.js.map +1 -1
- package/dist/server/lib/stageSnapshots.js +197 -0
- package/dist/server/lib/stageSnapshots.js.map +1 -0
- package/dist/server/lib/testSessions.js +164 -19
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomation.js +80 -4
- package/dist/server/lib/ticketAutomation.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +466 -91
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +214 -20
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketOpenQuestions.js +30 -14
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +75 -7
- package/dist/server/lib/ticketRunOperations.js.map +1 -1
- package/dist/server/lib/ticketUndo.js +77 -168
- package/dist/server/lib/ticketUndo.js.map +1 -1
- package/dist/server/lib/ticketWorkflowOperations.js +132 -28
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +46 -29
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/transcriptRuns.js +3 -3
- package/dist/server/lib/transcriptRuns.js.map +1 -1
- package/dist/server/lib/turnExecutionErrors.js +12 -0
- package/dist/server/lib/turnExecutionErrors.js.map +1 -0
- package/dist/server/lib/types.js +57 -1
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +596 -184
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +261 -130
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +720 -195
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +527 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +231 -69
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +796 -239
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +211 -45
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +425 -252
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
- package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
- package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
- package/dist/client/assets/channel-B4kCvLCt.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
- package/dist/client/assets/index-9q1GXkUw.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.js +0 -1
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import db from '../db.js';
|
|
3
|
+
import { EXECUTION_RUN_KINDS, EXECUTION_RUN_LINK_ENTITY_TYPES } from './types.js';
|
|
4
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
|
|
5
|
+
const ACTIVE_STATUSES = new Set(['queued', 'running', 'waiting']);
|
|
6
|
+
const MAX_METADATA_BYTES = 16 * 1024;
|
|
7
|
+
const MAX_EVENT_MESSAGE_LENGTH = 4_000;
|
|
8
|
+
const MAX_FAILURE_MESSAGE_LENGTH = 4_000;
|
|
9
|
+
const MAX_JOBS_SNAPSHOT_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_TEST_OUTPUT_LENGTH = 256 * 1024;
|
|
11
|
+
const APPROVED_EXECUTION_RUN_KINDS = new Set(EXECUTION_RUN_KINDS);
|
|
12
|
+
const EXECUTION_RUN_INITIATORS = new Set(['user', 'automation', 'recovery', 'system']);
|
|
13
|
+
const EXECUTION_RUN_LINK_RELATIONSHIPS = new Set(['primary', 'context', 'result', 'trigger']);
|
|
14
|
+
const EXECUTION_RUN_LINK_ENTITY_TYPE_SET = new Set(EXECUTION_RUN_LINK_ENTITY_TYPES);
|
|
15
|
+
const GENERIC_RETRY_DENIED_KINDS = new Set([
|
|
16
|
+
'automation_commit',
|
|
17
|
+
'automation_push',
|
|
18
|
+
'automation_pull_request',
|
|
19
|
+
]);
|
|
20
|
+
export class GitHubActionsDispatchConflictError extends Error {
|
|
21
|
+
executionRunId;
|
|
22
|
+
constructor(executionRunId) {
|
|
23
|
+
super('A dispatch for this project, workflow, and ref is still being resolved.');
|
|
24
|
+
this.name = 'GitHubActionsDispatchConflictError';
|
|
25
|
+
this.executionRunId = executionRunId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const getRunStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ? AND project_id = ?');
|
|
29
|
+
const getRunByIdStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ?');
|
|
30
|
+
const projectExistsStatement = db.prepare('SELECT 1 FROM projects WHERE id = ?');
|
|
31
|
+
const insertRunStatement = db.prepare(`
|
|
32
|
+
INSERT INTO execution_runs (
|
|
33
|
+
id, project_id, kind, status, domain_status, source_kind, source_id, source_label, source_url,
|
|
34
|
+
correlation_id, initiated_by, metadata_schema_version, metadata_json, parent_run_id,
|
|
35
|
+
retry_of_run_id, attempt, created_at, updated_at
|
|
36
|
+
) VALUES (?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
37
|
+
`);
|
|
38
|
+
const insertEventStatement = db.prepare(`
|
|
39
|
+
INSERT INTO lifecycle_events (
|
|
40
|
+
id, project_id, execution_run_id, correlation_id, sequence, event_type, from_status,
|
|
41
|
+
to_status, domain_status, message, metadata_schema_version, metadata_json, created_at
|
|
42
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
43
|
+
`);
|
|
44
|
+
const nextEventSequenceStatement = db.prepare(`
|
|
45
|
+
SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
|
|
46
|
+
FROM lifecycle_events
|
|
47
|
+
WHERE execution_run_id = ?
|
|
48
|
+
`);
|
|
49
|
+
const insertRunLinkStatement = db.prepare(`
|
|
50
|
+
INSERT INTO execution_run_links (
|
|
51
|
+
execution_run_id, project_id, entity_type, entity_id, relationship,
|
|
52
|
+
label_snapshot, url_snapshot, created_at
|
|
53
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
`);
|
|
55
|
+
const getRunLinksStatement = db.prepare(`
|
|
56
|
+
SELECT * FROM execution_run_links
|
|
57
|
+
WHERE execution_run_id = ? AND project_id = ?
|
|
58
|
+
ORDER BY created_at ASC, entity_type ASC, entity_id ASC
|
|
59
|
+
`);
|
|
60
|
+
const getRunLinkStatement = db.prepare(`
|
|
61
|
+
SELECT * FROM execution_run_links
|
|
62
|
+
WHERE execution_run_id = ? AND project_id = ? AND entity_type = ? AND entity_id = ? AND relationship = ?
|
|
63
|
+
`);
|
|
64
|
+
const getDirectRetryChildStatement = db.prepare('SELECT id FROM execution_runs WHERE retry_of_run_id = ? LIMIT 1');
|
|
65
|
+
const getEventStatement = db.prepare('SELECT * FROM lifecycle_events WHERE id = ? AND project_id = ?');
|
|
66
|
+
const insertTestDetailsStatement = db.prepare(`
|
|
67
|
+
INSERT INTO test_execution_details (
|
|
68
|
+
execution_run_id, command_id, command_name, command_runner, command_snapshot,
|
|
69
|
+
active_command, target_kind, target_bundle_id, target_label, ticket_id,
|
|
70
|
+
selection_json, summary_json, exit_code, duration_ms, output, output_truncated,
|
|
71
|
+
output_retention_state, output_expired_at, output_byte_count
|
|
72
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
|
|
73
|
+
`);
|
|
74
|
+
const insertGitHubActionsDetailsStatement = db.prepare(`
|
|
75
|
+
INSERT INTO github_actions_run_details (
|
|
76
|
+
execution_run_id, workflow_id, workflow_name, ref, input_names_json, input_fingerprint,
|
|
77
|
+
resolution_attempts, provider_error_count, provider_run_id, provider_status,
|
|
78
|
+
conclusion, html_url, head_sha, head_branch, provider_created_at, provider_updated_at,
|
|
79
|
+
provider_started_at, dispatched_at, last_synced_at, jobs_json,
|
|
80
|
+
jobs_retention_state, jobs_expired_at, jobs_count
|
|
81
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
|
|
82
|
+
`);
|
|
83
|
+
const getTestTargetBundleStatement = db.prepare(`
|
|
84
|
+
SELECT id, kind
|
|
85
|
+
FROM worktree_bundles
|
|
86
|
+
WHERE id = ? AND project_id = ?
|
|
87
|
+
`);
|
|
88
|
+
const getTestTargetTicketStatement = db.prepare(`
|
|
89
|
+
SELECT id, worktree_bundle_id
|
|
90
|
+
FROM tickets
|
|
91
|
+
WHERE id = ? AND project_id = ?
|
|
92
|
+
`);
|
|
93
|
+
const updateRunStatement = db.prepare(`
|
|
94
|
+
UPDATE execution_runs
|
|
95
|
+
SET status = ?, domain_status = ?, failure_code = ?, failure_message = ?,
|
|
96
|
+
failure_details_json = ?, started_at = ?, completed_at = ?, updated_at = ?
|
|
97
|
+
WHERE id = ? AND project_id = ?
|
|
98
|
+
`);
|
|
99
|
+
const assertBoundedString = (value, name, maxLength) => {
|
|
100
|
+
if (value !== null && value !== undefined && value.length > maxLength) {
|
|
101
|
+
throw new Error(`${name} exceeds ${maxLength} characters`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const normalizeBoundedMessage = (value, maxLength) => {
|
|
105
|
+
if (value === null || value === undefined) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (value.length <= maxLength) {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
const truncated = value.slice(0, maxLength);
|
|
112
|
+
const lastCodeUnit = truncated.charCodeAt(truncated.length - 1);
|
|
113
|
+
return lastCodeUnit >= 0xD800 && lastCodeUnit <= 0xDBFF
|
|
114
|
+
? truncated.slice(0, -1)
|
|
115
|
+
: truncated;
|
|
116
|
+
};
|
|
117
|
+
const serializeBoundedObject = (value, name) => {
|
|
118
|
+
if (value === null || value === undefined) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
let serialized;
|
|
122
|
+
try {
|
|
123
|
+
serialized = JSON.stringify(value);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new Error(`${name} must be JSON serializable`);
|
|
127
|
+
}
|
|
128
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_METADATA_BYTES) {
|
|
129
|
+
throw new Error(`${name} exceeds ${MAX_METADATA_BYTES} bytes`);
|
|
130
|
+
}
|
|
131
|
+
return serialized;
|
|
132
|
+
};
|
|
133
|
+
const parseStoredObject = (value) => {
|
|
134
|
+
if (!value) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(value);
|
|
139
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
const parseStoredArray = (value) => {
|
|
149
|
+
if (!value) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(value);
|
|
154
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const serializeJobsSnapshot = (jobs) => {
|
|
161
|
+
if (jobs === null || jobs === undefined) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const bounded = jobs.slice(0, 50).map(job => ({
|
|
165
|
+
...job,
|
|
166
|
+
steps: job.steps.slice(0, 50),
|
|
167
|
+
}));
|
|
168
|
+
while (bounded.length > 0) {
|
|
169
|
+
const serialized = JSON.stringify(bounded);
|
|
170
|
+
if (Buffer.byteLength(serialized, 'utf8') <= MAX_JOBS_SNAPSHOT_BYTES) {
|
|
171
|
+
return serialized;
|
|
172
|
+
}
|
|
173
|
+
bounded.pop();
|
|
174
|
+
}
|
|
175
|
+
return '[]';
|
|
176
|
+
};
|
|
177
|
+
const boundTestOutput = (output, alreadyTruncated = false) => {
|
|
178
|
+
if (output.length <= MAX_TEST_OUTPUT_LENGTH) {
|
|
179
|
+
return { output, outputTruncated: alreadyTruncated };
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
output: output.slice(-MAX_TEST_OUTPUT_LENGTH),
|
|
183
|
+
outputTruncated: true,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
const isSourceAvailable = (row) => {
|
|
187
|
+
if (!row.source_kind || !row.source_id) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (row.source_kind === 'ticket') {
|
|
191
|
+
return Boolean(db.prepare('SELECT 1 FROM tickets WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
|
|
192
|
+
}
|
|
193
|
+
if (row.source_kind === 'project') {
|
|
194
|
+
return Boolean(projectExistsStatement.get(row.source_id));
|
|
195
|
+
}
|
|
196
|
+
if (row.source_kind === 'bundle') {
|
|
197
|
+
return Boolean(db.prepare('SELECT 1 FROM worktree_bundles WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
|
|
198
|
+
}
|
|
199
|
+
if (row.source_kind === 'chat_session') {
|
|
200
|
+
return Boolean(db.prepare('SELECT 1 FROM chat_sessions WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
|
|
201
|
+
}
|
|
202
|
+
if (row.source_kind === 'spec_session') {
|
|
203
|
+
return Boolean(db.prepare('SELECT 1 FROM spec_sessions WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
|
|
204
|
+
}
|
|
205
|
+
return Boolean(safeSnapshotUrl(row.source_url));
|
|
206
|
+
};
|
|
207
|
+
const safeSnapshotUrl = (value) => {
|
|
208
|
+
if (!value) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const url = new URL(value);
|
|
213
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? value : null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const serializeRun = (row) => ({
|
|
220
|
+
id: row.id,
|
|
221
|
+
project_id: row.project_id,
|
|
222
|
+
kind: row.kind,
|
|
223
|
+
status: row.status,
|
|
224
|
+
domain_status: row.domain_status,
|
|
225
|
+
source_reference: row.source_kind && row.source_id
|
|
226
|
+
? {
|
|
227
|
+
kind: row.source_kind,
|
|
228
|
+
id: row.source_id,
|
|
229
|
+
label: row.source_label,
|
|
230
|
+
available: isSourceAvailable(row),
|
|
231
|
+
url: safeSnapshotUrl(row.source_url),
|
|
232
|
+
}
|
|
233
|
+
: null,
|
|
234
|
+
correlation_id: row.correlation_id,
|
|
235
|
+
initiated_by: row.initiated_by,
|
|
236
|
+
metadata_schema_version: row.metadata_schema_version,
|
|
237
|
+
metadata: parseStoredObject(row.metadata_json),
|
|
238
|
+
parent_run_id: row.parent_run_id,
|
|
239
|
+
retry_of_run_id: row.retry_of_run_id,
|
|
240
|
+
attempt: row.attempt,
|
|
241
|
+
failure: row.failure_message
|
|
242
|
+
? {
|
|
243
|
+
code: row.failure_code,
|
|
244
|
+
message: row.failure_message,
|
|
245
|
+
details: parseStoredObject(row.failure_details_json),
|
|
246
|
+
}
|
|
247
|
+
: null,
|
|
248
|
+
created_at: row.created_at,
|
|
249
|
+
started_at: row.started_at,
|
|
250
|
+
completed_at: row.completed_at,
|
|
251
|
+
updated_at: row.updated_at,
|
|
252
|
+
});
|
|
253
|
+
const serializeEvent = (row) => ({
|
|
254
|
+
...row,
|
|
255
|
+
metadata: parseStoredObject(row.metadata_json),
|
|
256
|
+
});
|
|
257
|
+
const PUBLIC_EVENT_METADATA_KEYS = new Set([
|
|
258
|
+
'candidate_run_ids',
|
|
259
|
+
'conclusion',
|
|
260
|
+
'input_names',
|
|
261
|
+
'provider_run_id',
|
|
262
|
+
'ref',
|
|
263
|
+
'resolution_attempts',
|
|
264
|
+
'trigger',
|
|
265
|
+
'workflow_id',
|
|
266
|
+
]);
|
|
267
|
+
export const toCanonicalLifecycleEvent = (event) => {
|
|
268
|
+
const metadata = event.metadata
|
|
269
|
+
? Object.fromEntries(Object.entries(event.metadata).filter(([key]) => PUBLIC_EVENT_METADATA_KEYS.has(key)))
|
|
270
|
+
: null;
|
|
271
|
+
return {
|
|
272
|
+
...event,
|
|
273
|
+
metadata: metadata && Object.keys(metadata).length > 0 ? metadata : null,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const encodeCursor = (cursor) => Buffer.from(JSON.stringify({
|
|
277
|
+
version: 1,
|
|
278
|
+
created_at: cursor.created_at,
|
|
279
|
+
id: cursor.id,
|
|
280
|
+
}), 'utf8').toString('base64url');
|
|
281
|
+
const decodeCursor = (value) => {
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
284
|
+
if (parsed.version !== 1
|
|
285
|
+
|| typeof parsed.created_at !== 'string'
|
|
286
|
+
|| parsed.created_at.length === 0
|
|
287
|
+
|| parsed.created_at.length > 64
|
|
288
|
+
|| typeof parsed.id !== 'string'
|
|
289
|
+
|| parsed.id.length === 0
|
|
290
|
+
|| parsed.id.length > 128) {
|
|
291
|
+
throw new Error('Invalid execution run cursor');
|
|
292
|
+
}
|
|
293
|
+
return { created_at: parsed.created_at, id: parsed.id };
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (error instanceof Error && error.message === 'Invalid execution run cursor') {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
throw new Error('Invalid execution run cursor');
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const encodeLifecycleEventCursor = (cursor) => Buffer.from(JSON.stringify({
|
|
303
|
+
version: 1,
|
|
304
|
+
created_at: cursor.created_at,
|
|
305
|
+
execution_run_id: cursor.execution_run_id,
|
|
306
|
+
sequence: cursor.sequence,
|
|
307
|
+
id: cursor.id,
|
|
308
|
+
}), 'utf8').toString('base64url');
|
|
309
|
+
const decodeLifecycleEventCursor = (value) => {
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
312
|
+
if (parsed.version !== 1
|
|
313
|
+
|| typeof parsed.created_at !== 'string'
|
|
314
|
+
|| parsed.created_at.length === 0
|
|
315
|
+
|| parsed.created_at.length > 64
|
|
316
|
+
|| typeof parsed.execution_run_id !== 'string'
|
|
317
|
+
|| parsed.execution_run_id.length === 0
|
|
318
|
+
|| parsed.execution_run_id.length > 128
|
|
319
|
+
|| !Number.isSafeInteger(parsed.sequence)
|
|
320
|
+
|| parsed.sequence < 1
|
|
321
|
+
|| typeof parsed.id !== 'string'
|
|
322
|
+
|| parsed.id.length === 0
|
|
323
|
+
|| parsed.id.length > 128) {
|
|
324
|
+
throw new Error('Invalid lifecycle event cursor');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
created_at: parsed.created_at,
|
|
328
|
+
execution_run_id: parsed.execution_run_id,
|
|
329
|
+
sequence: parsed.sequence,
|
|
330
|
+
id: parsed.id,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error instanceof Error && error.message === 'Invalid lifecycle event cursor') {
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
throw new Error('Invalid lifecycle event cursor');
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const resolveLinkLabel = (projectId, link) => {
|
|
341
|
+
if (link.label) {
|
|
342
|
+
return link.label;
|
|
343
|
+
}
|
|
344
|
+
if (link.entityType === 'ticket') {
|
|
345
|
+
const row = db.prepare('SELECT title FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
|
|
346
|
+
return row?.title ?? null;
|
|
347
|
+
}
|
|
348
|
+
if (link.entityType === 'project') {
|
|
349
|
+
const row = db.prepare('SELECT name FROM projects WHERE id = ?').get(link.entityId);
|
|
350
|
+
return row?.name ?? null;
|
|
351
|
+
}
|
|
352
|
+
if (link.entityType === 'bundle') {
|
|
353
|
+
const row = db.prepare('SELECT name FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
|
|
354
|
+
return row?.name ?? null;
|
|
355
|
+
}
|
|
356
|
+
if (link.entityType === 'chat_session') {
|
|
357
|
+
const row = db.prepare('SELECT title FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
|
|
358
|
+
return row?.title ?? null;
|
|
359
|
+
}
|
|
360
|
+
if (link.entityType === 'spec_session') {
|
|
361
|
+
const row = db.prepare('SELECT title FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
|
|
362
|
+
return row?.title ?? null;
|
|
363
|
+
}
|
|
364
|
+
if (link.entityType === 'bundle_pull_request') {
|
|
365
|
+
const row = db.prepare(`
|
|
366
|
+
SELECT pr_number FROM bundle_pull_requests
|
|
367
|
+
WHERE id = ? AND project_id = ?
|
|
368
|
+
`).get(link.entityId, projectId);
|
|
369
|
+
return row ? `Pull request #${row.pr_number}` : null;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
};
|
|
373
|
+
const resolveLinkOwner = (projectId, link) => {
|
|
374
|
+
switch (link.entityType) {
|
|
375
|
+
case 'project':
|
|
376
|
+
return link.entityId === projectId && projectExistsStatement.get(link.entityId)
|
|
377
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
378
|
+
: null;
|
|
379
|
+
case 'ticket':
|
|
380
|
+
return db.prepare('SELECT 1 FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
|
|
381
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
382
|
+
: null;
|
|
383
|
+
case 'bundle':
|
|
384
|
+
return db.prepare('SELECT 1 FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
|
|
385
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
386
|
+
: null;
|
|
387
|
+
case 'chat_session':
|
|
388
|
+
return db.prepare('SELECT 1 FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
|
|
389
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
390
|
+
: null;
|
|
391
|
+
case 'spec_session':
|
|
392
|
+
return db.prepare('SELECT 1 FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
|
|
393
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
394
|
+
: null;
|
|
395
|
+
case 'spec_snapshot': {
|
|
396
|
+
const row = db.prepare(`
|
|
397
|
+
SELECT snapshot.spec_session_id AS parent_id FROM spec_snapshots snapshot
|
|
398
|
+
JOIN spec_sessions session ON session.id = snapshot.spec_session_id
|
|
399
|
+
WHERE snapshot.id = ? AND session.project_id = ?
|
|
400
|
+
`).get(link.entityId, projectId);
|
|
401
|
+
return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
|
|
402
|
+
}
|
|
403
|
+
case 'review_run': {
|
|
404
|
+
const row = db.prepare(`
|
|
405
|
+
SELECT review.ticket_id AS parent_id FROM review_runs review
|
|
406
|
+
JOIN tickets ticket ON ticket.id = review.ticket_id
|
|
407
|
+
WHERE review.id = ? AND ticket.project_id = ?
|
|
408
|
+
`).get(link.entityId, projectId);
|
|
409
|
+
return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
|
|
410
|
+
}
|
|
411
|
+
case 'spec_review_run': {
|
|
412
|
+
const row = db.prepare(`
|
|
413
|
+
SELECT review.spec_session_id AS parent_id FROM spec_review_runs review
|
|
414
|
+
JOIN spec_sessions session ON session.id = review.spec_session_id
|
|
415
|
+
JOIN spec_snapshots snapshot
|
|
416
|
+
ON snapshot.id = review.spec_snapshot_id AND snapshot.spec_session_id = review.spec_session_id
|
|
417
|
+
WHERE review.id = ? AND review.project_id = ? AND session.project_id = ?
|
|
418
|
+
`).get(link.entityId, projectId, projectId);
|
|
419
|
+
return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
|
|
420
|
+
}
|
|
421
|
+
case 'build_sequence':
|
|
422
|
+
return db.prepare('SELECT 1 FROM build_sequences WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
|
|
423
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
424
|
+
: null;
|
|
425
|
+
case 'github_workflow':
|
|
426
|
+
return projectExistsStatement.get(projectId)
|
|
427
|
+
? { parentEntityType: null, parentEntityId: null }
|
|
428
|
+
: null;
|
|
429
|
+
case 'ticket_message': {
|
|
430
|
+
const row = db.prepare(`
|
|
431
|
+
SELECT message.ticket_id AS parent_id
|
|
432
|
+
FROM ticket_messages message
|
|
433
|
+
JOIN tickets ticket ON ticket.id = message.ticket_id
|
|
434
|
+
WHERE message.id = ? AND ticket.project_id = ?
|
|
435
|
+
`).get(link.entityId, projectId);
|
|
436
|
+
return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
|
|
437
|
+
}
|
|
438
|
+
case 'chat_message': {
|
|
439
|
+
const row = db.prepare(`
|
|
440
|
+
SELECT message.chat_session_id AS parent_id
|
|
441
|
+
FROM chat_messages message
|
|
442
|
+
JOIN chat_sessions session ON session.id = message.chat_session_id
|
|
443
|
+
WHERE message.id = ? AND session.project_id = ?
|
|
444
|
+
`).get(link.entityId, projectId);
|
|
445
|
+
return row ? { parentEntityType: 'chat_session', parentEntityId: row.parent_id } : null;
|
|
446
|
+
}
|
|
447
|
+
case 'spec_message': {
|
|
448
|
+
const row = db.prepare(`
|
|
449
|
+
SELECT message.spec_session_id AS parent_id
|
|
450
|
+
FROM spec_messages message
|
|
451
|
+
JOIN spec_sessions session ON session.id = message.spec_session_id
|
|
452
|
+
WHERE message.id = ? AND session.project_id = ?
|
|
453
|
+
`).get(link.entityId, projectId);
|
|
454
|
+
return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
|
|
455
|
+
}
|
|
456
|
+
case 'bundle_pull_request': {
|
|
457
|
+
const row = db.prepare(`
|
|
458
|
+
SELECT pull_request.bundle_id AS parent_id
|
|
459
|
+
FROM bundle_pull_requests pull_request
|
|
460
|
+
JOIN worktree_bundles bundle
|
|
461
|
+
ON bundle.id = pull_request.bundle_id AND bundle.project_id = pull_request.project_id
|
|
462
|
+
WHERE pull_request.id = ? AND pull_request.project_id = ?
|
|
463
|
+
`).get(link.entityId, projectId);
|
|
464
|
+
return row ? { parentEntityType: 'bundle', parentEntityId: row.parent_id } : null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const assertLinkOwnership = (projectId, link) => {
|
|
469
|
+
if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
|
|
470
|
+
throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
|
|
471
|
+
}
|
|
472
|
+
const owner = resolveLinkOwner(projectId, link);
|
|
473
|
+
if (!owner) {
|
|
474
|
+
throw new Error(`${link.entityType} ${link.entityId} does not belong to project ${projectId}`);
|
|
475
|
+
}
|
|
476
|
+
if (link.expectedParent && (owner.parentEntityType !== link.expectedParent.entityType
|
|
477
|
+
|| owner.parentEntityId !== link.expectedParent.entityId)) {
|
|
478
|
+
throw new Error(`${link.entityType} ${link.entityId} does not belong to expected ${link.expectedParent.entityType} ${link.expectedParent.entityId}`);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const validateExecutionRunLink = (projectId, link) => {
|
|
482
|
+
if (!EXECUTION_RUN_LINK_RELATIONSHIPS.has(link.relationship)) {
|
|
483
|
+
throw new Error(`Unsupported execution run link relationship: ${link.relationship}`);
|
|
484
|
+
}
|
|
485
|
+
if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
|
|
486
|
+
throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
|
|
487
|
+
}
|
|
488
|
+
if (link.entityType.trim().length === 0 || link.entityId.trim().length === 0) {
|
|
489
|
+
throw new Error('Execution run links require an entity type and ID');
|
|
490
|
+
}
|
|
491
|
+
assertBoundedString(link.entityType, 'Link entity type', 100);
|
|
492
|
+
assertBoundedString(link.entityId, 'Link entity ID', 500);
|
|
493
|
+
assertBoundedString(link.url, 'Link URL', 2_000);
|
|
494
|
+
assertLinkOwnership(projectId, link);
|
|
495
|
+
const label = resolveLinkLabel(projectId, link);
|
|
496
|
+
assertBoundedString(label, 'Link label', 500);
|
|
497
|
+
return { ...link, label };
|
|
498
|
+
};
|
|
499
|
+
const normalizeInitialLinks = (input) => {
|
|
500
|
+
const links = [...(input.links ?? [])];
|
|
501
|
+
if (input.sourceReference) {
|
|
502
|
+
links.push({
|
|
503
|
+
entityType: input.sourceReference.kind,
|
|
504
|
+
entityId: input.sourceReference.id,
|
|
505
|
+
relationship: 'primary',
|
|
506
|
+
label: input.sourceReference.label,
|
|
507
|
+
url: input.sourceReference.url,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (links.filter(link => link.relationship === 'primary').length > 1) {
|
|
511
|
+
throw new Error('Execution runs may have only one primary link');
|
|
512
|
+
}
|
|
513
|
+
return links;
|
|
514
|
+
};
|
|
515
|
+
const resolveCorrelationId = (parent) => {
|
|
516
|
+
if (parent) {
|
|
517
|
+
return parent.correlation_id;
|
|
518
|
+
}
|
|
519
|
+
return randomUUID();
|
|
520
|
+
};
|
|
521
|
+
const assertRetryEligible = (kind, retrySource, allowAutomationSideEffect) => {
|
|
522
|
+
if (!TERMINAL_STATUSES.has(retrySource.status)) {
|
|
523
|
+
throw new Error('Retry source execution run must be terminal');
|
|
524
|
+
}
|
|
525
|
+
if (kind === 'github_actions') {
|
|
526
|
+
throw new Error('GitHub Actions execution runs cannot be retried directly');
|
|
527
|
+
}
|
|
528
|
+
if (GENERIC_RETRY_DENIED_KINDS.has(kind) && !allowAutomationSideEffect) {
|
|
529
|
+
throw new Error(`${kind} execution runs cannot be retried through the generic retry API`);
|
|
530
|
+
}
|
|
531
|
+
if (kind !== 'test' && retrySource.status === 'completed') {
|
|
532
|
+
throw new Error('Completed execution runs are not eligible for retry');
|
|
533
|
+
}
|
|
534
|
+
if (hasDirectRetryChild(retrySource.id)) {
|
|
535
|
+
throw new Error(`Execution run ${retrySource.id} already has a direct retry child`);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const assertTestTarget = (projectId, details) => {
|
|
539
|
+
let expectedTargetKind = 'repo_root';
|
|
540
|
+
let expectedBundleId = null;
|
|
541
|
+
if (details.ticketId) {
|
|
542
|
+
const ticket = getTestTargetTicketStatement.get(details.ticketId, projectId);
|
|
543
|
+
if (!ticket) {
|
|
544
|
+
throw new Error(`Ticket ${details.ticketId} does not belong to project ${projectId}`);
|
|
545
|
+
}
|
|
546
|
+
if (ticket.worktree_bundle_id) {
|
|
547
|
+
const bundle = getTestTargetBundleStatement.get(ticket.worktree_bundle_id, projectId);
|
|
548
|
+
if (!bundle) {
|
|
549
|
+
throw new Error(`Ticket ${details.ticketId} bundle target does not belong to project ${projectId}`);
|
|
550
|
+
}
|
|
551
|
+
if (bundle.kind !== 'project_root') {
|
|
552
|
+
expectedTargetKind = 'bundle';
|
|
553
|
+
expectedBundleId = bundle.id;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else if (details.targetKind === 'bundle') {
|
|
558
|
+
const bundle = getTestTargetBundleStatement.get(details.targetBundleId, projectId);
|
|
559
|
+
if (!bundle || bundle.kind === 'project_root') {
|
|
560
|
+
throw new Error(`Bundle ${details.targetBundleId} does not belong to project ${projectId}`);
|
|
561
|
+
}
|
|
562
|
+
expectedTargetKind = 'bundle';
|
|
563
|
+
expectedBundleId = bundle.id;
|
|
564
|
+
}
|
|
565
|
+
if (details.targetKind !== expectedTargetKind || (details.targetBundleId ?? null) !== expectedBundleId) {
|
|
566
|
+
throw new Error('Test execution target does not match its ticket or bundle context');
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
export const hasDirectRetryChild = (runId) => Boolean(getDirectRetryChildStatement.get(runId));
|
|
570
|
+
const insertTestDetails = (runId, details) => {
|
|
571
|
+
const boundedOutput = boundTestOutput(details.output ?? '', details.outputTruncated);
|
|
572
|
+
insertTestDetailsStatement.run(runId, details.commandId ?? null, details.commandName ?? null, details.commandRunner ?? null, details.commandSnapshot ?? null, details.activeCommand ?? null, details.targetKind ?? null, details.targetBundleId ?? null, details.targetLabel ?? null, details.ticketId ?? null, serializeBoundedObject(details.selection, 'Test selection'), serializeBoundedObject(details.summary, 'Test summary'), details.exitCode ?? null, details.durationMs ?? null, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, Buffer.byteLength(boundedOutput.output, 'utf8'));
|
|
573
|
+
};
|
|
574
|
+
const insertGitHubActionsDetails = (runId, details) => {
|
|
575
|
+
const jobsJson = serializeJobsSnapshot(details.jobs);
|
|
576
|
+
insertGitHubActionsDetailsStatement.run(runId, details.workflowId, details.workflowName ?? null, details.ref ?? null, details.inputNames ? JSON.stringify([...new Set(details.inputNames)].sort()) : null, details.inputFingerprint ?? null, details.resolutionAttempts ?? 0, details.providerErrorCount ?? 0, details.providerRunId ?? null, details.providerStatus ?? null, details.conclusion ?? null, details.htmlUrl ?? null, details.headSha ?? null, details.headBranch ?? null, details.providerCreatedAt ?? null, details.providerUpdatedAt ?? null, details.providerStartedAt ?? null, details.dispatchedAt ?? null, details.lastSyncedAt ?? null, jobsJson, details.jobs?.length ?? 0);
|
|
577
|
+
};
|
|
578
|
+
const getRunRow = (projectId, runId) => {
|
|
579
|
+
const row = getRunStatement.get(runId, projectId);
|
|
580
|
+
if (!row) {
|
|
581
|
+
throw new Error(`Execution run ${runId} was not found in project ${projectId}`);
|
|
582
|
+
}
|
|
583
|
+
return row;
|
|
584
|
+
};
|
|
585
|
+
const getRelatedRun = (projectId, runId, relation) => {
|
|
586
|
+
const row = getRunByIdStatement.get(runId);
|
|
587
|
+
if (!row) {
|
|
588
|
+
throw new Error(`${relation} execution run ${runId} was not found`);
|
|
589
|
+
}
|
|
590
|
+
if (row.project_id !== projectId) {
|
|
591
|
+
throw new Error(`${relation} execution run must belong to project ${projectId}`);
|
|
592
|
+
}
|
|
593
|
+
return row;
|
|
594
|
+
};
|
|
595
|
+
const assertNoLineageCycle = (runId, relatedRun, column) => {
|
|
596
|
+
const visited = new Set();
|
|
597
|
+
let current = relatedRun;
|
|
598
|
+
while (current) {
|
|
599
|
+
if (current.id === runId) {
|
|
600
|
+
throw new Error(`Execution run ${runId} would create a lineage cycle`);
|
|
601
|
+
}
|
|
602
|
+
if (visited.has(current.id)) {
|
|
603
|
+
throw new Error(`Existing execution run lineage contains a cycle at ${current.id}`);
|
|
604
|
+
}
|
|
605
|
+
visited.add(current.id);
|
|
606
|
+
const nextId = current[column];
|
|
607
|
+
current = nextId ? getRunByIdStatement.get(nextId) : undefined;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const createEvent = (input) => {
|
|
611
|
+
assertBoundedString(input.eventType, 'Lifecycle event type', 100);
|
|
612
|
+
const message = normalizeBoundedMessage(input.message, MAX_EVENT_MESSAGE_LENGTH);
|
|
613
|
+
const schemaVersion = input.metadataSchemaVersion ?? 1;
|
|
614
|
+
if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {
|
|
615
|
+
throw new Error('Lifecycle event metadata schema version must be a positive integer');
|
|
616
|
+
}
|
|
617
|
+
const id = input.id ?? randomUUID();
|
|
618
|
+
const run = getRunRow(input.projectId, input.runId);
|
|
619
|
+
const sequence = nextEventSequenceStatement.get(input.runId).sequence;
|
|
620
|
+
insertEventStatement.run(id, input.projectId, input.runId, run.correlation_id, sequence, input.eventType, input.fromStatus, input.toStatus, input.domainStatus, message, schemaVersion, input.metadataJson, input.createdAt);
|
|
621
|
+
const row = getEventStatement.get(id, input.projectId);
|
|
622
|
+
return serializeEvent(row);
|
|
623
|
+
};
|
|
624
|
+
const emitCommittedNotifications = (notifications) => {
|
|
625
|
+
for (const notification of notifications) {
|
|
626
|
+
try {
|
|
627
|
+
if (notification.event === 'execution-run:updated') {
|
|
628
|
+
notification.emitter?.emit(notification.event, notification.payload);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
notification.emitter?.emit(notification.event, toCanonicalLifecycleEvent(notification.payload));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
// Realtime delivery is best-effort after the database transaction commits.
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
const enqueueRunAndEvent = (notifications, run, event, emitter) => {
|
|
640
|
+
notifications.push({ event: 'execution-run:updated', payload: run, emitter }, { event: 'lifecycle-event:created', payload: event, emitter });
|
|
641
|
+
};
|
|
642
|
+
const createExecutionRunInTransaction = (input, allowAutomationSideEffectRetry = false) => {
|
|
643
|
+
if (!projectExistsStatement.get(input.projectId)) {
|
|
644
|
+
throw new Error(`Project ${input.projectId} was not found`);
|
|
645
|
+
}
|
|
646
|
+
const id = input.id ?? randomUUID();
|
|
647
|
+
assertBoundedString(id, 'Execution run ID', 128);
|
|
648
|
+
if (!APPROVED_EXECUTION_RUN_KINDS.has(input.kind)) {
|
|
649
|
+
throw new Error(`Unsupported execution run kind: ${input.kind}`);
|
|
650
|
+
}
|
|
651
|
+
const initiatedBy = input.initiatedBy ?? 'user';
|
|
652
|
+
if (!EXECUTION_RUN_INITIATORS.has(initiatedBy)) {
|
|
653
|
+
throw new Error(`Unsupported execution run initiator: ${initiatedBy}`);
|
|
654
|
+
}
|
|
655
|
+
assertBoundedString(input.domainStatus, 'Domain status', 200);
|
|
656
|
+
const links = normalizeInitialLinks(input).map(link => validateExecutionRunLink(input.projectId, link));
|
|
657
|
+
const primaryLink = links.find(link => link.relationship === 'primary') ?? null;
|
|
658
|
+
const metadataSchemaVersion = input.metadataSchemaVersion ?? 1;
|
|
659
|
+
if (!Number.isSafeInteger(metadataSchemaVersion) || metadataSchemaVersion < 1) {
|
|
660
|
+
throw new Error('Execution run metadata schema version must be a positive integer');
|
|
661
|
+
}
|
|
662
|
+
const metadataJson = serializeBoundedObject(input.metadata, 'Execution run metadata');
|
|
663
|
+
let retrySource = null;
|
|
664
|
+
if (input.retryOfRunId) {
|
|
665
|
+
retrySource = getRelatedRun(input.projectId, input.retryOfRunId, 'Retry source');
|
|
666
|
+
if (retrySource.kind !== input.kind) {
|
|
667
|
+
throw new Error('Retry source execution run must have the same kind');
|
|
668
|
+
}
|
|
669
|
+
assertNoLineageCycle(id, retrySource, 'retry_of_run_id');
|
|
670
|
+
if (allowAutomationSideEffectRetry && !GENERIC_RETRY_DENIED_KINDS.has(input.kind)) {
|
|
671
|
+
throw new Error('Dedicated automation retry creation only supports automation side-effect runs');
|
|
672
|
+
}
|
|
673
|
+
assertRetryEligible(input.kind, retrySource, allowAutomationSideEffectRetry);
|
|
674
|
+
}
|
|
675
|
+
let expectedParentId = retrySource?.parent_run_id ?? null;
|
|
676
|
+
if (retrySource && input.parentRunId !== undefined && input.parentRunId !== expectedParentId) {
|
|
677
|
+
const recoveryParent = allowAutomationSideEffectRetry && typeof input.parentRunId === 'string'
|
|
678
|
+
? getRelatedRun(input.projectId, input.parentRunId, 'Recovery parent')
|
|
679
|
+
: null;
|
|
680
|
+
if (recoveryParent?.kind !== 'automation_progression'
|
|
681
|
+
|| recoveryParent.initiated_by !== 'recovery'
|
|
682
|
+
|| recoveryParent.parent_run_id !== retrySource.id
|
|
683
|
+
|| !ACTIVE_STATUSES.has(recoveryParent.status)) {
|
|
684
|
+
throw new Error('Retry parent execution run must match the retry source parent');
|
|
685
|
+
}
|
|
686
|
+
expectedParentId = recoveryParent.id;
|
|
687
|
+
}
|
|
688
|
+
const parentRunId = retrySource ? expectedParentId : input.parentRunId ?? null;
|
|
689
|
+
let parent = null;
|
|
690
|
+
if (parentRunId) {
|
|
691
|
+
parent = getRelatedRun(input.projectId, parentRunId, 'Parent');
|
|
692
|
+
assertNoLineageCycle(id, parent, 'parent_run_id');
|
|
693
|
+
}
|
|
694
|
+
const correlationId = resolveCorrelationId(parent);
|
|
695
|
+
if (input.kind !== 'test' && input.testDetails) {
|
|
696
|
+
throw new Error('Test execution details require a test execution run');
|
|
697
|
+
}
|
|
698
|
+
if (input.kind !== 'github_actions' && input.githubActionsDetails) {
|
|
699
|
+
throw new Error('GitHub Actions details require a github_actions execution run');
|
|
700
|
+
}
|
|
701
|
+
if (input.kind === 'github_actions' && !input.githubActionsDetails) {
|
|
702
|
+
throw new Error('GitHub Actions execution runs require domain details');
|
|
703
|
+
}
|
|
704
|
+
if (input.kind === 'test') {
|
|
705
|
+
assertTestTarget(input.projectId, input.testDetails);
|
|
706
|
+
}
|
|
707
|
+
if (input.kind === 'github_actions') {
|
|
708
|
+
const details = input.githubActionsDetails;
|
|
709
|
+
if (!Number.isSafeInteger(details.workflowId) || details.workflowId < 1) {
|
|
710
|
+
throw new Error('GitHub Actions execution runs require a positive workflow ID');
|
|
711
|
+
}
|
|
712
|
+
const conflict = db.prepare(`
|
|
713
|
+
SELECT er.id
|
|
714
|
+
FROM execution_runs er
|
|
715
|
+
JOIN github_actions_run_details ga ON ga.execution_run_id = er.id
|
|
716
|
+
WHERE er.project_id = ?
|
|
717
|
+
AND er.kind = 'github_actions'
|
|
718
|
+
AND er.status IN ('queued', 'waiting')
|
|
719
|
+
AND ga.provider_run_id IS NULL
|
|
720
|
+
AND ga.workflow_id = ?
|
|
721
|
+
AND ga.ref = ?
|
|
722
|
+
LIMIT 1
|
|
723
|
+
`).get(input.projectId, details.workflowId, details.ref ?? null);
|
|
724
|
+
if (conflict) {
|
|
725
|
+
throw new GitHubActionsDispatchConflictError(conflict.id);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const now = new Date().toISOString();
|
|
729
|
+
insertRunStatement.run(id, input.projectId, input.kind, input.domainStatus ?? null, primaryLink?.entityType ?? null, primaryLink?.entityId ?? null, primaryLink?.label ?? null, primaryLink?.url ?? null, correlationId, initiatedBy, metadataSchemaVersion, metadataJson, parent?.id ?? null, retrySource?.id ?? null, retrySource ? retrySource.attempt + 1 : 1, now, now);
|
|
730
|
+
for (const link of links) {
|
|
731
|
+
insertRunLinkStatement.run(id, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
|
|
732
|
+
}
|
|
733
|
+
if (input.kind === 'test') {
|
|
734
|
+
insertTestDetails(id, input.testDetails);
|
|
735
|
+
}
|
|
736
|
+
if (input.kind === 'github_actions') {
|
|
737
|
+
insertGitHubActionsDetails(id, input.githubActionsDetails);
|
|
738
|
+
}
|
|
739
|
+
const event = createEvent({
|
|
740
|
+
projectId: input.projectId,
|
|
741
|
+
runId: id,
|
|
742
|
+
eventType: input.eventType ?? 'created',
|
|
743
|
+
fromStatus: null,
|
|
744
|
+
toStatus: 'queued',
|
|
745
|
+
domainStatus: input.domainStatus ?? null,
|
|
746
|
+
message: input.eventMessage,
|
|
747
|
+
metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
|
|
748
|
+
createdAt: now,
|
|
749
|
+
});
|
|
750
|
+
return { run: serializeRun(getRunRow(input.projectId, id)), event };
|
|
751
|
+
};
|
|
752
|
+
const insertRunLink = (input) => {
|
|
753
|
+
if (input.link.relationship === 'primary') {
|
|
754
|
+
throw new Error('Primary execution run links can only be set at run creation');
|
|
755
|
+
}
|
|
756
|
+
const link = validateExecutionRunLink(input.projectId, input.link);
|
|
757
|
+
const existing = getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
|
|
758
|
+
if (existing) {
|
|
759
|
+
throw new Error(`Execution run link already exists for ${link.entityType} ${link.entityId} as ${link.relationship}`);
|
|
760
|
+
}
|
|
761
|
+
const now = new Date().toISOString();
|
|
762
|
+
insertRunLinkStatement.run(input.runId, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
|
|
763
|
+
return getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
|
|
764
|
+
};
|
|
765
|
+
const addExecutionRunLinkInTransaction = (input) => {
|
|
766
|
+
getRunRow(input.projectId, input.runId);
|
|
767
|
+
return insertRunLink(input);
|
|
768
|
+
};
|
|
769
|
+
const addExecutionRunLinksInTransaction = (input) => {
|
|
770
|
+
getRunRow(input.projectId, input.runId);
|
|
771
|
+
return input.links.map(link => insertRunLink({
|
|
772
|
+
projectId: input.projectId,
|
|
773
|
+
runId: input.runId,
|
|
774
|
+
link,
|
|
775
|
+
}));
|
|
776
|
+
};
|
|
777
|
+
const transitionExecutionRunInTransaction = (input, allowAmbiguousAutomationReconciliation = false) => {
|
|
778
|
+
const current = getRunRow(input.projectId, input.runId);
|
|
779
|
+
const domainStatus = input.domainStatus === undefined ? current.domain_status : input.domainStatus;
|
|
780
|
+
const failure = input.failure
|
|
781
|
+
? { ...input.failure, message: normalizeBoundedMessage(input.failure.message, MAX_FAILURE_MESSAGE_LENGTH) ?? '' }
|
|
782
|
+
: null;
|
|
783
|
+
assertBoundedString(domainStatus, 'Domain status', 200);
|
|
784
|
+
assertBoundedString(failure?.code, 'Failure code', 200);
|
|
785
|
+
const failureDetailsJson = serializeBoundedObject(failure?.details, 'Failure details');
|
|
786
|
+
const isAmbiguousAutomationReconciliation = allowAmbiguousAutomationReconciliation
|
|
787
|
+
&& current.status === 'failed'
|
|
788
|
+
&& current.domain_status === 'ambiguous'
|
|
789
|
+
&& GENERIC_RETRY_DENIED_KINDS.has(current.kind)
|
|
790
|
+
&& (input.status === 'completed' || input.status === 'interrupted');
|
|
791
|
+
if (TERMINAL_STATUSES.has(current.status) && !isAmbiguousAutomationReconciliation) {
|
|
792
|
+
const terminalDataAgrees = current.status === input.status
|
|
793
|
+
&& current.domain_status === domainStatus
|
|
794
|
+
&& current.failure_code === (failure?.code ?? null)
|
|
795
|
+
&& current.failure_message === (failure?.message ?? null)
|
|
796
|
+
&& current.failure_details_json === failureDetailsJson
|
|
797
|
+
&& (input.completedAt === undefined || current.completed_at === input.completedAt);
|
|
798
|
+
if (!terminalDataAgrees) {
|
|
799
|
+
throw new Error(`Execution run ${input.runId} is already terminal with conflicting data`);
|
|
800
|
+
}
|
|
801
|
+
return { run: serializeRun(current), event: null };
|
|
802
|
+
}
|
|
803
|
+
if (current.status === input.status) {
|
|
804
|
+
throw new Error(`Execution run ${input.runId} is already ${input.status}`);
|
|
805
|
+
}
|
|
806
|
+
if (ACTIVE_STATUSES.has(input.status)) {
|
|
807
|
+
const activeTransitionAllowed = (current.status === 'queued' && (input.status === 'running' || input.status === 'waiting'))
|
|
808
|
+
|| (current.status === 'running' && input.status === 'waiting')
|
|
809
|
+
|| (current.status === 'waiting' && input.status === 'running');
|
|
810
|
+
if (!activeTransitionAllowed) {
|
|
811
|
+
throw new Error(`Illegal execution run transition from ${current.status} to ${input.status}`);
|
|
812
|
+
}
|
|
813
|
+
if (failure) {
|
|
814
|
+
throw new Error('Active execution runs cannot have failure information');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const now = new Date().toISOString();
|
|
818
|
+
const completedAt = TERMINAL_STATUSES.has(input.status) ? input.completedAt ?? now : null;
|
|
819
|
+
const startedAt = input.status === 'running' ? current.started_at ?? now : current.started_at;
|
|
820
|
+
updateRunStatement.run(input.status, domainStatus, failure?.code ?? null, failure?.message ?? null, failureDetailsJson, startedAt, completedAt, now, input.runId, input.projectId);
|
|
821
|
+
const event = createEvent({
|
|
822
|
+
projectId: input.projectId,
|
|
823
|
+
runId: input.runId,
|
|
824
|
+
eventType: input.eventType ?? 'status_changed',
|
|
825
|
+
fromStatus: current.status,
|
|
826
|
+
toStatus: input.status,
|
|
827
|
+
domainStatus,
|
|
828
|
+
message: input.eventMessage,
|
|
829
|
+
metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
|
|
830
|
+
createdAt: now,
|
|
831
|
+
});
|
|
832
|
+
return { run: serializeRun(getRunRow(input.projectId, input.runId)), event };
|
|
833
|
+
};
|
|
834
|
+
const appendLifecycleEventInTransaction = (input) => {
|
|
835
|
+
const run = getRunRow(input.projectId, input.runId);
|
|
836
|
+
const event = createEvent({
|
|
837
|
+
projectId: input.projectId,
|
|
838
|
+
runId: input.runId,
|
|
839
|
+
eventType: input.eventType,
|
|
840
|
+
fromStatus: run.status,
|
|
841
|
+
toStatus: run.status,
|
|
842
|
+
domainStatus: input.domainStatus === undefined ? run.domain_status : input.domainStatus,
|
|
843
|
+
message: input.message,
|
|
844
|
+
metadataSchemaVersion: input.metadataSchemaVersion,
|
|
845
|
+
metadataJson: serializeBoundedObject(input.metadata, 'Lifecycle event metadata'),
|
|
846
|
+
createdAt: new Date().toISOString(),
|
|
847
|
+
});
|
|
848
|
+
return { run: serializeRun(run), event };
|
|
849
|
+
};
|
|
850
|
+
const isPromiseLike = (value) => {
|
|
851
|
+
if ((typeof value !== 'object' || value === null) && typeof value !== 'function') {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
return typeof value.then === 'function';
|
|
855
|
+
};
|
|
856
|
+
const executionTransactionNotificationScopes = [];
|
|
857
|
+
export const withExecutionRunTransaction = (callback, emitter) => {
|
|
858
|
+
const notifications = [];
|
|
859
|
+
const runTransaction = db.transaction(() => {
|
|
860
|
+
const transaction = {
|
|
861
|
+
createExecutionRun: input => {
|
|
862
|
+
const result = createExecutionRunInTransaction(input);
|
|
863
|
+
enqueueRunAndEvent(notifications, result.run, result.event, emitter);
|
|
864
|
+
return result.run;
|
|
865
|
+
},
|
|
866
|
+
createAutomationRetryExecutionRun: input => {
|
|
867
|
+
const result = createExecutionRunInTransaction(input, true);
|
|
868
|
+
enqueueRunAndEvent(notifications, result.run, result.event, emitter);
|
|
869
|
+
return result.run;
|
|
870
|
+
},
|
|
871
|
+
transitionExecutionRun: input => {
|
|
872
|
+
const result = transitionExecutionRunInTransaction(input);
|
|
873
|
+
if (result.event) {
|
|
874
|
+
enqueueRunAndEvent(notifications, result.run, result.event, emitter);
|
|
875
|
+
}
|
|
876
|
+
return result.run;
|
|
877
|
+
},
|
|
878
|
+
reconcileAmbiguousAutomationExecutionRun: input => {
|
|
879
|
+
const result = transitionExecutionRunInTransaction(input, true);
|
|
880
|
+
if (result.event) {
|
|
881
|
+
enqueueRunAndEvent(notifications, result.run, result.event, emitter);
|
|
882
|
+
}
|
|
883
|
+
return result.run;
|
|
884
|
+
},
|
|
885
|
+
appendLifecycleEvent: input => {
|
|
886
|
+
const result = appendLifecycleEventInTransaction(input);
|
|
887
|
+
notifications.push({ event: 'lifecycle-event:created', payload: result.event, emitter });
|
|
888
|
+
return result.event;
|
|
889
|
+
},
|
|
890
|
+
addExecutionRunLink: input => {
|
|
891
|
+
const link = addExecutionRunLinkInTransaction(input);
|
|
892
|
+
return { ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) };
|
|
893
|
+
},
|
|
894
|
+
addExecutionRunLinks: input => addExecutionRunLinksInTransaction(input).map(link => ({
|
|
895
|
+
...link,
|
|
896
|
+
url_snapshot: safeSnapshotUrl(link.url_snapshot),
|
|
897
|
+
})),
|
|
898
|
+
};
|
|
899
|
+
const result = callback(transaction);
|
|
900
|
+
if (isPromiseLike(result)) {
|
|
901
|
+
throw new Error('Execution run transactions require a synchronous callback');
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
904
|
+
});
|
|
905
|
+
const parentNotifications = executionTransactionNotificationScopes.at(-1);
|
|
906
|
+
executionTransactionNotificationScopes.push(notifications);
|
|
907
|
+
try {
|
|
908
|
+
const result = runTransaction();
|
|
909
|
+
executionTransactionNotificationScopes.pop();
|
|
910
|
+
if (parentNotifications) {
|
|
911
|
+
parentNotifications.push(...notifications);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
emitCommittedNotifications(notifications);
|
|
915
|
+
}
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
executionTransactionNotificationScopes.pop();
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
export const createExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.createExecutionRun(input), emitter));
|
|
924
|
+
export const addExecutionRunLink = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLink(input)));
|
|
925
|
+
export const addExecutionRunLinks = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLinks(input)));
|
|
926
|
+
export const transitionExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.transitionExecutionRun(input), emitter));
|
|
927
|
+
export const appendLifecycleEvent = (input, emitter) => {
|
|
928
|
+
return withExecutionRunTransaction(transaction => transaction.appendLifecycleEvent(input), emitter);
|
|
929
|
+
};
|
|
930
|
+
export const getExecutionRun = (projectId, runId) => {
|
|
931
|
+
const row = getRunStatement.get(runId, projectId);
|
|
932
|
+
return row ? serializeRun(row) : null;
|
|
933
|
+
};
|
|
934
|
+
export const getQueuedSpecReviewExecutionRun = (projectId, snapshotId) => {
|
|
935
|
+
const row = db.prepare(`
|
|
936
|
+
SELECT execution_runs.id
|
|
937
|
+
FROM execution_runs
|
|
938
|
+
INNER JOIN execution_run_links snapshot_link
|
|
939
|
+
ON snapshot_link.execution_run_id = execution_runs.id
|
|
940
|
+
AND snapshot_link.project_id = execution_runs.project_id
|
|
941
|
+
AND snapshot_link.entity_type = 'spec_snapshot'
|
|
942
|
+
AND snapshot_link.relationship = 'context'
|
|
943
|
+
WHERE execution_runs.project_id = ?
|
|
944
|
+
AND execution_runs.kind = 'spec_review'
|
|
945
|
+
AND execution_runs.status = 'queued'
|
|
946
|
+
AND snapshot_link.entity_id = ?
|
|
947
|
+
ORDER BY execution_runs.created_at DESC, execution_runs.id DESC
|
|
948
|
+
LIMIT 1
|
|
949
|
+
`).get(projectId, snapshotId);
|
|
950
|
+
return row ? getExecutionRun(projectId, row.id) : null;
|
|
951
|
+
};
|
|
952
|
+
export const getQueuedBuildExecutionRun = (projectId, ticketId, sequenceId) => {
|
|
953
|
+
const row = db.prepare(`
|
|
954
|
+
SELECT execution_runs.id
|
|
955
|
+
FROM execution_runs
|
|
956
|
+
INNER JOIN execution_run_links ticket_link
|
|
957
|
+
ON ticket_link.execution_run_id = execution_runs.id
|
|
958
|
+
AND ticket_link.project_id = execution_runs.project_id
|
|
959
|
+
AND ticket_link.entity_type = 'ticket'
|
|
960
|
+
AND ticket_link.relationship = 'primary'
|
|
961
|
+
INNER JOIN execution_run_links sequence_link
|
|
962
|
+
ON sequence_link.execution_run_id = execution_runs.id
|
|
963
|
+
AND sequence_link.project_id = execution_runs.project_id
|
|
964
|
+
AND sequence_link.entity_type = 'build_sequence'
|
|
965
|
+
AND sequence_link.relationship = 'context'
|
|
966
|
+
WHERE execution_runs.project_id = ?
|
|
967
|
+
AND execution_runs.kind = 'build'
|
|
968
|
+
AND execution_runs.status = 'queued'
|
|
969
|
+
AND ticket_link.entity_id = ?
|
|
970
|
+
AND sequence_link.entity_id = ?
|
|
971
|
+
ORDER BY execution_runs.created_at ASC, execution_runs.id ASC
|
|
972
|
+
LIMIT 1
|
|
973
|
+
`).get(projectId, ticketId, sequenceId);
|
|
974
|
+
return row ? getExecutionRun(projectId, row.id) : null;
|
|
975
|
+
};
|
|
976
|
+
export const hasActiveBuildExecutionRunForTicket = (projectId, ticketId) => {
|
|
977
|
+
return Boolean(db.prepare(`
|
|
978
|
+
SELECT 1
|
|
979
|
+
FROM execution_runs
|
|
980
|
+
INNER JOIN execution_run_links ticket_link
|
|
981
|
+
ON ticket_link.execution_run_id = execution_runs.id
|
|
982
|
+
AND ticket_link.project_id = execution_runs.project_id
|
|
983
|
+
AND ticket_link.entity_type = 'ticket'
|
|
984
|
+
AND ticket_link.relationship = 'primary'
|
|
985
|
+
WHERE execution_runs.project_id = ?
|
|
986
|
+
AND execution_runs.kind = 'build'
|
|
987
|
+
AND execution_runs.status IN ('queued', 'running', 'waiting')
|
|
988
|
+
AND ticket_link.entity_id = ?
|
|
989
|
+
LIMIT 1
|
|
990
|
+
`).get(projectId, ticketId));
|
|
991
|
+
};
|
|
992
|
+
const purgeTestExecutionRunTransaction = db.transaction((projectId, runId) => {
|
|
993
|
+
return db.prepare(`
|
|
994
|
+
DELETE FROM execution_runs
|
|
995
|
+
WHERE id = ? AND project_id = ? AND kind = 'test'
|
|
996
|
+
`).run(runId, projectId).changes > 0;
|
|
997
|
+
});
|
|
998
|
+
export const purgeTestExecutionRun = (projectId, runId) => {
|
|
999
|
+
return purgeTestExecutionRunTransaction(projectId, runId);
|
|
1000
|
+
};
|
|
1001
|
+
export const listExecutionRunLinks = (projectId, runId) => {
|
|
1002
|
+
getRunRow(projectId, runId);
|
|
1003
|
+
const links = getRunLinksStatement.all(runId, projectId);
|
|
1004
|
+
return links.map(link => ({ ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) }));
|
|
1005
|
+
};
|
|
1006
|
+
const testDetailsSelect = `
|
|
1007
|
+
SELECT td.*, er.project_id, er.status AS execution_status
|
|
1008
|
+
FROM test_execution_details td
|
|
1009
|
+
JOIN execution_runs er ON er.id = td.execution_run_id
|
|
1010
|
+
`;
|
|
1011
|
+
const serializeTestDetails = (row) => ({
|
|
1012
|
+
execution_run_id: row.execution_run_id,
|
|
1013
|
+
project_id: row.project_id,
|
|
1014
|
+
execution_status: row.execution_status,
|
|
1015
|
+
command_id: row.command_id,
|
|
1016
|
+
command_name: row.command_name,
|
|
1017
|
+
command_runner: row.command_runner,
|
|
1018
|
+
command_snapshot: row.command_snapshot,
|
|
1019
|
+
active_command: row.active_command,
|
|
1020
|
+
target_kind: row.target_kind,
|
|
1021
|
+
target_bundle_id: row.target_bundle_id,
|
|
1022
|
+
target_label: row.target_label,
|
|
1023
|
+
ticket_id: row.ticket_id,
|
|
1024
|
+
selection: parseStoredObject(row.selection_json),
|
|
1025
|
+
summary: parseStoredObject(row.summary_json),
|
|
1026
|
+
exit_code: row.exit_code,
|
|
1027
|
+
duration_ms: row.duration_ms,
|
|
1028
|
+
output: row.output,
|
|
1029
|
+
output_truncated: row.output_truncated === 1,
|
|
1030
|
+
output_retention_state: row.output_retention_state,
|
|
1031
|
+
output_expired_at: row.output_expired_at,
|
|
1032
|
+
output_byte_count: row.output_byte_count ?? Buffer.byteLength(row.output, 'utf8'),
|
|
1033
|
+
});
|
|
1034
|
+
export const getTestExecutionDetails = (projectId, runId) => {
|
|
1035
|
+
const row = db.prepare(`${testDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
|
|
1036
|
+
.get(projectId, runId);
|
|
1037
|
+
return row ? serializeTestDetails(row) : null;
|
|
1038
|
+
};
|
|
1039
|
+
export const updateTestExecutionDetails = (input) => {
|
|
1040
|
+
const current = getTestExecutionDetails(input.projectId, input.runId);
|
|
1041
|
+
if (!current) {
|
|
1042
|
+
throw new Error(`Test execution run ${input.runId} was not found in project ${input.projectId}`);
|
|
1043
|
+
}
|
|
1044
|
+
const boundedOutput = input.output === undefined
|
|
1045
|
+
? { output: current.output, outputTruncated: current.output_truncated }
|
|
1046
|
+
: boundTestOutput(input.output, input.outputTruncated ?? current.output_truncated);
|
|
1047
|
+
db.prepare(`
|
|
1048
|
+
UPDATE test_execution_details
|
|
1049
|
+
SET summary_json = ?, exit_code = ?, duration_ms = ?, output = ?, output_truncated = ?,
|
|
1050
|
+
output_retention_state = ?, output_expired_at = ?, output_byte_count = ?
|
|
1051
|
+
WHERE execution_run_id = ?
|
|
1052
|
+
`).run(input.summary === undefined ? serializeBoundedObject(current.summary, 'Test summary') : serializeBoundedObject(input.summary, 'Test summary'), input.exitCode === undefined ? current.exit_code : input.exitCode, input.durationMs === undefined ? current.duration_ms : input.durationMs, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, input.output === undefined ? current.output_retention_state : 'retained', input.output === undefined ? current.output_expired_at : null, input.output === undefined ? current.output_byte_count : Buffer.byteLength(boundedOutput.output, 'utf8'), input.runId);
|
|
1053
|
+
return getTestExecutionDetails(input.projectId, input.runId);
|
|
1054
|
+
};
|
|
1055
|
+
export const reconcileInterruptedTestRuns = (emitter) => {
|
|
1056
|
+
const rows = db.prepare(`
|
|
1057
|
+
SELECT project_id, id
|
|
1058
|
+
FROM execution_runs
|
|
1059
|
+
WHERE kind = 'test' AND status IN ('queued', 'running', 'waiting')
|
|
1060
|
+
`).all();
|
|
1061
|
+
for (const row of rows) {
|
|
1062
|
+
transitionExecutionRun({
|
|
1063
|
+
projectId: row.project_id,
|
|
1064
|
+
runId: row.id,
|
|
1065
|
+
status: 'interrupted',
|
|
1066
|
+
domainStatus: 'interrupted',
|
|
1067
|
+
failure: {
|
|
1068
|
+
code: 'server_restarted',
|
|
1069
|
+
message: 'The server restarted while this local test run was active.',
|
|
1070
|
+
},
|
|
1071
|
+
eventType: 'test_run_interrupted',
|
|
1072
|
+
}, emitter);
|
|
1073
|
+
}
|
|
1074
|
+
return rows.length;
|
|
1075
|
+
};
|
|
1076
|
+
const serializeGitHubActionsDetails = (row) => ({
|
|
1077
|
+
execution_run_id: row.execution_run_id,
|
|
1078
|
+
project_id: row.project_id,
|
|
1079
|
+
execution_status: row.execution_status,
|
|
1080
|
+
domain_status: row.domain_status,
|
|
1081
|
+
failure: row.failure_message ? {
|
|
1082
|
+
code: row.failure_code,
|
|
1083
|
+
message: row.failure_message,
|
|
1084
|
+
details: parseStoredObject(row.failure_details_json),
|
|
1085
|
+
} : null,
|
|
1086
|
+
workflow_id: row.workflow_id,
|
|
1087
|
+
workflow_name: row.workflow_name,
|
|
1088
|
+
ref: row.ref,
|
|
1089
|
+
input_names: parseStoredArray(row.input_names_json).filter(value => typeof value === 'string'),
|
|
1090
|
+
input_fingerprint: row.input_fingerprint,
|
|
1091
|
+
resolution_attempts: row.resolution_attempts,
|
|
1092
|
+
provider_error_count: row.provider_error_count,
|
|
1093
|
+
provider_run_id: row.provider_run_id,
|
|
1094
|
+
provider_status: row.provider_status,
|
|
1095
|
+
conclusion: row.conclusion,
|
|
1096
|
+
html_url: row.html_url,
|
|
1097
|
+
head_sha: row.head_sha,
|
|
1098
|
+
head_branch: row.head_branch,
|
|
1099
|
+
provider_created_at: row.provider_created_at,
|
|
1100
|
+
provider_updated_at: row.provider_updated_at,
|
|
1101
|
+
provider_started_at: row.provider_started_at,
|
|
1102
|
+
dispatched_at: row.dispatched_at,
|
|
1103
|
+
last_synced_at: row.last_synced_at,
|
|
1104
|
+
jobs: parseStoredArray(row.jobs_json),
|
|
1105
|
+
jobs_retention_state: row.jobs_retention_state,
|
|
1106
|
+
jobs_expired_at: row.jobs_expired_at,
|
|
1107
|
+
jobs_count: row.jobs_count ?? parseStoredArray(row.jobs_json).length,
|
|
1108
|
+
});
|
|
1109
|
+
const githubActionsDetailsSelect = `
|
|
1110
|
+
SELECT ga.*, er.project_id, er.status AS execution_status, er.domain_status,
|
|
1111
|
+
er.failure_code, er.failure_message, er.failure_details_json
|
|
1112
|
+
FROM github_actions_run_details ga
|
|
1113
|
+
JOIN execution_runs er ON er.id = ga.execution_run_id
|
|
1114
|
+
`;
|
|
1115
|
+
export const getGitHubActionsExecutionDetails = (projectId, runId) => {
|
|
1116
|
+
const row = db.prepare(`${githubActionsDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
|
|
1117
|
+
.get(projectId, runId);
|
|
1118
|
+
return row ? serializeGitHubActionsDetails(row) : null;
|
|
1119
|
+
};
|
|
1120
|
+
export const listGitHubActionsExecutionDetails = (projectId, statuses) => {
|
|
1121
|
+
const clauses = ["er.kind = 'github_actions'"];
|
|
1122
|
+
const parameters = [];
|
|
1123
|
+
if (projectId) {
|
|
1124
|
+
clauses.push('er.project_id = ?');
|
|
1125
|
+
parameters.push(projectId);
|
|
1126
|
+
}
|
|
1127
|
+
if (statuses && statuses.length > 0) {
|
|
1128
|
+
clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
|
|
1129
|
+
parameters.push(...statuses);
|
|
1130
|
+
}
|
|
1131
|
+
const rows = db.prepare(`${githubActionsDetailsSelect} WHERE ${clauses.join(' AND ')} ORDER BY er.created_at DESC, er.id DESC`)
|
|
1132
|
+
.all(...parameters);
|
|
1133
|
+
return rows.map(serializeGitHubActionsDetails);
|
|
1134
|
+
};
|
|
1135
|
+
export const updateGitHubActionsRunDetails = (input) => {
|
|
1136
|
+
const current = getGitHubActionsExecutionDetails(input.projectId, input.runId);
|
|
1137
|
+
if (!current) {
|
|
1138
|
+
throw new Error(`GitHub Actions execution run ${input.runId} was not found in project ${input.projectId}`);
|
|
1139
|
+
}
|
|
1140
|
+
const jobsJson = input.jobs === undefined ? serializeJobsSnapshot(current.jobs) : serializeJobsSnapshot(input.jobs);
|
|
1141
|
+
db.prepare(`
|
|
1142
|
+
UPDATE github_actions_run_details
|
|
1143
|
+
SET workflow_name = ?, resolution_attempts = ?, provider_error_count = ?, provider_run_id = ?, provider_status = ?,
|
|
1144
|
+
conclusion = ?, html_url = ?, head_sha = ?, head_branch = ?, provider_created_at = ?,
|
|
1145
|
+
provider_updated_at = ?, provider_started_at = ?, last_synced_at = ?, jobs_json = ?,
|
|
1146
|
+
jobs_retention_state = ?, jobs_expired_at = ?, jobs_count = ?
|
|
1147
|
+
WHERE execution_run_id = ?
|
|
1148
|
+
`).run(input.workflowName === undefined ? current.workflow_name : input.workflowName, input.resolutionAttempts ?? current.resolution_attempts, input.providerErrorCount ?? current.provider_error_count, input.providerRunId === undefined ? current.provider_run_id : input.providerRunId, input.providerStatus === undefined ? current.provider_status : input.providerStatus, input.conclusion === undefined ? current.conclusion : input.conclusion, input.htmlUrl === undefined ? current.html_url : input.htmlUrl, input.headSha === undefined ? current.head_sha : input.headSha, input.headBranch === undefined ? current.head_branch : input.headBranch, input.providerCreatedAt === undefined ? current.provider_created_at : input.providerCreatedAt, input.providerUpdatedAt === undefined ? current.provider_updated_at : input.providerUpdatedAt, input.providerStartedAt === undefined ? current.provider_started_at : input.providerStartedAt, input.lastSyncedAt === undefined ? current.last_synced_at : input.lastSyncedAt, jobsJson, input.jobs === undefined ? current.jobs_retention_state : 'retained', input.jobs === undefined ? current.jobs_expired_at : null, input.jobs === undefined ? current.jobs_count : input.jobs?.length ?? 0, input.runId);
|
|
1149
|
+
return getGitHubActionsExecutionDetails(input.projectId, input.runId);
|
|
1150
|
+
};
|
|
1151
|
+
export const toGitHubActionsRun = (details) => {
|
|
1152
|
+
if (details.provider_run_id === null) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
id: details.provider_run_id,
|
|
1157
|
+
name: details.workflow_name,
|
|
1158
|
+
workflow_id: details.workflow_id,
|
|
1159
|
+
status: details.provider_status === 'queued' || details.provider_status === 'in_progress' || details.provider_status === 'completed'
|
|
1160
|
+
? details.provider_status
|
|
1161
|
+
: 'unknown',
|
|
1162
|
+
conclusion: details.conclusion,
|
|
1163
|
+
html_url: details.html_url,
|
|
1164
|
+
head_branch: details.head_branch,
|
|
1165
|
+
created_at: details.provider_created_at,
|
|
1166
|
+
updated_at: details.provider_updated_at,
|
|
1167
|
+
run_started_at: details.provider_started_at,
|
|
1168
|
+
jobs: details.jobs,
|
|
1169
|
+
};
|
|
1170
|
+
};
|
|
1171
|
+
export const listExecutionRuns = (input) => {
|
|
1172
|
+
const limit = input.limit ?? 50;
|
|
1173
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
|
|
1174
|
+
throw new Error('Execution run page limit must be between 1 and 200');
|
|
1175
|
+
}
|
|
1176
|
+
const statuses = [...new Set(input.statuses ?? [])];
|
|
1177
|
+
const cursor = input.cursor ? decodeCursor(input.cursor) : null;
|
|
1178
|
+
const clauses = ['project_id = ?'];
|
|
1179
|
+
const parameters = [input.projectId];
|
|
1180
|
+
if (input.kind) {
|
|
1181
|
+
clauses.push('kind = ?');
|
|
1182
|
+
parameters.push(input.kind);
|
|
1183
|
+
}
|
|
1184
|
+
if (statuses.length > 0) {
|
|
1185
|
+
clauses.push(`status IN (${statuses.map(() => '?').join(', ')})`);
|
|
1186
|
+
parameters.push(...statuses);
|
|
1187
|
+
}
|
|
1188
|
+
if (cursor) {
|
|
1189
|
+
clauses.push('(created_at < ? OR (created_at = ? AND id < ?))');
|
|
1190
|
+
parameters.push(cursor.created_at, cursor.created_at, cursor.id);
|
|
1191
|
+
}
|
|
1192
|
+
const rows = db.prepare(`
|
|
1193
|
+
SELECT * FROM execution_runs
|
|
1194
|
+
WHERE ${clauses.join(' AND ')}
|
|
1195
|
+
ORDER BY created_at DESC, id DESC
|
|
1196
|
+
LIMIT ?
|
|
1197
|
+
`).all(...parameters, limit + 1);
|
|
1198
|
+
const pageRows = rows.slice(0, limit);
|
|
1199
|
+
const lastRow = pageRows.at(-1);
|
|
1200
|
+
return {
|
|
1201
|
+
runs: pageRows.map(serializeRun),
|
|
1202
|
+
next_cursor: rows.length > limit && lastRow
|
|
1203
|
+
? encodeCursor({ created_at: lastRow.created_at, id: lastRow.id })
|
|
1204
|
+
: null,
|
|
1205
|
+
};
|
|
1206
|
+
};
|
|
1207
|
+
export const listLifecycleEvents = (projectId, runId) => {
|
|
1208
|
+
getRunRow(projectId, runId);
|
|
1209
|
+
const rows = db.prepare(`
|
|
1210
|
+
SELECT * FROM lifecycle_events
|
|
1211
|
+
WHERE project_id = ? AND execution_run_id = ?
|
|
1212
|
+
ORDER BY sequence ASC
|
|
1213
|
+
`).all(projectId, runId);
|
|
1214
|
+
return rows.map(serializeEvent);
|
|
1215
|
+
};
|
|
1216
|
+
export const listProjectLifecycleEvents = (input) => {
|
|
1217
|
+
const limit = input.limit ?? 50;
|
|
1218
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
|
|
1219
|
+
throw new Error('Lifecycle event page limit must be between 1 and 200');
|
|
1220
|
+
}
|
|
1221
|
+
const statuses = [...new Set(input.statuses ?? [])];
|
|
1222
|
+
const cursor = input.cursor ? decodeLifecycleEventCursor(input.cursor) : null;
|
|
1223
|
+
const clauses = ['le.project_id = ?'];
|
|
1224
|
+
const parameters = [input.projectId];
|
|
1225
|
+
if (input.kind) {
|
|
1226
|
+
clauses.push('er.kind = ?');
|
|
1227
|
+
parameters.push(input.kind);
|
|
1228
|
+
}
|
|
1229
|
+
if (statuses.length > 0) {
|
|
1230
|
+
clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
|
|
1231
|
+
parameters.push(...statuses);
|
|
1232
|
+
}
|
|
1233
|
+
if (cursor) {
|
|
1234
|
+
clauses.push(`(
|
|
1235
|
+
le.created_at < ?
|
|
1236
|
+
OR (le.created_at = ? AND le.execution_run_id < ?)
|
|
1237
|
+
OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence < ?)
|
|
1238
|
+
OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence = ? AND le.id < ?)
|
|
1239
|
+
)`);
|
|
1240
|
+
parameters.push(cursor.created_at, cursor.created_at, cursor.execution_run_id, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.id);
|
|
1241
|
+
}
|
|
1242
|
+
const rows = db.prepare(`
|
|
1243
|
+
SELECT le.*
|
|
1244
|
+
FROM lifecycle_events le
|
|
1245
|
+
JOIN execution_runs er ON er.id = le.execution_run_id AND er.project_id = le.project_id
|
|
1246
|
+
WHERE ${clauses.join(' AND ')}
|
|
1247
|
+
ORDER BY le.created_at DESC, le.execution_run_id DESC, le.sequence DESC, le.id DESC
|
|
1248
|
+
LIMIT ?
|
|
1249
|
+
`).all(...parameters, limit + 1);
|
|
1250
|
+
const pageRows = rows.slice(0, limit);
|
|
1251
|
+
const lastRow = pageRows.at(-1);
|
|
1252
|
+
return {
|
|
1253
|
+
events: pageRows.map(serializeEvent),
|
|
1254
|
+
next_cursor: rows.length > limit && lastRow
|
|
1255
|
+
? encodeLifecycleEventCursor({
|
|
1256
|
+
created_at: lastRow.created_at,
|
|
1257
|
+
execution_run_id: lastRow.execution_run_id,
|
|
1258
|
+
sequence: lastRow.sequence,
|
|
1259
|
+
id: lastRow.id,
|
|
1260
|
+
})
|
|
1261
|
+
: null,
|
|
1262
|
+
};
|
|
1263
|
+
};
|
|
1264
|
+
//# sourceMappingURL=executionRuns.js.map
|