@defrex/autobuild 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +10 -0
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +24 -3
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +26 -7
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/tickets/SKILL.md +57 -5
|
@@ -51,6 +51,7 @@ import { KERNEL, agentActor, type Actor } from '../events/envelope'
|
|
|
51
51
|
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
52
52
|
import type { IdSource } from '../ids'
|
|
53
53
|
import { decideNext, type Decision } from '../kernel/engine'
|
|
54
|
+
import { defineReducer } from '../kernel/incremental'
|
|
54
55
|
import { PHASE_SPECS } from '../kernel/phases'
|
|
55
56
|
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
56
57
|
import { evaluateVerifyApplicability } from '../kernel/verify-applicability'
|
|
@@ -314,22 +315,155 @@ function providerAttemptSummary(attempts: readonly ProviderAttempt[] | undefined
|
|
|
314
315
|
)
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
export interface PhaseFailureTally {
|
|
319
|
+
count: number
|
|
320
|
+
lastError?: string
|
|
321
|
+
lastWillRetry?: boolean
|
|
322
|
+
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const PHASE_FAILURES_REDUCER_VERSION = 1
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* phase.failed tally for one phase+round — D5's retry-guard input. A factory
|
|
329
|
+
* because phase and round are parameters of the projection.
|
|
330
|
+
*
|
|
331
|
+
* An answered escalation re-arms this budget only when its semantic class
|
|
332
|
+
* acknowledges phase-runner failures (§15.6-B). Runner-failure causes reset
|
|
333
|
+
* their matching round, the phase-level verify cause resets all rounds, and
|
|
334
|
+
* unrelated policy causes reset nothing. Cause-less historical and
|
|
335
|
+
* non-policy raises retain the legacy round-shaped behavior. Failures after
|
|
336
|
+
* an applicable answer count again, so a still-failing phase re-escalates on
|
|
337
|
+
* new evidence instead of deadlocking before its recovery session starts.
|
|
338
|
+
*/
|
|
339
|
+
export function phaseFailuresReducer(phase: Phase, round: number) {
|
|
340
|
+
return defineReducer<
|
|
341
|
+
{
|
|
342
|
+
/** id → the raise's reset semantics — answers are matched by id (§15.3). */
|
|
343
|
+
raised: Record<string, PhaseFailureResetEscalation>
|
|
344
|
+
count: number
|
|
345
|
+
lastError?: string
|
|
346
|
+
lastWillRetry?: boolean
|
|
347
|
+
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
348
|
+
},
|
|
349
|
+
AbEvent,
|
|
350
|
+
PhaseFailureTally
|
|
351
|
+
>({
|
|
352
|
+
version: PHASE_FAILURES_REDUCER_VERSION,
|
|
353
|
+
initial: () => ({ raised: {}, count: 0 }),
|
|
354
|
+
fold(acc, events) {
|
|
355
|
+
for (const event of events) {
|
|
356
|
+
switch (event.type) {
|
|
357
|
+
case 'escalation.raised':
|
|
358
|
+
acc.raised[event.payload.id] = {
|
|
359
|
+
phase: event.payload.phase,
|
|
360
|
+
source: event.payload.source,
|
|
361
|
+
...(event.payload.policyCause !== undefined
|
|
362
|
+
? { policyCause: event.payload.policyCause }
|
|
363
|
+
: {}),
|
|
364
|
+
...(event.payload.round !== undefined ? { round: event.payload.round } : {}),
|
|
365
|
+
}
|
|
366
|
+
break
|
|
367
|
+
case 'escalation.answered': {
|
|
368
|
+
const raise = acc.raised[event.payload.id]
|
|
369
|
+
if (raise !== undefined && resetsPhaseFailureBudget(raise, phase, round)) {
|
|
370
|
+
acc.count = 0
|
|
371
|
+
delete acc.lastError
|
|
372
|
+
delete acc.lastWillRetry
|
|
373
|
+
delete acc.lastProviderAttempts
|
|
374
|
+
}
|
|
375
|
+
break
|
|
376
|
+
}
|
|
377
|
+
case 'phase.failed':
|
|
378
|
+
if (event.payload.phase === phase && event.payload.round === round) {
|
|
379
|
+
acc.count += 1
|
|
380
|
+
acc.lastError = event.payload.error
|
|
381
|
+
acc.lastWillRetry = event.payload.willRetry
|
|
382
|
+
acc.lastProviderAttempts = event.payload.providerAttempts
|
|
383
|
+
}
|
|
384
|
+
break
|
|
385
|
+
default:
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
finish: (acc) => ({
|
|
391
|
+
count: acc.count,
|
|
392
|
+
...(acc.lastError !== undefined ? { lastError: acc.lastError } : {}),
|
|
393
|
+
...(acc.lastWillRetry !== undefined ? { lastWillRetry: acc.lastWillRetry } : {}),
|
|
394
|
+
...(acc.lastProviderAttempts !== undefined
|
|
395
|
+
? { lastProviderAttempts: acc.lastProviderAttempts }
|
|
396
|
+
: {}),
|
|
397
|
+
}),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export const SETUP_STREAK_REDUCER_VERSION = 1
|
|
402
|
+
|
|
403
|
+
/** Consecutive durable setup failures since the latest successful attachment
|
|
404
|
+
* or explicit human answer. The accumulator keeps the boundary seq and the
|
|
405
|
+
* failure seqs, so it is exact for any array order. */
|
|
406
|
+
export const setupStreakReducer = defineReducer<
|
|
407
|
+
{ escalations: string[]; boundary: number; failures: number[] },
|
|
408
|
+
AbEvent,
|
|
409
|
+
number
|
|
410
|
+
>({
|
|
411
|
+
version: SETUP_STREAK_REDUCER_VERSION,
|
|
412
|
+
initial: () => ({ escalations: [], boundary: 0, failures: [] }),
|
|
413
|
+
fold(acc, events) {
|
|
414
|
+
for (const event of events) {
|
|
415
|
+
if (event.type === 'runner.attached') acc.boundary = event.seq
|
|
416
|
+
else if (
|
|
417
|
+
event.type === 'escalation.raised' &&
|
|
418
|
+
event.payload.phase === 'setup' &&
|
|
419
|
+
event.payload.source === 'policy'
|
|
420
|
+
) {
|
|
421
|
+
if (!acc.escalations.includes(event.payload.id)) acc.escalations.push(event.payload.id)
|
|
422
|
+
} else if (
|
|
423
|
+
event.type === 'escalation.answered' &&
|
|
424
|
+
acc.escalations.includes(event.payload.id)
|
|
425
|
+
) {
|
|
426
|
+
acc.boundary = event.seq
|
|
427
|
+
} else if (event.type === 'runner.setup-failed') {
|
|
428
|
+
acc.failures.push(event.seq)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
finish: (acc) => acc.failures.filter((seq) => seq > acc.boundary).length,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
export const PUBLISHED_BRANCH_HEAD_REDUCER_VERSION = 1
|
|
436
|
+
|
|
437
|
+
/** Latest event-checkpointed branch head that kernel plumbing published;
|
|
438
|
+
* undefined while the build has none. */
|
|
439
|
+
export const publishedBranchHeadReducer = defineReducer<
|
|
440
|
+
{ head?: string },
|
|
441
|
+
AbEvent,
|
|
442
|
+
string | undefined
|
|
443
|
+
>({
|
|
444
|
+
version: PUBLISHED_BRANCH_HEAD_REDUCER_VERSION,
|
|
445
|
+
initial: () => ({}),
|
|
446
|
+
fold(acc, events) {
|
|
447
|
+
for (const event of events) {
|
|
448
|
+
if (event.type === 'implement.completed') {
|
|
449
|
+
acc.head = event.payload.commits.head
|
|
450
|
+
} else if (event.type === 'reconcile.completed') {
|
|
451
|
+
acc.head = event.payload.mergeCommit
|
|
452
|
+
} else if (
|
|
453
|
+
event.type === 'finalize.step-completed' &&
|
|
454
|
+
event.payload.ok &&
|
|
455
|
+
event.payload.headSha !== undefined
|
|
456
|
+
) {
|
|
457
|
+
acc.head = event.payload.headSha
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
finish: (acc) => acc.head,
|
|
462
|
+
})
|
|
463
|
+
|
|
317
464
|
/** Latest event-checkpointed branch head that kernel plumbing published. */
|
|
318
465
|
export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
319
|
-
|
|
320
|
-
for (const event of events) {
|
|
321
|
-
if (event.type === 'implement.completed') {
|
|
322
|
-
head = event.payload.commits.head
|
|
323
|
-
} else if (event.type === 'reconcile.completed') {
|
|
324
|
-
head = event.payload.mergeCommit
|
|
325
|
-
} else if (
|
|
326
|
-
event.type === 'finalize.step-completed' &&
|
|
327
|
-
event.payload.ok &&
|
|
328
|
-
event.payload.headSha !== undefined
|
|
329
|
-
) {
|
|
330
|
-
head = event.payload.headSha
|
|
331
|
-
}
|
|
332
|
-
}
|
|
466
|
+
const head = publishedBranchHeadReducer.reduce(events)
|
|
333
467
|
if (head === undefined) {
|
|
334
468
|
throw new Error(
|
|
335
469
|
'finalize publication requires a prior implement.completed, reconcile.completed, ' +
|
|
@@ -339,6 +473,37 @@ export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
|
339
473
|
return head
|
|
340
474
|
}
|
|
341
475
|
|
|
476
|
+
export const VERIFY_DIFF_BASE_REDUCER_VERSION = 1
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The durable tree against which a conditional verifier evaluates the
|
|
480
|
+
* branch's current HEAD, or undefined until a `workspace.provisioned` exists.
|
|
481
|
+
* The accumulator keeps the first provisioned base and the latest promoted
|
|
482
|
+
* reconcile base separately, so the result is exact for any array order (a
|
|
483
|
+
* promotion that precedes the provisioned fact in the array still wins).
|
|
484
|
+
*/
|
|
485
|
+
export const verifyDiffBaseReducer = defineReducer<
|
|
486
|
+
{ provisioned?: string; promoted?: string; pendingReconcileBase?: string },
|
|
487
|
+
AbEvent,
|
|
488
|
+
string | undefined
|
|
489
|
+
>({
|
|
490
|
+
version: VERIFY_DIFF_BASE_REDUCER_VERSION,
|
|
491
|
+
initial: () => ({}),
|
|
492
|
+
fold(acc, events) {
|
|
493
|
+
for (const event of events) {
|
|
494
|
+
if (event.type === 'workspace.provisioned') {
|
|
495
|
+
if (acc.provisioned === undefined) acc.provisioned = event.payload.base.sha
|
|
496
|
+
} else if (event.type === 'reconcile.started') {
|
|
497
|
+
acc.pendingReconcileBase = event.payload.baseSha
|
|
498
|
+
} else if (event.type === 'reconcile.completed') {
|
|
499
|
+
if (acc.pendingReconcileBase !== undefined) acc.promoted = acc.pendingReconcileBase
|
|
500
|
+
delete acc.pendingReconcileBase
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
finish: (acc) => (acc.provisioned === undefined ? undefined : (acc.promoted ?? acc.provisioned)),
|
|
505
|
+
})
|
|
506
|
+
|
|
342
507
|
/**
|
|
343
508
|
* Select the durable tree against which a conditional verifier evaluates the
|
|
344
509
|
* branch's current HEAD. The initial branch-cut SHA is authoritative until a
|
|
@@ -346,24 +511,13 @@ export function selectPublishedBranchHead(events: readonly AbEvent[]): string {
|
|
|
346
511
|
* refreshed base SHA. A dangling start is intentionally ignored.
|
|
347
512
|
*/
|
|
348
513
|
export function selectVerifyDiffBase(events: readonly AbEvent[]): string {
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
514
|
+
const base = verifyDiffBaseReducer.reduce(events)
|
|
515
|
+
if (base === undefined) {
|
|
351
516
|
throw new Error(
|
|
352
517
|
'conditional verify requires a workspace.provisioned base SHA, but this build has none',
|
|
353
518
|
)
|
|
354
519
|
}
|
|
355
|
-
|
|
356
|
-
let baseSha = provisioned.payload.base.sha
|
|
357
|
-
let pendingReconcileBase: string | undefined
|
|
358
|
-
for (const event of events) {
|
|
359
|
-
if (event.type === 'reconcile.started') {
|
|
360
|
-
pendingReconcileBase = event.payload.baseSha
|
|
361
|
-
} else if (event.type === 'reconcile.completed') {
|
|
362
|
-
if (pendingReconcileBase !== undefined) baseSha = pendingReconcileBase
|
|
363
|
-
pendingReconcileBase = undefined
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return baseSha
|
|
520
|
+
return base
|
|
367
521
|
}
|
|
368
522
|
|
|
369
523
|
/** Parse `git diff --name-only -z`; malformed output fails closed. */
|
|
@@ -483,22 +637,7 @@ export class BuildRunner {
|
|
|
483
637
|
* or explicit human answer. The answer re-arms the budget but deliberately
|
|
484
638
|
* does not erase the current diagnostic while recovery is still running. */
|
|
485
639
|
private setupStreak(events: readonly AbEvent[]): number {
|
|
486
|
-
|
|
487
|
-
let boundary = 0
|
|
488
|
-
for (const event of events) {
|
|
489
|
-
if (event.type === 'runner.attached') boundary = event.seq
|
|
490
|
-
else if (
|
|
491
|
-
event.type === 'escalation.raised' &&
|
|
492
|
-
event.payload.phase === 'setup' &&
|
|
493
|
-
event.payload.source === 'policy'
|
|
494
|
-
) {
|
|
495
|
-
setupEscalations.add(event.payload.id)
|
|
496
|
-
} else if (event.type === 'escalation.answered' && setupEscalations.has(event.payload.id)) {
|
|
497
|
-
boundary = event.seq
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return events.filter((event) => event.type === 'runner.setup-failed' && event.seq > boundary)
|
|
501
|
-
.length
|
|
640
|
+
return setupStreakReducer.reduce(events)
|
|
502
641
|
}
|
|
503
642
|
|
|
504
643
|
private setupQuestion(failure: EventPayload<'runner.setup-failed'>): string {
|
|
@@ -2452,52 +2591,7 @@ export class BuildRunner {
|
|
|
2452
2591
|
lastWillRetry?: boolean
|
|
2453
2592
|
lastProviderAttempts?: readonly ProviderAttempt[]
|
|
2454
2593
|
} {
|
|
2455
|
-
|
|
2456
|
-
const raised = new Map<string, PhaseFailureResetEscalation>()
|
|
2457
|
-
let count = 0
|
|
2458
|
-
let lastError: string | undefined
|
|
2459
|
-
let lastWillRetry: boolean | undefined
|
|
2460
|
-
let lastProviderAttempts: readonly ProviderAttempt[] | undefined
|
|
2461
|
-
for (const event of events) {
|
|
2462
|
-
switch (event.type) {
|
|
2463
|
-
case 'escalation.raised':
|
|
2464
|
-
raised.set(event.payload.id, {
|
|
2465
|
-
phase: event.payload.phase,
|
|
2466
|
-
source: event.payload.source,
|
|
2467
|
-
...(event.payload.policyCause !== undefined
|
|
2468
|
-
? { policyCause: event.payload.policyCause }
|
|
2469
|
-
: {}),
|
|
2470
|
-
...(event.payload.round !== undefined ? { round: event.payload.round } : {}),
|
|
2471
|
-
})
|
|
2472
|
-
break
|
|
2473
|
-
case 'escalation.answered': {
|
|
2474
|
-
const raise = raised.get(event.payload.id)
|
|
2475
|
-
if (raise !== undefined && resetsPhaseFailureBudget(raise, phase, round)) {
|
|
2476
|
-
count = 0
|
|
2477
|
-
lastError = undefined
|
|
2478
|
-
lastWillRetry = undefined
|
|
2479
|
-
lastProviderAttempts = undefined
|
|
2480
|
-
}
|
|
2481
|
-
break
|
|
2482
|
-
}
|
|
2483
|
-
case 'phase.failed':
|
|
2484
|
-
if (event.payload.phase === phase && event.payload.round === round) {
|
|
2485
|
-
count += 1
|
|
2486
|
-
lastError = event.payload.error
|
|
2487
|
-
lastWillRetry = event.payload.willRetry
|
|
2488
|
-
lastProviderAttempts = event.payload.providerAttempts
|
|
2489
|
-
}
|
|
2490
|
-
break
|
|
2491
|
-
default:
|
|
2492
|
-
break
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
return {
|
|
2496
|
-
count,
|
|
2497
|
-
...(lastError !== undefined ? { lastError } : {}),
|
|
2498
|
-
...(lastWillRetry !== undefined ? { lastWillRetry } : {}),
|
|
2499
|
-
...(lastProviderAttempts !== undefined ? { lastProviderAttempts } : {}),
|
|
2500
|
-
}
|
|
2594
|
+
return phaseFailuresReducer(phase, round).reduce(events)
|
|
2501
2595
|
}
|
|
2502
2596
|
|
|
2503
2597
|
private async failPhase(
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure event-log projections the dispatcher and janitor read, extracted from
|
|
3
|
+
* `dispatcher.ts` so each is an incremental reducer (see
|
|
4
|
+
* `kernel/incremental.ts`). The dispatcher's private helpers delegate here.
|
|
5
|
+
*/
|
|
6
|
+
import type { AbEvent } from '../events/catalog'
|
|
7
|
+
import type { EventPayload } from '../events/payloads'
|
|
8
|
+
import { defineReducer } from '../kernel/incremental'
|
|
9
|
+
|
|
10
|
+
export type OpenBuildWorkspace = EventPayload<'workspace.provisioned'>
|
|
11
|
+
|
|
12
|
+
export const OPEN_BUILD_WORKSPACE_REDUCER_VERSION = 1
|
|
13
|
+
|
|
14
|
+
/** Latest `workspace.provisioned` not followed by a `workspace.released` —
|
|
15
|
+
* the reducer deliberately ignores workspace events (liveness is the
|
|
16
|
+
* dispatcher's concern), so the janitor scans the raw log. */
|
|
17
|
+
export const openBuildWorkspaceReducer = defineReducer<
|
|
18
|
+
{ open: OpenBuildWorkspace | null },
|
|
19
|
+
AbEvent,
|
|
20
|
+
OpenBuildWorkspace | null
|
|
21
|
+
>({
|
|
22
|
+
version: OPEN_BUILD_WORKSPACE_REDUCER_VERSION,
|
|
23
|
+
initial: () => ({ open: null }),
|
|
24
|
+
fold(acc, events) {
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event.type === 'workspace.provisioned') acc.open = event.payload
|
|
27
|
+
else if (event.type === 'workspace.released') acc.open = null
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
finish: (acc) => (acc.open === null ? null : { ...acc.open }),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export function openBuildWorkspace(events: readonly AbEvent[]): OpenBuildWorkspace | null {
|
|
34
|
+
return openBuildWorkspaceReducer.reduce(events)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const BASE_BRANCH_REDUCER_VERSION = 1
|
|
38
|
+
|
|
39
|
+
/** The base branch recorded on the FIRST `build.created` in array order, or
|
|
40
|
+
* undefined for logs missing one (callers fall back to the repo config). */
|
|
41
|
+
export const baseBranchReducer = defineReducer<
|
|
42
|
+
{ baseBranch?: string },
|
|
43
|
+
AbEvent,
|
|
44
|
+
string | undefined
|
|
45
|
+
>({
|
|
46
|
+
version: BASE_BRANCH_REDUCER_VERSION,
|
|
47
|
+
initial: () => ({}),
|
|
48
|
+
fold(acc, events) {
|
|
49
|
+
if (acc.baseBranch !== undefined) return
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
if (event.type === 'build.created') {
|
|
52
|
+
acc.baseBranch = event.payload.baseBranch
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
finish: (acc) => acc.baseBranch,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export interface ProvisionMarker {
|
|
61
|
+
provider: string
|
|
62
|
+
branch: string
|
|
63
|
+
generation: number
|
|
64
|
+
ts: string
|
|
65
|
+
seq: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const PROVISION_MARKER_REDUCER_VERSION = 1
|
|
69
|
+
|
|
70
|
+
/** The latest `workspace.provision-started` not yet followed by a
|
|
71
|
+
* provisioned/released fact — an open provisioning marker. */
|
|
72
|
+
export const provisionMarkerReducer = defineReducer<
|
|
73
|
+
{ marker: ProvisionMarker | null },
|
|
74
|
+
AbEvent,
|
|
75
|
+
ProvisionMarker | undefined
|
|
76
|
+
>({
|
|
77
|
+
version: PROVISION_MARKER_REDUCER_VERSION,
|
|
78
|
+
initial: () => ({ marker: null }),
|
|
79
|
+
fold(acc, events) {
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (event.type === 'workspace.provision-started') {
|
|
82
|
+
acc.marker = { ...event.payload, ts: event.ts, seq: event.seq }
|
|
83
|
+
} else if (
|
|
84
|
+
acc.marker !== null &&
|
|
85
|
+
(event.type === 'workspace.provisioned' || event.type === 'workspace.released')
|
|
86
|
+
) {
|
|
87
|
+
acc.marker = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
finish: (acc) => (acc.marker === null ? undefined : { ...acc.marker }),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const RECOVERY_CHECKPOINT_REDUCER_VERSION = 1
|
|
95
|
+
|
|
96
|
+
/** The commit a recovered workspace is re-cut from: the latest settled
|
|
97
|
+
* publication head, else the first provisioned base. */
|
|
98
|
+
export const recoveryCheckpointReducer = defineReducer<
|
|
99
|
+
{ settled?: string; original?: string },
|
|
100
|
+
AbEvent,
|
|
101
|
+
string | undefined
|
|
102
|
+
>({
|
|
103
|
+
version: RECOVERY_CHECKPOINT_REDUCER_VERSION,
|
|
104
|
+
initial: () => ({}),
|
|
105
|
+
fold(acc, events) {
|
|
106
|
+
for (const event of events) {
|
|
107
|
+
if (event.type === 'workspace.provisioned' && acc.original === undefined) {
|
|
108
|
+
acc.original = event.payload.base.sha
|
|
109
|
+
} else if (event.type === 'implement.completed') {
|
|
110
|
+
acc.settled = event.payload.commits.head
|
|
111
|
+
} else if (event.type === 'reconcile.completed') {
|
|
112
|
+
acc.settled = event.payload.mergeCommit
|
|
113
|
+
} else if (
|
|
114
|
+
event.type === 'finalize.step-completed' &&
|
|
115
|
+
event.payload.ok &&
|
|
116
|
+
event.payload.headSha !== undefined
|
|
117
|
+
) {
|
|
118
|
+
acc.settled = event.payload.headSha
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
finish: (acc) => acc.settled ?? acc.original,
|
|
123
|
+
})
|