@highstate/backend 0.19.1 → 0.21.1

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.
Files changed (134) hide show
  1. package/dist/chunk-b05q6fm2.js +37 -0
  2. package/dist/{chunk-V2NILDHS.js → chunk-gxjwa93h.js} +704 -604
  3. package/dist/{chunk-X2WG3WGL.js → chunk-vzdz6chj.js} +18 -15
  4. package/dist/highstate.manifest.json +4 -4
  5. package/dist/index.js +7350 -3514
  6. package/dist/library/package-resolution-worker.js +121 -10
  7. package/dist/library/worker/main.js +31 -17
  8. package/dist/shared/index.js +254 -4
  9. package/package.json +19 -20
  10. package/prisma/backend/_schema/object.prisma +12 -0
  11. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  12. package/prisma/project/artifact.prisma +3 -0
  13. package/prisma/project/entity.prisma +125 -0
  14. package/prisma/project/instance.prisma +6 -0
  15. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  16. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  17. package/prisma/project/operation.prisma +3 -0
  18. package/src/artifact/factory.ts +3 -2
  19. package/src/business/artifact.test.ts +22 -2
  20. package/src/business/artifact.ts +7 -1
  21. package/src/business/entity-snapshot.test.ts +684 -0
  22. package/src/business/entity-snapshot.ts +904 -0
  23. package/src/business/evaluation.test.ts +56 -0
  24. package/src/business/evaluation.ts +102 -22
  25. package/src/business/global-search.test.ts +344 -0
  26. package/src/business/global-search.ts +902 -0
  27. package/src/business/index.ts +4 -0
  28. package/src/business/instance-lock.ts +58 -74
  29. package/src/business/instance-state.test.ts +15 -1
  30. package/src/business/instance-state.ts +37 -14
  31. package/src/business/object-ref-index.test.ts +140 -0
  32. package/src/business/object-ref-index.ts +193 -0
  33. package/src/business/operation.test.ts +15 -1
  34. package/src/business/operation.ts +4 -0
  35. package/src/business/project-model.ts +154 -13
  36. package/src/business/project-unlock.ts +25 -2
  37. package/src/business/project.ts +9 -0
  38. package/src/business/secret.test.ts +35 -2
  39. package/src/business/secret.ts +32 -9
  40. package/src/business/settings.ts +761 -0
  41. package/src/business/unit-output.test.ts +477 -0
  42. package/src/business/unit-output.ts +461 -0
  43. package/src/business/worker.ts +55 -4
  44. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  45. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  47. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  49. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  50. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  51. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  52. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  54. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  56. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  57. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  58. package/src/database/_generated/project/browser.ts +23 -0
  59. package/src/database/_generated/project/client.ts +23 -0
  60. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  61. package/src/database/_generated/project/enums.ts +8 -0
  62. package/src/database/_generated/project/internal/class.ts +53 -5
  63. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  64. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  65. package/src/database/_generated/project/models/Artifact.ts +199 -11
  66. package/src/database/_generated/project/models/Entity.ts +1274 -0
  67. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  68. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  69. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  70. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  71. package/src/database/_generated/project/models/Operation.ts +148 -3
  72. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  73. package/src/database/_generated/project/models.ts +4 -0
  74. package/src/database/migration.ts +3 -0
  75. package/src/library/find-package-json.test.ts +77 -0
  76. package/src/library/find-package-json.ts +149 -0
  77. package/src/library/package-resolution-worker.ts +7 -3
  78. package/src/library/worker/evaluator.ts +7 -1
  79. package/src/orchestrator/manager.ts +7 -0
  80. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  81. package/src/orchestrator/operation-context.ts +154 -16
  82. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  83. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  84. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  85. package/src/orchestrator/operation-plan.md +4 -1
  86. package/src/orchestrator/operation-plan.ts +286 -92
  87. package/src/orchestrator/operation-plan.update.test.md +286 -11
  88. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  89. package/src/orchestrator/operation-workset.ts +72 -22
  90. package/src/orchestrator/operation.cancel.test.ts +4 -0
  91. package/src/orchestrator/operation.composite.test.ts +341 -0
  92. package/src/orchestrator/operation.destroy.test.ts +4 -0
  93. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  94. package/src/orchestrator/operation.preview.test.ts +4 -0
  95. package/src/orchestrator/operation.refresh.test.ts +4 -0
  96. package/src/orchestrator/operation.test-utils.ts +52 -13
  97. package/src/orchestrator/operation.ts +230 -68
  98. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  99. package/src/orchestrator/operation.update.skip.test.ts +196 -0
  100. package/src/orchestrator/operation.update.test.ts +4 -0
  101. package/src/orchestrator/plan-test-builder.ts +1 -0
  102. package/src/orchestrator/unit-input-values.test.ts +450 -0
  103. package/src/orchestrator/unit-input-values.ts +281 -0
  104. package/src/pubsub/manager.ts +3 -0
  105. package/src/runner/abstractions.ts +23 -54
  106. package/src/runner/factory.ts +3 -3
  107. package/src/runner/force-abort.ts +7 -2
  108. package/src/runner/local.ts +116 -87
  109. package/src/runner/pulumi.ts +3 -5
  110. package/src/services.ts +53 -2
  111. package/src/shared/models/prisma.ts +1 -0
  112. package/src/shared/models/project/entity.ts +121 -0
  113. package/src/shared/models/project/index.ts +1 -0
  114. package/src/shared/models/project/operation.ts +61 -3
  115. package/src/shared/models/project/state.ts +10 -0
  116. package/src/shared/models/project/worker.ts +7 -0
  117. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  118. package/src/shared/resolvers/effective-output-type.ts +162 -0
  119. package/src/shared/resolvers/index.ts +1 -0
  120. package/src/shared/resolvers/input.ts +59 -9
  121. package/src/shared/utils/index.ts +1 -0
  122. package/src/shared/utils/stable-json.ts +41 -0
  123. package/src/terminal/manager.ts +6 -0
  124. package/src/terminal/run.sh.ts +9 -4
  125. package/src/worker/manager.ts +97 -1
  126. package/LICENSE +0 -21
  127. package/dist/chunk-I7BWSAN6.js +0 -49
  128. package/dist/chunk-I7BWSAN6.js.map +0 -1
  129. package/dist/chunk-V2NILDHS.js.map +0 -1
  130. package/dist/chunk-X2WG3WGL.js.map +0 -1
  131. package/dist/index.js.map +0 -1
  132. package/dist/library/package-resolution-worker.js.map +0 -1
  133. package/dist/library/worker/main.js.map +0 -1
  134. package/dist/shared/index.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { Logger } from "pino"
2
2
  import type { ArtifactService } from "../artifact"
3
3
  import type {
4
+ EntitySnapshotService,
4
5
  InstanceLockService,
5
6
  InstanceStatePatch,
6
7
  InstanceStateService,
@@ -8,6 +9,7 @@ import type {
8
9
  ProjectModelService,
9
10
  SecretService,
10
11
  UnitExtraService,
12
+ UnitOutputService,
11
13
  } from "../business"
12
14
  import type { Operation, OperationUpdateInput, Project } from "../database"
13
15
  import type { LibraryBackend } from "../library"
@@ -18,7 +20,7 @@ import {
18
20
  parseInstanceId,
19
21
  type TriggerInvocation,
20
22
  type UnitConfig,
21
- type UnitInputReference,
23
+ type UnitInputValue,
22
24
  type VersionedName,
23
25
  } from "@highstate/contract"
24
26
  import { createId } from "@paralleldrive/cuid2"
@@ -30,15 +32,18 @@ import {
30
32
  type OperationPhase,
31
33
  PromiseTracker,
32
34
  type ResolvedInstanceInput,
35
+ resolveEffectiveOutputType,
33
36
  waitAll,
34
37
  } from "../shared"
35
38
  import { OperationContext } from "./operation-context"
36
39
  import { createOperationPlan } from "./operation-plan"
37
40
  import { OperationWorkset } from "./operation-workset"
41
+ import { resolveUnitInputValues } from "./unit-input-values"
38
42
 
39
43
  export class RuntimeOperation {
40
44
  private readonly instancePromiseMap = new Map<InstanceId, Promise<void>>()
41
45
  private readonly promiseTracker = new PromiseTracker()
46
+ private readonly pendingGhostDeletionIds = new Set<InstanceId>()
42
47
 
43
48
  private workset!: OperationWorkset
44
49
  private context!: OperationContext
@@ -57,6 +62,8 @@ export class RuntimeOperation {
57
62
  private readonly instanceStateService: InstanceStateService,
58
63
  private readonly projectModelService: ProjectModelService,
59
64
  private readonly unitExtraService: UnitExtraService,
65
+ private readonly entitySnapshotService: EntitySnapshotService,
66
+ private readonly unitOutputService: UnitOutputService,
60
67
  private readonly logger: Logger,
61
68
  ) {}
62
69
 
@@ -92,17 +99,21 @@ export class RuntimeOperation {
92
99
  }
93
100
 
94
101
  this.logger.error({ error }, "an error occurred while running the operation")
102
+ console.error(error)
95
103
 
96
104
  await this.updateOperation({ status: "failed" })
97
105
  await this.writeOperationLog(errorToString(error))
98
106
  } finally {
99
107
  try {
108
+ this.promiseTracker.track(this.flushGhostInstanceDeletions())
100
109
  this.promiseTracker.track(this.ensureInstancesUnlocked())
101
110
  this.promiseTracker.track(this.ensureOperationStatesFinalized())
102
111
 
103
112
  // ensure that all promises are resolved even if the operation failed
104
113
  await this.promiseTracker.waitForAll()
105
114
  } catch (error) {
115
+ console.error(error)
116
+
106
117
  this.logger.error(
107
118
  { error },
108
119
  "one of the tracked promises failed after the operation failed",
@@ -111,6 +122,17 @@ export class RuntimeOperation {
111
122
  }
112
123
  }
113
124
 
125
+ private async flushGhostInstanceDeletions(): Promise<void> {
126
+ if (this.pendingGhostDeletionIds.size === 0) {
127
+ return
128
+ }
129
+
130
+ const instanceIds = Array.from(this.pendingGhostDeletionIds)
131
+ this.pendingGhostDeletionIds.clear()
132
+
133
+ this.instanceStateService.publishGhostInstanceDeletion(this.project.id, instanceIds)
134
+ }
135
+
114
136
  private async operate(): Promise<void> {
115
137
  this.logger.info("starting operation")
116
138
 
@@ -120,6 +142,7 @@ export class RuntimeOperation {
120
142
  this.libraryBackend,
121
143
  this.instanceStateService,
122
144
  this.projectModelService,
145
+ this.entitySnapshotService,
123
146
  this.logger,
124
147
  )
125
148
 
@@ -143,6 +166,7 @@ export class RuntimeOperation {
143
166
  this.workset = new OperationWorkset(
144
167
  this.project,
145
168
  this.operation.id,
169
+ this.operation.type,
146
170
  plan,
147
171
  this.context,
148
172
  this.instanceStateService,
@@ -205,7 +229,7 @@ export class RuntimeOperation {
205
229
 
206
230
  // lauch all instances in this phase
207
231
  for (const instanceId of this.workset.phaseAffectedInstanceIds) {
208
- const instance = this.context.getInstance(instanceId)
232
+ const instance = this.getPhaseInstance(instanceId)
209
233
  const state = this.context.getState(instanceId)
210
234
  const promise = this.getInstancePromiseForOperation(instance, state)
211
235
 
@@ -291,7 +315,7 @@ export class RuntimeOperation {
291
315
  const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
292
316
  signal.throwIfAborted()
293
317
 
294
- const config = this.prepareUnitConfig(instance, secrets)
318
+ const config = this.prepareUnitConfig(instance, state.id, secrets)
295
319
  const artifactIds = this.collectArtifactIdsForInstance(instance)
296
320
  const artifacts = await this.artifactService.getArtifactsByIds(this.project.id, artifactIds)
297
321
 
@@ -299,13 +323,13 @@ export class RuntimeOperation {
299
323
 
300
324
  await this.runnerBackend.preview({
301
325
  projectId: this.project.id,
326
+ operationId: this.operation.id,
302
327
  libraryId: this.project.libraryId,
303
328
  stateId: state.id,
304
329
  instanceType: instance.type,
305
330
  instanceName: instance.name,
306
331
  config,
307
332
  refresh: this.operation.options.refresh,
308
- secrets,
309
333
  artifacts,
310
334
  signal,
311
335
  forceSignal,
@@ -351,7 +375,7 @@ export class RuntimeOperation {
351
375
  for (const child of children) {
352
376
  logger.debug(`waiting for child "%s"`, child)
353
377
 
354
- const instance = this.context.getInstance(child)
378
+ const instance = this.getPhaseInstance(child)
355
379
  const state = this.context.getState(child)
356
380
  const promise = this.getInstancePromiseForOperation(instance, state)
357
381
 
@@ -401,12 +425,15 @@ export class RuntimeOperation {
401
425
  state.status === "deployed" &&
402
426
  state.selfHash != null &&
403
427
  state.dependencyOutputHash != null &&
428
+ // do not short-circuit after destroy phase in recreate operations
429
+ state.lastOperationState?.status !== "destroyed" &&
404
430
  // ignore explicitly requested updates
405
431
  !this.operation.requestedInstanceIds.includes(instance.id) &&
406
432
  // ignore when side effects are requested
407
433
  !this.operation.options.refresh &&
408
434
  !this.operation.options.deleteUnreachableResources &&
409
- !this.operation.options.forceUpdateDependencies
435
+ !this.operation.options.forceUpdateDependencies &&
436
+ !this.operation.options.forceUpdateChildren
410
437
  ) {
411
438
  const expected = await this.context.getUpToDateInputHashOutput(instance)
412
439
 
@@ -426,6 +453,7 @@ export class RuntimeOperation {
426
453
  },
427
454
  instanceState: {
428
455
  inputHash: expected.inputHash,
456
+ parentId: instance.parentId ? this.context.getState(instance.parentId).id : null,
429
457
  },
430
458
  })
431
459
 
@@ -448,7 +476,7 @@ export class RuntimeOperation {
448
476
 
449
477
  signal.throwIfAborted()
450
478
 
451
- const config = this.prepareUnitConfig(instance, secrets)
479
+ const config = this.prepareUnitConfig(instance, state.id, secrets)
452
480
 
453
481
  // collect artifacts authorized for this instance
454
482
  const artifactIds = this.collectArtifactIdsForInstance(instance)
@@ -458,6 +486,7 @@ export class RuntimeOperation {
458
486
 
459
487
  await this.runnerBackend.update({
460
488
  projectId: this.project.id,
489
+ operationId: this.operation.id,
461
490
  libraryId: this.project.libraryId,
462
491
  stateId: state.id,
463
492
  instanceType: instance.type,
@@ -465,7 +494,6 @@ export class RuntimeOperation {
465
494
  config,
466
495
  refresh: this.operation.options.refresh,
467
496
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
468
- secrets,
469
497
  artifacts,
470
498
  signal,
471
499
  forceSignal,
@@ -541,14 +569,14 @@ export class RuntimeOperation {
541
569
 
542
570
  await this.runnerBackend.update({
543
571
  projectId: this.project.id,
572
+ operationId: this.operation.id,
544
573
  stateId: state.id,
545
574
  libraryId: this.project.libraryId,
546
575
  instanceType: instance.type,
547
576
  instanceName: instance.name,
548
- config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
577
+ config: this.prepareUnitConfig(instance, state.id, secrets, invokedTriggers),
549
578
  refresh: this.operation.options.refresh,
550
579
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
551
- secrets,
552
580
  signal,
553
581
  forceSignal,
554
582
  debug: this.operation.options.debug,
@@ -571,7 +599,7 @@ export class RuntimeOperation {
571
599
  continue
572
600
  }
573
601
 
574
- const instance = this.context.getInstance(dependent.instanceId)
602
+ const instance = this.getPhaseInstance(dependent.instanceId)
575
603
  dependentPromises.push(this.getInstancePromiseForOperation(instance, dependent))
576
604
  }
577
605
 
@@ -604,6 +632,7 @@ export class RuntimeOperation {
604
632
 
605
633
  await this.runnerBackend.destroy({
606
634
  projectId: this.project.id,
635
+ operationId: this.operation.id,
607
636
  stateId: state.id,
608
637
  libraryId: this.project.libraryId,
609
638
  instanceType: type,
@@ -613,6 +642,7 @@ export class RuntimeOperation {
613
642
  forceSignal,
614
643
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
615
644
  forceDeleteState: this.operation.options.forceDeleteState,
645
+ hasResourceHooks: state.hasResourceHooks ?? false,
616
646
  debug: this.operation.options.debug,
617
647
  })
618
648
 
@@ -639,6 +669,7 @@ export class RuntimeOperation {
639
669
 
640
670
  await this.runnerBackend.refresh({
641
671
  projectId: this.project.id,
672
+ operationId: this.operation.id,
642
673
  stateId: state.id,
643
674
  libraryId: this.project.libraryId,
644
675
  instanceType: type,
@@ -663,6 +694,7 @@ export class RuntimeOperation {
663
694
  ): Promise<void> {
664
695
  const stream = this.runnerBackend.watch({
665
696
  projectId: this.project.id,
697
+ operationId: this.operation.id,
666
698
  stateId: state.id,
667
699
  libraryId: this.project.libraryId,
668
700
  instanceType,
@@ -673,12 +705,18 @@ export class RuntimeOperation {
673
705
  let update: UnitStateUpdate | undefined
674
706
 
675
707
  for await (update of stream) {
708
+ let handlerError: Error | null = null
709
+
676
710
  try {
677
- await this.handleUnitStateUpdate(update, state)
711
+ handlerError = await this.handleUnitStateUpdate(update, state)
678
712
  } catch (error) {
679
713
  logger.error({ error }, "failed to handle unit state update")
680
714
  }
681
715
 
716
+ if (handlerError) {
717
+ throw handlerError
718
+ }
719
+
682
720
  if (update.type === "error") {
683
721
  // rethrow the error to stop the execution of dependent units
684
722
  throw new Error(
@@ -698,80 +736,121 @@ export class RuntimeOperation {
698
736
 
699
737
  private prepareUnitConfig(
700
738
  instance: InstanceModel,
739
+ stateId: string,
701
740
  secrets: Record<string, unknown>,
702
741
  invokedTriggers: TriggerInvocation[] = [],
703
742
  ): UnitConfig {
704
743
  const resolvedInputs = this.context.getResolvedInputs(instance.id)
744
+ const component = this.context.library.components[instance.type]!
745
+
746
+ const unfoldedInputs = mapValues(resolvedInputs ?? {}, (input, inputName) =>
747
+ input.flatMap(value => this.getUnitInputValues(inputName, value)),
748
+ )
749
+
750
+ for (const [inputName, inputSpec] of Object.entries(component.inputs)) {
751
+ if (inputSpec.multiple) {
752
+ continue
753
+ }
754
+
755
+ const values = unfoldedInputs[inputName] ?? []
756
+ if (values.length > 1) {
757
+ throw new Error(
758
+ `Input "${inputName}" of instance "${instance.id}" expects a single value, but ${values.length} values were resolved after unfolding.`,
759
+ )
760
+ }
761
+ }
705
762
 
706
763
  return {
707
764
  instanceId: instance.id,
765
+ stateId,
708
766
  args: instance.args ?? {},
709
- inputs: mapValues(resolvedInputs ?? {}, (input, inputName) =>
710
- input.map(value => this.getUnitInputRef(inputName, value)),
711
- ),
767
+ inputs: unfoldedInputs,
712
768
  invokedTriggers,
713
- secretNames: Object.keys(secrets),
714
- stateIdMap: this.context.getInstanceIdToStateIdMap(instance.id),
769
+ secretValues: secrets,
715
770
  importBasePath: this.libraryBackend.importPath,
716
771
  }
717
772
  }
718
773
 
719
- private getUnitInputRef(inputName: string, input: ResolvedInstanceInput): UnitInputReference {
720
- const instance = this.context.getInstance(input.input.instanceId)
721
- const component = this.context.library.components[instance.type]
774
+ private getUnitInputValues(inputName: string, input: ResolvedInstanceInput): UnitInputValue[] {
775
+ const dependencyInstance = this.context.getInstance(input.input.instanceId)
776
+ const captured = this.context.getCapturedOutputValues(
777
+ input.input.instanceId,
778
+ input.input.output,
779
+ )
722
780
 
723
- const outputSpec = component.outputs[input.input.output]
724
- if (!outputSpec) {
725
- throw new Error(
726
- `Output "${input.input.output}" is not defined on component "${instance.type}"`,
727
- )
728
- }
781
+ const dependencyComponent = this.context.library.components[dependencyInstance.type]
782
+ const fallbackType = dependencyComponent?.outputs[input.input.output]?.type ?? input.type
729
783
 
730
- const entity = this.context.library.entities[outputSpec.type]
731
- if (!entity) {
732
- throw new Error(`Entity type "${outputSpec.type}" is not defined in the library`)
733
- }
784
+ const getInstanceContext = (instanceId: string) => {
785
+ const resolvedOutput = this.context.inputResolver.outputs.get(`instance:${instanceId}`)
786
+ if (resolvedOutput && resolvedOutput.kind === "instance") {
787
+ return {
788
+ instance: resolvedOutput.instance,
789
+ component: resolvedOutput.component,
790
+ entities: resolvedOutput.entities,
791
+ }
792
+ }
793
+
794
+ try {
795
+ const instance = this.context.getInstance(instanceId as InstanceId)
796
+ const component = this.context.library.components[instance.type]
734
797
 
735
- // if output type matches input type or extends it, return simple reference, no transformation needed
736
- if (input.type === outputSpec.type || entity.extensions?.includes(input.type)) {
737
- return {
738
- instanceId: input.input.instanceId,
739
- output: input.input.output,
798
+ if (!component) {
799
+ return undefined
800
+ }
801
+
802
+ return {
803
+ instance,
804
+ component,
805
+ entities: this.context.library.entities,
806
+ }
807
+ } catch {
808
+ return undefined
740
809
  }
741
810
  }
742
811
 
743
- // otherwise, find matching inclusion to perform transformation
744
- const inclusion = entity.inclusions?.find(inc => inc.type === input.type)
745
- if (!inclusion) {
746
- throw new Error(
747
- `Cannot use output "${input.input.output}" of type "${outputSpec.type}" from instance "${input.input.instanceId}" for input "${inputName}" of type "${input.type}": no matching inclusion found in entity "${entity.type}"`,
748
- )
749
- }
812
+ const effectiveOutputType = resolveEffectiveOutputType({
813
+ input: input.input,
814
+ fallbackType,
815
+ getInstanceContext,
816
+ })
750
817
 
751
- return {
752
- instanceId: input.input.instanceId,
753
- output: input.input.output,
754
- inclusion,
755
- }
818
+ const effectiveRootOutputType = resolveEffectiveOutputType({
819
+ input: {
820
+ ...input.input,
821
+ path: undefined,
822
+ },
823
+ fallbackType,
824
+ getInstanceContext,
825
+ })
826
+
827
+ return resolveUnitInputValues({
828
+ library: this.context.library,
829
+ inputName,
830
+ resolvedInput: input,
831
+ dependencyInstanceType: dependencyInstance.type,
832
+ captured,
833
+ effectiveOutputType,
834
+ effectiveRootOutputType,
835
+ })
756
836
  }
757
837
 
758
838
  private async handleUnitStateUpdate(
759
839
  update: UnitStateUpdate,
760
840
  state: InstanceState,
761
- ): Promise<void> {
841
+ ): Promise<Error | null> {
762
842
  switch (update.type) {
763
843
  case "message":
764
844
  this.handleUnitMessage(update, state)
765
- return
845
+ return null
766
846
  case "progress":
767
847
  await this.handleUnitProgress(update)
768
- return
848
+ return null
769
849
  case "error":
770
850
  await this.handleUnitError(update, state)
771
- return
851
+ return null
772
852
  case "completion":
773
- await this.handleUnitCompletion(update, state)
774
- return
853
+ return await this.handleUnitCompletion(update, state)
775
854
  }
776
855
  }
777
856
 
@@ -811,7 +890,7 @@ export class RuntimeOperation {
811
890
  private async handleUnitCompletion(
812
891
  update: TypedUnitStateUpdate<"completion">,
813
892
  state: InstanceState,
814
- ): Promise<void> {
893
+ ): Promise<Error | null> {
815
894
  if (this.operation.type === "preview") {
816
895
  await this.workset.updateState(update.unitId, {
817
896
  operationState: {
@@ -819,24 +898,75 @@ export class RuntimeOperation {
819
898
  finishedAt: new Date(),
820
899
  },
821
900
  })
822
- return
901
+ return null
823
902
  }
824
903
 
825
- const instance = this.context.getInstance(update.unitId)
904
+ const instance = this.getPhaseInstance(update.unitId)
905
+
906
+ if (update.rawOutputs && update.operationType !== "destroy") {
907
+ this.context.updateCapturedOutputValuesFromUnitOutputs({
908
+ instanceId: instance.id,
909
+ instanceType: instance.type,
910
+ outputs: update.rawOutputs,
911
+ })
912
+ }
913
+
914
+ const parsed = update.rawOutputs
915
+ ? await this.unitOutputService.parseUnitOutputs({
916
+ libraryId: this.context.project.libraryId,
917
+ instanceType: instance.type,
918
+ outputs: update.rawOutputs,
919
+ })
920
+ : {
921
+ outputHash: null,
922
+ statusFields: null,
923
+ terminals: null,
924
+ pages: null,
925
+ triggers: null,
926
+ secrets: null,
927
+ workers: null,
928
+ exportedArtifactIds: null,
929
+ hasResourceHooks: false,
930
+ entitySnapshotError: null,
931
+ entitySnapshotPayload: null,
932
+ }
933
+
934
+ if (parsed.entitySnapshotError) {
935
+ await this.operationService.appendLog(
936
+ this.project.id,
937
+ this.operation.id,
938
+ state.id,
939
+ `Failed to parse unit outputs: ${parsed.entitySnapshotError}`,
940
+ )
941
+
942
+ await this.workset.updateState(update.unitId, {
943
+ instanceState: {
944
+ status: "failed",
945
+ },
946
+ operationState: {
947
+ status: "failed",
948
+ finishedAt: new Date(),
949
+ },
950
+ })
951
+
952
+ return new Error(
953
+ `Failed to parse unit outputs for unit "${instance.id}": ${parsed.entitySnapshotError}`,
954
+ )
955
+ }
826
956
 
827
957
  const data: InstanceStatePatch = {
828
958
  status: this.workset.getNextStableInstanceStatus(instance.id),
829
- statusFields: update.statusFields ?? null,
959
+ statusFields: parsed.statusFields,
830
960
  }
831
961
 
832
- const artifactIds = update.exportedArtifactIds
833
- ? Object.values(update.exportedArtifactIds).flat()
962
+ const artifactIds = parsed.exportedArtifactIds
963
+ ? Object.values(parsed.exportedArtifactIds).flat()
834
964
  : []
835
965
 
836
966
  if (update.operationType !== "destroy") {
837
967
  // давайте еще больше усложним и без того сложную штуку
838
968
  // set output hash before calculating input hash to capture up-to-date output hash for dependencies
839
- state.outputHash = update.outputHash ?? null
969
+ state.outputHash = parsed.outputHash
840
970
 
841
971
  // recalculate the input and output hashes for the instance
842
972
  const { selfHash, inputHash, dependencyOutputHash } =
@@ -845,9 +975,13 @@ export class RuntimeOperation {
845
975
  data.selfHash = selfHash
846
976
  data.inputHash = inputHash
847
977
  data.dependencyOutputHash = dependencyOutputHash
848
- data.outputHash = update.outputHash
978
+ data.outputHash = parsed.outputHash
849
979
 
850
- data.exportedArtifactIds = update.exportedArtifactIds
980
+ data.exportedArtifactIds = parsed.exportedArtifactIds
981
+
982
+ if (update.rawOutputs) {
983
+ data.hasResourceHooks = parsed.hasResourceHooks
984
+ }
851
985
 
852
986
  // also update the parent ID
853
987
  if (instance.parentId) {
@@ -865,6 +999,7 @@ export class RuntimeOperation {
865
999
  data.model = null
866
1000
  data.resolvedInputs = null
867
1001
  data.exportedArtifactIds = null
1002
+ data.hasResourceHooks = false
868
1003
  }
869
1004
 
870
1005
  // update the operation state
@@ -883,23 +1018,34 @@ export class RuntimeOperation {
883
1018
  // also do not write unit extra data for non-last phases of the instance
884
1019
  unitExtra: this.workset.isLastPhaseForInstance(instance.id)
885
1020
  ? {
886
- pages: update.pages ?? [],
887
- terminals: update.terminals ?? [],
888
- triggers: update.triggers ?? [],
889
- workers: update.workers ?? [],
890
- secrets: update.secrets ?? {},
1021
+ pages: parsed.pages ?? [],
1022
+ terminals: parsed.terminals ?? [],
1023
+ triggers: parsed.triggers ?? [],
1024
+ workers: parsed.workers ?? [],
1025
+ secrets: parsed.secrets ?? {},
891
1026
  artifactIds,
892
1027
  }
893
1028
  : undefined,
894
1029
  })
895
1030
 
1031
+ if (update.operationType !== "destroy" && parsed.entitySnapshotPayload) {
1032
+ await this.entitySnapshotService.persistUnitEntitySnapshots({
1033
+ projectId: this.project.id,
1034
+ operationId: this.operation.id,
1035
+ stateId: state.id,
1036
+ payload: parsed.entitySnapshotPayload,
1037
+ })
1038
+ }
1039
+
896
1040
  if (
897
1041
  update.operationType === "destroy" &&
898
1042
  this.workset.isLastPhaseForInstance(instance.id) &&
899
1043
  this.context.isGhostInstance(instance.id)
900
1044
  ) {
901
- this.instanceStateService.publishGhostInstanceDeletion(this.project.id, [instance.id])
1045
+ this.pendingGhostDeletionIds.add(instance.id)
902
1046
  }
1047
+
1048
+ return null
903
1049
  }
904
1050
 
905
1051
  private getInstancePromise(
@@ -1055,4 +1201,20 @@ export class RuntimeOperation {
1055
1201
 
1056
1202
  return Array.from(artifactIds)
1057
1203
  }
1204
+
1205
+ private getPhaseInstance(instanceId: InstanceId): InstanceModel {
1206
+ const modelInstance = this.context.tryGetInstance(instanceId)
1207
+ if (modelInstance) {
1208
+ return modelInstance
1209
+ }
1210
+
1211
+ if (this.workset.currentPhase === "destroy") {
1212
+ const destroyInstance = this.context.tryGetInstanceForDestroy(instanceId)
1213
+ if (destroyInstance) {
1214
+ return destroyInstance
1215
+ }
1216
+ }
1217
+
1218
+ throw new Error(`Instance with ID ${instanceId} not found in the operation context`)
1219
+ }
1058
1220
  }
@@ -18,6 +18,8 @@ describe("Operation - Update Failure", () => {
18
18
  instanceStateService,
19
19
  projectModelService,
20
20
  unitExtraService,
21
+ entitySnapshotService,
22
+ unitOutputService,
21
23
  createUnit,
22
24
  createDeployedUnitState,
23
25
  createOperation,
@@ -60,6 +62,8 @@ describe("Operation - Update Failure", () => {
60
62
  instanceStateService,
61
63
  projectModelService,
62
64
  unitExtraService,
65
+ entitySnapshotService,
66
+ unitOutputService,
63
67
  logger,
64
68
  )
65
69