@sixb/sqlite 0.1.17 → 0.1.19

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.
@@ -5,31 +5,21 @@ import {
5
5
  serializeActionRunFailure,
6
6
  } from "@sixb/core/internal/action-run-storage"
7
7
  import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider"
8
- import {
9
- actionRunPhaseRecordsEqual,
10
- canRequeueActionRunAfterEnqueueFailure,
11
- finishActionRunPhase,
12
- } from "@sixb/core/internal/storage"
8
+ import { normalizeRecordActionRunInput, resolveActionRunEffects } from "@sixb/core/internal/storage"
13
9
  import type {
14
10
  ActionRunEffectsRecord,
15
- ActionRunFailure,
16
- ActionRunParams,
17
11
  ActionRunPhase,
18
12
  ActionRunRecord,
13
+ ActionRunStatus,
19
14
  ActionRunStorage,
20
15
  ActionRunWritebackRecord,
21
- EnterActionRunPhaseInput,
22
16
  ExecutionStorage,
23
- FinishActionRunInput,
24
17
  ListActionRunsInput,
25
18
  ListActionRunsResult,
26
- LockActionMaterializationRunInput,
27
- QueueActionRunInput,
28
19
  RecordActionEffectsInput,
29
- RecordActionWritebackInput,
30
- StartActionRunInput,
20
+ RecordActionRunInput,
31
21
  } from "@sixb/core/storage"
32
- import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage"
22
+ import { ActionRunError } from "@sixb/core/storage"
33
23
  import { installFreshSqliteSchema } from "./migrations"
34
24
  import {
35
25
  closeSqliteStoreConnection,
@@ -62,293 +52,84 @@ export class SqliteActionRunStorage implements ActionRunStorage {
62
52
  }
63
53
  }
64
54
 
65
- async lockForMaterialization(input: LockActionMaterializationRunInput): Promise<ActionRunRecord> {
66
- const row = this.db
67
- .query(`SELECT action_id, status FROM action_runs WHERE project_id = ? AND id = ?`)
68
- .get(input.projectId, input.runId) as {
69
- readonly action_id: string
70
- readonly status: string
71
- } | null
72
- if (!row) {
73
- throw new ActionRunError(
74
- `[SixbSqlite] Action run '${input.runId}' not found for project '${input.projectId}'.`
75
- )
76
- }
77
- if (row.action_id !== input.actionId) {
78
- throw new ActionRunError(
79
- `[SixbSqlite] Action run '${input.runId}' does not belong to action '${input.actionId}'.`
80
- )
81
- }
82
- if (row.status !== "running") {
83
- throw new ActionRunError(
84
- `[SixbSqlite] Action run '${input.runId}' cannot materialize from status '${row.status}'.`
85
- )
86
- }
87
- const record = await this.getById({ projectId: input.projectId, id: input.runId })
88
- if (!record) {
89
- throw new ActionRunError(
90
- `[SixbSqlite] Action run '${input.runId}' disappeared while locking materialization.`
91
- )
92
- }
93
- return record
94
- }
95
-
96
- async queue(input: QueueActionRunInput): Promise<ActionRunRecord> {
97
- const queuedAt = input.queuedAt ?? new Date()
55
+ async record(input: RecordActionRunInput): Promise<ActionRunRecord> {
56
+ const record = normalizeRecordActionRunInput(input)
98
57
  await assertActionRunExecution({
99
58
  executions: this.executions,
100
- projectId: input.projectId,
101
- executionId: input.executionId,
102
- runId: input.id,
103
- actionId: input.actionId,
59
+ projectId: record.projectId,
60
+ executionId: record.executionId,
61
+ runId: record.id,
62
+ actionId: record.actionId,
104
63
  })
105
64
 
106
- try {
107
- this.db
108
- .query(
109
- `
110
- INSERT INTO action_runs (
111
- project_id,
112
- id,
113
- execution_id,
114
- action_id,
115
- subject_kind,
116
- object_type_id,
117
- primary_id,
118
- status,
119
- phase,
120
- queued_at,
121
- started_at,
122
- finished_at,
123
- params,
124
- idempotency_key,
125
- writeback_status,
126
- writeback_completed_at,
127
- writeback_result,
128
- writeback_error,
129
- effects_status,
130
- effects_completed_at,
131
- effects_error,
132
- error
133
- ) VALUES (
134
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?,
135
- NULL, NULL, NULL, NULL,
136
- NULL, NULL, NULL,
137
- NULL
138
- )
65
+ const inserted = this.db
66
+ .query(
139
67
  `
140
- )
141
- .run(
142
- input.projectId,
143
- input.id,
144
- input.executionId,
145
- input.actionId,
146
- input.subject.kind,
147
- input.subject.kind === "object" ? input.subject.objectTypeId : null,
148
- input.subject.kind === "object" ? input.subject.primaryId : null,
149
- "queued",
150
- "request",
151
- queuedAt.toISOString(),
152
- JSON.stringify(input.params),
153
- input.idempotencyKey
154
- )
155
- } catch (error) {
156
- if (isUniqueConstraintError(error)) {
157
- return this.requeueAfterEnqueueFailure(input, queuedAt)
158
- }
159
-
160
- throw error
161
- }
68
+ INSERT INTO action_runs (
69
+ project_id,
70
+ id,
71
+ execution_id,
72
+ action_id,
73
+ subject_kind,
74
+ object_type_id,
75
+ primary_id,
76
+ status,
77
+ phase,
78
+ started_at,
79
+ finished_at,
80
+ params,
81
+ idempotency_key,
82
+ writeback_status,
83
+ writeback_completed_at,
84
+ writeback_result,
85
+ writeback_error,
86
+ error
87
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
+ ON CONFLICT DO NOTHING
89
+ `
90
+ )
91
+ .run(
92
+ record.projectId,
93
+ record.id,
94
+ record.executionId,
95
+ record.actionId,
96
+ record.subject.kind,
97
+ record.subject.kind === "object" ? record.subject.objectTypeId : null,
98
+ record.subject.kind === "object" ? record.subject.primaryId : null,
99
+ record.status,
100
+ record.phase,
101
+ record.startedAt.toISOString(),
102
+ record.finishedAt.toISOString(),
103
+ JSON.stringify(record.params),
104
+ record.idempotencyKey,
105
+ record.writeback?.status ?? null,
106
+ record.writeback?.completedAt.toISOString() ?? null,
107
+ record.writeback?.status === "succeeded" ? JSON.stringify(record.writeback.result) : null,
108
+ record.writeback?.status === "failed"
109
+ ? serializeActionRunFailure(record.writeback.error, "writeback")
110
+ : null,
111
+ record.status === "failed" ? serializeActionRunFailure(record.error) : null
112
+ )
162
113
 
163
- const record = await this.getById({ projectId: input.projectId, id: input.id })
164
- if (!record) {
114
+ if (inserted.changes === 0) {
115
+ const conflict = this.selectRow(record.projectId, record.id)
165
116
  throw new ActionRunError(
166
- `[SixbSqlite] Failed to load action run '${input.id}' for project '${input.projectId}'.`
117
+ conflict
118
+ ? `[SixbSqlite] Action run '${record.id}' is already recorded for project '${record.projectId}'.`
119
+ : `[SixbSqlite] Execution '${record.executionId}' already belongs to another Action run.`
167
120
  )
168
121
  }
169
-
170
- return record
171
- }
172
-
173
- private async requeueAfterEnqueueFailure(
174
- input: QueueActionRunInput,
175
- queuedAt: Date
176
- ): Promise<ActionRunRecord> {
177
- return this.db.transaction(() => {
178
- const existing = this.db
179
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
180
- .get(input.projectId, input.id) as DatabaseRow | null
181
-
182
- if (
183
- !existing ||
184
- !canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)
185
- ) {
186
- throw new ActionRunError(
187
- `[SixbSqlite] Action run '${input.id}' already exists for project '${input.projectId}'.`
188
- )
189
- }
190
-
191
- this.db
192
- .query(
193
- `
194
- UPDATE action_runs
195
- SET
196
- status = ?,
197
- phase = ?,
198
- queued_at = ?,
199
- started_at = NULL,
200
- finished_at = NULL,
201
- writeback_status = NULL,
202
- writeback_completed_at = NULL,
203
- writeback_result = NULL,
204
- writeback_error = NULL,
205
- effects_status = NULL,
206
- effects_completed_at = NULL,
207
- effects_error = NULL,
208
- error = NULL
209
- WHERE project_id = ? AND id = ?
210
- `
211
- )
212
- .run("queued", "request", queuedAt.toISOString(), input.projectId, input.id)
213
-
214
- const updated = this.db
215
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
216
- .get(input.projectId, input.id) as DatabaseRow
217
-
218
- return rowToActionRunRecord(updated)
219
- })()
220
- }
221
-
222
- async start(input: StartActionRunInput): Promise<ActionRunRecord> {
223
- return this.db.transaction(() => {
224
- const existing = this.db
225
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
226
- .get(input.projectId, input.id) as DatabaseRow | null
227
-
228
- if (!existing) {
229
- throw new ActionRunError(
230
- `[SixbSqlite] Action run '${input.id}' not found for project '${input.projectId}'.`
231
- )
232
- }
233
-
234
- if (existing.status !== "queued") {
235
- throw new ActionRunError(
236
- `[SixbSqlite] Action run '${input.id}' cannot start from status '${existing.status}'.`
237
- )
238
- }
239
-
240
- this.db
241
- .query(
242
- `
243
- UPDATE action_runs
244
- SET
245
- status = ?,
246
- phase = ?,
247
- started_at = ?,
248
- error = NULL
249
- WHERE project_id = ? AND id = ?
250
- `
251
- )
252
- .run(
253
- "running",
254
- input.phase ?? "validation",
255
- (input.startedAt ?? new Date()).toISOString(),
256
- input.projectId,
257
- input.id
258
- )
259
-
260
- const updated = this.db
261
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
262
- .get(input.projectId, input.id) as DatabaseRow
263
-
264
- return rowToActionRunRecord(updated)
265
- })()
266
- }
267
-
268
- async enterPhase(input: EnterActionRunPhaseInput): Promise<ActionRunRecord> {
269
- return this.db.transaction(() => {
270
- this.requireRunningRun(input.projectId, input.id, "transition phase")
271
-
272
- this.db
273
- .query(
274
- `
275
- UPDATE action_runs
276
- SET phase = ?
277
- WHERE project_id = ? AND id = ?
278
- `
279
- )
280
- .run(input.phase, input.projectId, input.id)
281
-
282
- const updated = this.db
283
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
284
- .get(input.projectId, input.id) as DatabaseRow
285
-
286
- return rowToActionRunRecord(updated)
287
- })()
288
- }
289
-
290
- async recordWriteback(input: RecordActionWritebackInput): Promise<ActionRunRecord> {
291
- return this.db.transaction(() => {
292
- const existing = this.requireRunningRun(input.projectId, input.id, "record writeback")
293
- const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date()))
294
- const currentWriteback = toActionRunWritebackRecord(existing)
295
-
296
- if (currentWriteback) {
297
- if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
298
- return rowToActionRunRecord(existing)
299
- }
300
-
301
- throw new ActionRunError(
302
- `[SixbSqlite] Action run '${input.id}' already has a different writeback record.`
303
- )
304
- }
305
-
306
- this.db
307
- .query(
308
- `
309
- UPDATE action_runs
310
- SET
311
- phase = ?,
312
- writeback_status = ?,
313
- writeback_completed_at = ?,
314
- writeback_result = ?,
315
- writeback_error = ?
316
- WHERE project_id = ? AND id = ?
317
- `
318
- )
319
- .run(
320
- "writeback",
321
- input.status,
322
- nextWriteback.completedAt.toISOString(),
323
- input.status === "succeeded" ? serializeJsonValue(input.result) : null,
324
- input.status === "failed" ? serializeActionRunFailure(input.error, "writeback") : null,
325
- input.projectId,
326
- input.id
327
- )
328
-
329
- const updated = this.db
330
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
331
- .get(input.projectId, input.id) as DatabaseRow
332
-
333
- return rowToActionRunRecord(updated)
334
- })()
122
+ return rowToActionRunRecord(this.requireRow(record.projectId, record.id))
335
123
  }
336
124
 
337
125
  async recordEffects(input: RecordActionEffectsInput): Promise<ActionRunRecord> {
338
126
  return this.db.transaction(() => {
339
- const existing = this.requireRunningRun(input.projectId, input.id, "record effects")
340
- const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date()))
341
- const currentEffects = toActionRunEffectsRecord(existing)
342
-
343
- if (currentEffects) {
344
- if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
345
- return rowToActionRunRecord(existing)
346
- }
347
-
348
- throw new ActionRunError(
349
- `[SixbSqlite] Action run '${input.id}' already has a different effects record.`
350
- )
351
- }
127
+ const existing = this.selectRow(input.projectId, input.id)
128
+ const { run, effects } = resolveActionRunEffects(
129
+ existing ? rowToActionRunRecord(existing) : null,
130
+ input
131
+ )
132
+ if (!effects) return run
352
133
 
353
134
  this.db
354
135
  .query(
@@ -364,67 +145,13 @@ export class SqliteActionRunStorage implements ActionRunStorage {
364
145
  )
365
146
  .run(
366
147
  "effects",
367
- input.status,
368
- nextEffects.completedAt.toISOString(),
369
- input.status === "failed" ? serializeActionRunFailure(input.error, "effects") : null,
148
+ effects.status,
149
+ effects.completedAt.toISOString(),
150
+ effects.status === "failed" ? serializeActionRunFailure(effects.error, "effects") : null,
370
151
  input.projectId,
371
152
  input.id
372
153
  )
373
-
374
- const updated = this.db
375
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
376
- .get(input.projectId, input.id) as DatabaseRow
377
-
378
- return rowToActionRunRecord(updated)
379
- })()
380
- }
381
-
382
- async finish(input: FinishActionRunInput): Promise<ActionRunRecord> {
383
- return this.db.transaction(() => {
384
- const existing = this.db
385
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
386
- .get(input.projectId, input.id) as DatabaseRow | null
387
-
388
- if (!existing) {
389
- throw new ActionRunError(
390
- `[SixbSqlite] Action run '${input.id}' not found for project '${input.projectId}'.`
391
- )
392
- }
393
-
394
- if (isTerminalActionRun({ status: existing.status })) {
395
- throw new ActionRunError(
396
- `[SixbSqlite] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`
397
- )
398
- }
399
-
400
- const phase = finishActionRunPhase(input, existing.phase)
401
-
402
- this.db
403
- .query(
404
- `
405
- UPDATE action_runs
406
- SET
407
- status = ?,
408
- phase = ?,
409
- finished_at = ?,
410
- error = ?
411
- WHERE project_id = ? AND id = ?
412
- `
413
- )
414
- .run(
415
- input.status,
416
- phase,
417
- (input.finishedAt ?? new Date()).toISOString(),
418
- input.status === "succeeded" ? null : serializeActionRunFailure(input.error),
419
- input.projectId,
420
- input.id
421
- )
422
-
423
- const updated = this.db
424
- .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
425
- .get(input.projectId, input.id) as DatabaseRow
426
-
427
- return rowToActionRunRecord(updated)
154
+ return rowToActionRunRecord(this.requireRow(input.projectId, input.id))
428
155
  })()
429
156
  }
430
157
 
@@ -503,12 +230,12 @@ export class SqliteActionRunStorage implements ActionRunStorage {
503
230
  }
504
231
 
505
232
  if (input.startedAfter) {
506
- whereClauses.push("COALESCE(started_at, queued_at) >= ?")
233
+ whereClauses.push("started_at >= ?")
507
234
  args.push(input.startedAfter.toISOString())
508
235
  }
509
236
 
510
237
  if (input.startedBefore) {
511
- whereClauses.push("COALESCE(started_at, queued_at) <= ?")
238
+ whereClauses.push("started_at <= ?")
512
239
  args.push(input.startedBefore.toISOString())
513
240
  }
514
241
 
@@ -524,7 +251,7 @@ export class SqliteActionRunStorage implements ActionRunStorage {
524
251
  let query = `
525
252
  SELECT * FROM action_runs
526
253
  ${where}
527
- ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
254
+ ORDER BY started_at ${order}, id ${order}
528
255
  `
529
256
  const queryArgs = [...args]
530
257
 
@@ -550,51 +277,29 @@ export class SqliteActionRunStorage implements ActionRunStorage {
550
277
  closeSqliteStoreConnection(this.connection)
551
278
  }
552
279
 
553
- private requireRunningRun(projectId: string, id: string, operation: string): DatabaseRow {
554
- const existing = this.db
280
+ private selectRow(projectId: string, id: string): DatabaseRow | null {
281
+ return this.db
555
282
  .query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
556
283
  .get(projectId, id) as DatabaseRow | null
284
+ }
557
285
 
558
- if (!existing) {
559
- throw new ActionRunError(
560
- `[SixbSqlite] Action run '${id}' not found for project '${projectId}'.`
561
- )
562
- }
563
-
564
- if (existing.status !== "running") {
286
+ private requireRow(projectId: string, id: string): DatabaseRow {
287
+ const row = this.selectRow(projectId, id)
288
+ if (!row) {
565
289
  throw new ActionRunError(
566
- `[SixbSqlite] Action run '${id}' cannot ${operation} from status '${existing.status}'.`
290
+ `[SixbSqlite] Action run '${id}' disappeared from project '${projectId}' while recording it.`
567
291
  )
568
292
  }
569
-
570
- return existing
293
+ return row
571
294
  }
572
295
  }
573
296
 
574
- function serializeJsonValue(value: JsonValue): string {
575
- return JSON.stringify(value)
576
- }
577
-
578
- function toActionRunFailure(row: DatabaseRow): ActionRunFailure | undefined {
579
- return row.error === null ? undefined : parseActionRunFailure(row.error)
580
- }
581
-
582
- function parsePhaseFailure<TPhase extends Extract<ActionRunPhase, "writeback" | "effects">>(
583
- value: string | null,
584
- expectedPhase: TPhase
585
- ): ActionRunFailure<TPhase> {
586
- return parseActionRunFailure(value, expectedPhase)
587
- }
588
-
589
297
  function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord | undefined {
590
298
  if (!row.writeback_status) {
591
299
  return undefined
592
300
  }
593
301
 
594
- const completedAt = row.writeback_completed_at
595
- ? new Date(row.writeback_completed_at)
596
- : new Date(row.queued_at)
597
-
302
+ const completedAt = new Date(row.writeback_completed_at ?? row.finished_at)
598
303
  if (row.writeback_status === "succeeded") {
599
304
  return {
600
305
  status: "succeeded",
@@ -607,7 +312,7 @@ function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord
607
312
  return {
608
313
  status: "failed",
609
314
  completedAt,
610
- error: parsePhaseFailure(row.writeback_error, "writeback"),
315
+ error: parseActionRunFailure(row.writeback_error, "writeback"),
611
316
  }
612
317
  }
613
318
 
@@ -616,10 +321,7 @@ function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | un
616
321
  return undefined
617
322
  }
618
323
 
619
- const completedAt = row.effects_completed_at
620
- ? new Date(row.effects_completed_at)
621
- : new Date(row.queued_at)
622
-
324
+ const completedAt = new Date(row.effects_completed_at ?? row.finished_at)
623
325
  if (row.effects_status === "succeeded") {
624
326
  return {
625
327
  status: "succeeded",
@@ -630,65 +332,30 @@ function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | un
630
332
  return {
631
333
  status: "failed",
632
334
  completedAt,
633
- error: parsePhaseFailure(row.effects_error, "effects"),
634
- }
635
- }
636
-
637
- function toWritebackRecord(
638
- input: RecordActionWritebackInput,
639
- completedAt: Date
640
- ): ActionRunWritebackRecord {
641
- if (input.status === "succeeded") {
642
- return {
643
- status: "succeeded",
644
- completedAt,
645
- result: input.result,
646
- }
647
- }
648
-
649
- return {
650
- status: "failed",
651
- completedAt,
652
- error: input.error,
653
- }
654
- }
655
-
656
- function toEffectsRecord(
657
- input: RecordActionEffectsInput,
658
- completedAt: Date
659
- ): ActionRunEffectsRecord {
660
- if (input.status === "succeeded") {
661
- return {
662
- status: "succeeded",
663
- completedAt,
664
- }
665
- }
666
-
667
- return {
668
- status: "failed",
669
- completedAt,
670
- error: input.error,
335
+ error: parseActionRunFailure(row.effects_error, "effects"),
671
336
  }
672
337
  }
673
338
 
674
339
  function rowToActionRunRecord(row: DatabaseRow): ActionRunRecord {
675
- return {
340
+ const writeback = toActionRunWritebackRecord(row)
341
+ const effects = toActionRunEffectsRecord(row)
342
+ const fields = {
676
343
  id: row.id,
677
344
  projectId: row.project_id,
678
345
  executionId: row.execution_id,
679
346
  actionId: row.action_id,
680
347
  subject: rowToActionSubject(row),
681
- status: row.status,
682
- phase: row.phase ?? undefined,
683
- queuedAt: new Date(row.queued_at),
684
- startedAt: row.started_at ? new Date(row.started_at) : undefined,
685
- finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
686
- params: JSON.parse(row.params) as ActionRunParams,
348
+ phase: row.phase,
349
+ startedAt: new Date(row.started_at),
350
+ finishedAt: new Date(row.finished_at),
351
+ params: JSON.parse(row.params) as ActionRunRecord["params"],
687
352
  idempotencyKey: row.idempotency_key,
688
- writeback: toActionRunWritebackRecord(row),
689
- effects: toActionRunEffectsRecord(row),
690
- error: toActionRunFailure(row),
353
+ ...(writeback === undefined ? {} : { writeback }),
354
+ ...(effects === undefined ? {} : { effects }),
691
355
  }
356
+
357
+ if (row.status === "succeeded") return { ...fields, status: "succeeded" }
358
+ return { ...fields, status: "failed", error: parseActionRunFailure(row.error) }
692
359
  }
693
360
 
694
361
  function rowToActionSubject(row: DatabaseRow): ActionSubject {
@@ -707,10 +374,6 @@ function rowToActionSubject(row: DatabaseRow): ActionSubject {
707
374
  }
708
375
  }
709
376
 
710
- function isUniqueConstraintError(error: unknown): boolean {
711
- return error instanceof Error && error.message.includes("UNIQUE constraint failed")
712
- }
713
-
714
377
  interface DatabaseRow {
715
378
  project_id: string
716
379
  id: string
@@ -719,11 +382,10 @@ interface DatabaseRow {
719
382
  subject_kind: ActionSubject["kind"]
720
383
  object_type_id: string | null
721
384
  primary_id: string | null
722
- status: ActionRunRecord["status"]
723
- phase: ActionRunPhase | null
724
- queued_at: string
725
- started_at: string | null
726
- finished_at: string | null
385
+ status: ActionRunStatus
386
+ phase: ActionRunPhase
387
+ started_at: string
388
+ finished_at: string
727
389
  params: string
728
390
  idempotency_key: string
729
391
  writeback_status: ActionRunWritebackRecord["status"] | null