@highstate/backend 0.16.0 → 0.18.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.
Files changed (112) hide show
  1. package/dist/{chunk-4JUMOKLV.js → chunk-JT4KWE3B.js} +10 -8
  2. package/dist/chunk-JT4KWE3B.js.map +1 -0
  3. package/dist/{chunk-VB4YL327.js → chunk-X2WG3WGL.js} +9 -2
  4. package/dist/chunk-X2WG3WGL.js.map +1 -0
  5. package/dist/highstate.manifest.json +4 -5
  6. package/dist/index.js +256 -287
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +1 -1
  9. package/dist/shared/index.js +1 -1
  10. package/package.json +9 -10
  11. package/prisma/backend/postgresql/main.prisma +0 -2
  12. package/prisma/backend/sqlite/main.prisma +0 -2
  13. package/prisma/project/instance.prisma +12 -0
  14. package/prisma/project/main.prisma +0 -1
  15. package/prisma/project/migrations/20260123000000_add_instance_state_self_hash/migration.sql +2 -0
  16. package/src/business/instance-state.test.ts +1 -0
  17. package/src/business/instance-state.ts +3 -0
  18. package/src/business/project.ts +1 -2
  19. package/src/common/utils.ts +9 -0
  20. package/src/database/_generated/backend/postgresql/browser.ts +54 -0
  21. package/src/database/_generated/backend/postgresql/client.ts +7 -8
  22. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +3 -2
  23. package/src/database/_generated/backend/postgresql/enums.ts +3 -1
  24. package/src/database/_generated/backend/postgresql/internal/class.ts +24 -71
  25. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +41 -43
  26. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +191 -0
  27. package/src/database/_generated/backend/postgresql/models/BackendUnlockMethod.ts +12 -11
  28. package/src/database/_generated/backend/postgresql/models/Library.ts +29 -28
  29. package/src/database/_generated/backend/postgresql/models/Project.ts +69 -68
  30. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +29 -28
  31. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +26 -25
  32. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +29 -28
  33. package/src/database/_generated/backend/postgresql/models/UserWorkspaceLayout.ts +12 -11
  34. package/src/database/_generated/backend/postgresql/models.ts +2 -1
  35. package/src/database/_generated/backend/postgresql/pjtg.ts +1 -0
  36. package/src/database/_generated/backend/sqlite/browser.ts +54 -0
  37. package/src/database/_generated/backend/sqlite/client.ts +7 -8
  38. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +3 -2
  39. package/src/database/_generated/backend/sqlite/enums.ts +3 -1
  40. package/src/database/_generated/backend/sqlite/internal/class.ts +24 -71
  41. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +41 -43
  42. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +188 -0
  43. package/src/database/_generated/backend/sqlite/models/BackendUnlockMethod.ts +12 -11
  44. package/src/database/_generated/backend/sqlite/models/Library.ts +29 -28
  45. package/src/database/_generated/backend/sqlite/models/Project.ts +69 -68
  46. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +29 -28
  47. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +26 -25
  48. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +29 -28
  49. package/src/database/_generated/backend/sqlite/models/UserWorkspaceLayout.ts +12 -11
  50. package/src/database/_generated/backend/sqlite/models.ts +2 -1
  51. package/src/database/_generated/backend/sqlite/pjtg.ts +1 -0
  52. package/src/database/_generated/project/browser.ts +1 -0
  53. package/src/database/_generated/project/client.ts +4 -5
  54. package/src/database/_generated/project/commonInputTypes.ts +1 -0
  55. package/src/database/_generated/project/enums.ts +1 -0
  56. package/src/database/_generated/project/internal/class.ts +21 -63
  57. package/src/database/_generated/project/internal/prismaNamespace.ts +41 -36
  58. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +10 -6
  59. package/src/database/_generated/project/models/ApiKey.ts +1 -0
  60. package/src/database/_generated/project/models/Artifact.ts +1 -0
  61. package/src/database/_generated/project/models/HubModel.ts +1 -0
  62. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1 -0
  63. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1 -0
  64. package/src/database/_generated/project/models/InstanceLock.ts +1 -0
  65. package/src/database/_generated/project/models/InstanceModel.ts +1 -0
  66. package/src/database/_generated/project/models/InstanceOperationState.ts +1 -0
  67. package/src/database/_generated/project/models/InstanceState.ts +108 -1
  68. package/src/database/_generated/project/models/Operation.ts +1 -0
  69. package/src/database/_generated/project/models/OperationLog.ts +1 -0
  70. package/src/database/_generated/project/models/Page.ts +1 -0
  71. package/src/database/_generated/project/models/Secret.ts +1 -0
  72. package/src/database/_generated/project/models/ServiceAccount.ts +1 -0
  73. package/src/database/_generated/project/models/Terminal.ts +1 -0
  74. package/src/database/_generated/project/models/TerminalSession.ts +1 -0
  75. package/src/database/_generated/project/models/TerminalSessionLog.ts +1 -0
  76. package/src/database/_generated/project/models/Trigger.ts +1 -0
  77. package/src/database/_generated/project/models/UnlockMethod.ts +1 -0
  78. package/src/database/_generated/project/models/UserCompositeViewport.ts +1 -0
  79. package/src/database/_generated/project/models/UserProjectViewport.ts +1 -0
  80. package/src/database/_generated/project/models/Worker.ts +1 -0
  81. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1 -0
  82. package/src/database/_generated/project/models/WorkerVersion.ts +1 -0
  83. package/src/database/_generated/project/models/WorkerVersionLog.ts +1 -0
  84. package/src/database/_generated/project/models.ts +1 -0
  85. package/src/database/abstractions.ts +1 -7
  86. package/src/database/index.ts +1 -0
  87. package/src/database/local/backend.ts +19 -30
  88. package/src/database/local/project.ts +4 -9
  89. package/src/database/manager.ts +28 -34
  90. package/src/database/migration.ts +126 -0
  91. package/src/orchestrator/operation.cancel.test.ts +112 -0
  92. package/src/orchestrator/operation.composite.test.ts +123 -0
  93. package/src/orchestrator/operation.destroy.test.ts +77 -0
  94. package/src/orchestrator/operation.preview.test.ts +95 -0
  95. package/src/orchestrator/operation.refresh.test.ts +77 -0
  96. package/src/orchestrator/operation.test-utils.ts +646 -0
  97. package/src/orchestrator/operation.ts +91 -3
  98. package/src/orchestrator/operation.update.failure.test.ts +88 -0
  99. package/src/orchestrator/operation.update.skip.test.ts +95 -0
  100. package/src/orchestrator/operation.update.test.ts +117 -0
  101. package/src/orchestrator/plan-test-builder.ts +1 -0
  102. package/src/runner/abstractions.ts +5 -0
  103. package/src/runner/local.ts +1 -0
  104. package/src/shared/resolvers/input-hash.ts +10 -6
  105. package/src/terminal/manager.ts +0 -3
  106. package/src/test-utils/database.ts +28 -14
  107. package/dist/chunk-4JUMOKLV.js.map +0 -1
  108. package/dist/chunk-VB4YL327.js.map +0 -1
  109. package/dist/database/local/prisma.config.js +0 -26
  110. package/dist/database/local/prisma.config.js.map +0 -1
  111. package/src/database/local/prisma.config.ts +0 -25
  112. package/src/database/migrate.ts +0 -35
@@ -18,6 +18,7 @@ import {
18
18
  parseInstanceId,
19
19
  type TriggerInvocation,
20
20
  type UnitConfig,
21
+ type UnitInputReference,
21
22
  type VersionedName,
22
23
  } from "@highstate/contract"
23
24
  import { createId } from "@paralleldrive/cuid2"
@@ -28,6 +29,7 @@ import {
28
29
  isTransientInstanceOperationStatus,
29
30
  type OperationPhase,
30
31
  PromiseTracker,
32
+ type ResolvedInstanceInput,
31
33
  waitAll,
32
34
  } from "../shared"
33
35
  import { OperationContext } from "./operation-context"
@@ -186,7 +188,7 @@ export class RuntimeOperation {
186
188
  this.workset.markInstanceLocked(stateId)
187
189
  }
188
190
  },
189
- true, // allow partial locks to allow independent free branches to run
191
+ true, // allow partial locks to allow independent branches to run
190
192
  this.workset.abortController.signal,
191
193
  60_000, // wait up to 60 seconds for unlock events before retrying
192
194
  this.unlockToken,
@@ -390,6 +392,47 @@ export class RuntimeOperation {
390
392
  throw new AbortError("The operation is failing, aborting current branch (still not failed)")
391
393
  }
392
394
 
395
+ // Runtime short-circuit:
396
+ // If this instance's own config has not changed and none of its dependencies outputs changed,
397
+ // we can skip execution even if the full inputHash changed due to upstream config-only changes.
398
+ //
399
+ // Do not short-circuit when the caller requested side effects that require a real engine run or the instance was explicitly requested.
400
+ if (
401
+ state.status === "deployed" &&
402
+ state.selfHash != null &&
403
+ state.dependencyOutputHash != null &&
404
+ // ignore explicitly requested updates
405
+ !this.operation.requestedInstanceIds.includes(instance.id) &&
406
+ // ignore when side effects are requested
407
+ !this.operation.options.refresh &&
408
+ !this.operation.options.deleteUnreachableResources &&
409
+ !this.operation.options.forceUpdateDependencies
410
+ ) {
411
+ const expected = await this.context.getUpToDateInputHashOutput(instance)
412
+
413
+ if (
414
+ expected.selfHash === state.selfHash &&
415
+ expected.dependencyOutputHash === state.dependencyOutputHash
416
+ ) {
417
+ logger.info("skipping unit update (short-circuit: no effective changes)")
418
+
419
+ const now = new Date()
420
+
421
+ await this.workset.updateState(instance.id, {
422
+ operationState: {
423
+ status: "skipped",
424
+ startedAt: now,
425
+ finishedAt: now,
426
+ },
427
+ instanceState: {
428
+ inputHash: expected.inputHash,
429
+ },
430
+ })
431
+
432
+ return
433
+ }
434
+ }
435
+
393
436
  logger.info("updating unit")
394
437
 
395
438
  await this.workset.updateState(instance.id, {
@@ -421,6 +464,7 @@ export class RuntimeOperation {
421
464
  instanceName: instance.name,
422
465
  config,
423
466
  refresh: this.operation.options.refresh,
467
+ deleteUnreachable: this.operation.options.deleteUnreachableResources,
424
468
  secrets,
425
469
  artifacts,
426
470
  signal,
@@ -503,6 +547,7 @@ export class RuntimeOperation {
503
547
  instanceName: instance.name,
504
548
  config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
505
549
  refresh: this.operation.options.refresh,
550
+ deleteUnreachable: this.operation.options.deleteUnreachableResources,
506
551
  secrets,
507
552
  signal,
508
553
  forceSignal,
@@ -661,7 +706,9 @@ export class RuntimeOperation {
661
706
  return {
662
707
  instanceId: instance.id,
663
708
  args: instance.args ?? {},
664
- inputs: mapValues(resolvedInputs ?? {}, input => input.map(value => value.input)),
709
+ inputs: mapValues(resolvedInputs ?? {}, (input, inputName) =>
710
+ input.map(value => this.getUnitInputRef(inputName, value)),
711
+ ),
665
712
  invokedTriggers,
666
713
  secretNames: Object.keys(secrets),
667
714
  stateIdMap: this.context.getInstanceIdToStateIdMap(instance.id),
@@ -669,6 +716,45 @@ export class RuntimeOperation {
669
716
  }
670
717
  }
671
718
 
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]
722
+
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
+ }
729
+
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
+ }
734
+
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,
740
+ }
741
+ }
742
+
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
+ }
750
+
751
+ return {
752
+ instanceId: input.input.instanceId,
753
+ output: input.input.output,
754
+ inclusion,
755
+ }
756
+ }
757
+
672
758
  private async handleUnitStateUpdate(
673
759
  update: UnitStateUpdate,
674
760
  state: InstanceState,
@@ -753,9 +839,10 @@ export class RuntimeOperation {
753
839
  state.outputHash = update.outputHash ?? null
754
840
 
755
841
  // recalculate the input and output hashes for the instance
756
- const { inputHash, dependencyOutputHash } =
842
+ const { selfHash, inputHash, dependencyOutputHash } =
757
843
  await this.context.getUpToDateInputHashOutput(instance)
758
844
 
845
+ data.selfHash = selfHash
759
846
  data.inputHash = inputHash
760
847
  data.dependencyOutputHash = dependencyOutputHash
761
848
  data.outputHash = update.outputHash
@@ -770,6 +857,7 @@ export class RuntimeOperation {
770
857
  data.parentId = null
771
858
  }
772
859
  } else {
860
+ data.selfHash = null
773
861
  data.inputHash = null
774
862
  data.dependencyOutputHash = null
775
863
  data.outputHash = null
@@ -0,0 +1,88 @@
1
+ import { describe } from "vitest"
2
+ import { RuntimeOperation } from "./operation"
3
+ import { operationTest } from "./operation.test-utils"
4
+
5
+ describe("Operation - Update Failure", () => {
6
+ operationTest(
7
+ "marks operation failed when runner emits error update",
8
+ async ({
9
+ project,
10
+ logger,
11
+ runnerBackend,
12
+ runner,
13
+ libraryBackend,
14
+ artifactService,
15
+ instanceLockService,
16
+ operationService,
17
+ secretService,
18
+ instanceStateService,
19
+ projectModelService,
20
+ unitExtraService,
21
+ createUnit,
22
+ createDeployedUnitState,
23
+ createOperation,
24
+ createContext,
25
+ setupPersistenceMocks,
26
+ setupImmediateLocking,
27
+ expect,
28
+ }) => {
29
+ // arrange
30
+ const unit = createUnit("A")
31
+ const state = createDeployedUnitState(unit)
32
+
33
+ await createContext({ instances: [unit], states: [state] })
34
+ setupImmediateLocking()
35
+ setupPersistenceMocks({ instances: [unit] })
36
+
37
+ runner.setAutoCompletion(false)
38
+ runner.emitError(unit.id, "boom")
39
+
40
+ const operation = createOperation({
41
+ type: "update",
42
+ requestedInstanceIds: [unit.id],
43
+ phases: [
44
+ {
45
+ type: "update",
46
+ instances: [{ id: unit.id, message: "requested", parentId: undefined }],
47
+ },
48
+ ],
49
+ })
50
+
51
+ const runtimeOperation = new RuntimeOperation(
52
+ project,
53
+ operation,
54
+ runnerBackend,
55
+ libraryBackend,
56
+ artifactService,
57
+ instanceLockService,
58
+ operationService,
59
+ secretService,
60
+ instanceStateService,
61
+ projectModelService,
62
+ unitExtraService,
63
+ logger,
64
+ )
65
+
66
+ // act
67
+ await runtimeOperation.operateSafe()
68
+
69
+ // assert
70
+ expect(runnerBackend.update).toHaveBeenCalledTimes(1)
71
+
72
+ const updateOperationCalls = operationService.updateOperation.mock.calls
73
+ const wasMarkedFailed = updateOperationCalls.some(call => {
74
+ const patch = call[2] as unknown
75
+ if (!patch || typeof patch !== "object") {
76
+ return false
77
+ }
78
+
79
+ const maybePatch = patch as { status?: unknown }
80
+ return maybePatch.status === "failed"
81
+ })
82
+
83
+ expect(wasMarkedFailed).toBe(true)
84
+ expect(operationService.markOperationFinished).not.toHaveBeenCalled()
85
+ expect(operationService.appendLog).toHaveBeenCalled()
86
+ },
87
+ )
88
+ })
@@ -0,0 +1,95 @@
1
+ import { describe } from "vitest"
2
+ import { RuntimeOperation } from "./operation"
3
+ import { operationTest } from "./operation.test-utils"
4
+
5
+ describe("RuntimeOperation - Update Short-Circuit", () => {
6
+ operationTest(
7
+ "skips unit update when selfHash and dependencyOutputHash match",
8
+ async ({
9
+ project,
10
+ logger,
11
+ runnerBackend,
12
+ libraryBackend,
13
+ artifactService,
14
+ instanceLockService,
15
+ operationService,
16
+ secretService,
17
+ instanceStateService,
18
+ projectModelService,
19
+ unitExtraService,
20
+ createUnit,
21
+ createDeployedUnitState,
22
+ createContext,
23
+ createOperation,
24
+ setupImmediateLocking,
25
+ setupPersistenceMocks,
26
+ expect,
27
+ }) => {
28
+ // arrange
29
+ const unit = createUnit("A")
30
+ const state = createDeployedUnitState(unit)
31
+
32
+ const context = await createContext({ instances: [unit], states: [state] })
33
+
34
+ const expected = await context.getUpToDateInputHashOutput(unit)
35
+ state.selfHash = expected.selfHash
36
+ state.dependencyOutputHash = expected.dependencyOutputHash
37
+ instanceStateService.getInstanceStates.mockResolvedValue([state])
38
+
39
+ setupImmediateLocking()
40
+ setupPersistenceMocks({ instances: [unit] })
41
+
42
+ const operation = createOperation({
43
+ type: "update",
44
+ requestedInstanceIds: [],
45
+ phases: [
46
+ {
47
+ type: "update",
48
+ instances: [{ id: unit.id, message: "explicitly requested", parentId: undefined }],
49
+ },
50
+ ],
51
+ })
52
+
53
+ const runtimeOperation = new RuntimeOperation(
54
+ project,
55
+ operation,
56
+ runnerBackend,
57
+ libraryBackend,
58
+ artifactService,
59
+ instanceLockService,
60
+ operationService,
61
+ secretService,
62
+ instanceStateService,
63
+ projectModelService,
64
+ unitExtraService,
65
+ logger,
66
+ )
67
+
68
+ // act
69
+ await runtimeOperation.operateSafe()
70
+
71
+ // assert
72
+ expect(runnerBackend.update).not.toHaveBeenCalled()
73
+
74
+ const skipCall = instanceStateService.updateOperationState.mock.calls.find(
75
+ (_call): boolean => {
76
+ const options = _call[3]
77
+ return options.operationState?.status === "skipped"
78
+ },
79
+ )
80
+
81
+ expect(skipCall).toBeDefined()
82
+
83
+ const skipOptions = skipCall?.[3]
84
+ expect(skipOptions?.instanceState).toEqual({
85
+ inputHash: expected.inputHash,
86
+ })
87
+
88
+ expect(operationService.markOperationFinished).toHaveBeenCalledWith(
89
+ project.id,
90
+ operation.id,
91
+ "completed",
92
+ )
93
+ },
94
+ )
95
+ })
@@ -0,0 +1,117 @@
1
+ import type { InstanceModel } from "@highstate/contract"
2
+ import { describe } from "vitest"
3
+ import { RuntimeOperation } from "./operation"
4
+ import { createDeferred, operationTest } from "./operation.test-utils"
5
+
6
+ describe("Operation - Update", () => {
7
+ operationTest(
8
+ "waits for dependency update to complete before running dependent",
9
+ async ({
10
+ project,
11
+ logger,
12
+ runnerBackend,
13
+ runner,
14
+ libraryBackend,
15
+ artifactService,
16
+ instanceLockService,
17
+ operationService,
18
+ secretService,
19
+ instanceStateService,
20
+ projectModelService,
21
+ unitExtraService,
22
+ createUnit,
23
+ createDeployedUnitState,
24
+ createOperation,
25
+ createContext,
26
+ setupPersistenceMocks,
27
+ setupImmediateLocking,
28
+ expect,
29
+ }) => {
30
+ // arrange
31
+ const unitA = createUnit("A")
32
+ const unitB: InstanceModel = {
33
+ ...createUnit("B"),
34
+ inputs: {
35
+ dependency: [{ instanceId: unitA.id, output: "value" }],
36
+ },
37
+ }
38
+
39
+ const stateA = createDeployedUnitState(unitA)
40
+ const stateB = createDeployedUnitState(unitB)
41
+
42
+ await createContext({ instances: [unitA, unitB], states: [stateA, stateB] })
43
+
44
+ setupImmediateLocking()
45
+ setupPersistenceMocks({ instances: [unitA, unitB] })
46
+
47
+ const aStarted = createDeferred<void>()
48
+ const aDone = createDeferred<void>()
49
+ let canRunB = false
50
+
51
+ runner.setUpdateImpl(async input => {
52
+ if (input.instanceName === "A") {
53
+ aStarted.resolve(undefined)
54
+ await aDone.promise
55
+ canRunB = true
56
+ return
57
+ }
58
+
59
+ if (input.instanceName === "B") {
60
+ expect(canRunB).toBe(true)
61
+ return
62
+ }
63
+
64
+ throw new Error(`unexpected unit: ${input.instanceName}`)
65
+ })
66
+
67
+ const operation = createOperation({
68
+ type: "update",
69
+ requestedInstanceIds: [unitB.id],
70
+ phases: [
71
+ {
72
+ type: "update",
73
+ instances: [
74
+ { id: unitA.id, message: "dependency", parentId: undefined },
75
+ { id: unitB.id, message: "requested", parentId: undefined },
76
+ ],
77
+ },
78
+ ],
79
+ })
80
+
81
+ const runtimeOperation = new RuntimeOperation(
82
+ project,
83
+ operation,
84
+ runnerBackend,
85
+ libraryBackend,
86
+ artifactService,
87
+ instanceLockService,
88
+ operationService,
89
+ secretService,
90
+ instanceStateService,
91
+ projectModelService,
92
+ unitExtraService,
93
+ logger,
94
+ )
95
+
96
+ // act
97
+ const operationPromise = runtimeOperation.operateSafe()
98
+
99
+ // assert
100
+ await aStarted.promise
101
+ expect(runnerBackend.update).toHaveBeenCalledTimes(1)
102
+ expect(runnerBackend.update.mock.calls[0]?.[0].instanceName).toBe("A")
103
+
104
+ // act
105
+ aDone.resolve(undefined)
106
+ await operationPromise
107
+
108
+ // assert
109
+ expect(runnerBackend.update).toHaveBeenCalledTimes(2)
110
+ expect(operationService.markOperationFinished).toHaveBeenCalledWith(
111
+ project.id,
112
+ operation.id,
113
+ "completed",
114
+ )
115
+ },
116
+ )
117
+ })
@@ -103,6 +103,7 @@ export class PlanTestBuilder {
103
103
  kind: instance.kind,
104
104
  parentId: null,
105
105
  parentInstanceId: instance.parentId ?? null,
106
+ selfHash: null,
106
107
  inputHash: null,
107
108
  outputHash: null,
108
109
  dependencyOutputHash: null,
@@ -171,6 +171,11 @@ export type UnitUpdateOptions = UnitOptions & {
171
171
  * Whether to refresh the state before updating.
172
172
  */
173
173
  refresh?: boolean
174
+
175
+ /**
176
+ * Whether to delete the unreachable resources (e.g. k8s resources in unreachable clusters).
177
+ */
178
+ deleteUnreachable?: boolean
174
179
  }
175
180
 
176
181
  export type UnitDestroyOptions = UnitOptions & {
@@ -129,6 +129,7 @@ export class LocalRunnerBackend implements RunnerBackend {
129
129
  const envVars: Record<string, string> = {
130
130
  HIGHSTATE_CACHE_DIR: this.cacheDir,
131
131
  HIGHSTATE_TEMP_PATH: unitTempPath,
132
+ PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
132
133
  ...options.envVars,
133
134
  }
134
135
 
@@ -16,6 +16,7 @@ export type InputHashNode = {
16
16
  }
17
17
 
18
18
  export type InputHashOutput = {
19
+ selfHash: number
19
20
  inputHash: number
20
21
  dependencyOutputHash: number
21
22
  outputHash: number
@@ -44,27 +45,27 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
44
45
  sourceHash,
45
46
  state,
46
47
  }: InputHashNode): InputHashOutput {
47
- const inputHashSink: Uint8Array[] = []
48
+ const selfHashSink: Uint8Array[] = []
48
49
 
49
50
  // 0. include the instance id to reflect renames
50
- inputHashSink.push(Buffer.from(instance.id))
51
+ selfHashSink.push(Buffer.from(instance.id))
51
52
 
52
53
  // 1. include the component definition hash
53
- inputHashSink.push(int32ToBytes(component.definitionHash))
54
+ selfHashSink.push(int32ToBytes(component.definitionHash))
54
55
 
55
56
  // 2. include the input hash nonce if available
56
57
  if (state?.inputHashNonce) {
57
- inputHashSink.push(int32ToBytes(state.inputHashNonce))
58
+ selfHashSink.push(int32ToBytes(state.inputHashNonce))
58
59
  }
59
60
 
60
61
  // 3. include instance args encoded as msgpack
61
62
  if (instance.args) {
62
- inputHashSink.push(encode(instance.args))
63
+ selfHashSink.push(encode(instance.args))
63
64
  }
64
65
 
65
66
  // 4. include the source hash if available
66
67
  if (sourceHash) {
67
- inputHashSink.push(int32ToBytes(sourceHash))
68
+ selfHashSink.push(int32ToBytes(sourceHash))
68
69
  } else if (isUnitModel(component)) {
69
70
  this.logger.warn(
70
71
  { instanceId: instance.id },
@@ -72,6 +73,8 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
72
73
  )
73
74
  }
74
75
 
76
+ const inputHashSink = [...selfHashSink]
77
+
75
78
  const sortedInputs = Object.entries(resolvedInputs)
76
79
  //
77
80
  .sort(([a], [b]) => a.localeCompare(b))
@@ -126,6 +129,7 @@ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOut
126
129
  }
127
130
 
128
131
  return {
132
+ selfHash: crc32(Buffer.concat(selfHashSink)),
129
133
  inputHash: crc32(Buffer.concat(inputHashSink)),
130
134
  dependencyOutputHash: crc32(Buffer.concat(dependencyOutputHashSink)),
131
135
  outputHash: state?.outputHash ?? 0,
@@ -106,12 +106,9 @@ export class TerminalManager {
106
106
  throw new Error(`Terminal "${terminalId}" not found`)
107
107
  }
108
108
 
109
- const sessionId = uuidv7()
110
-
111
109
  // create the terminal session in database
112
110
  const session = await database.terminalSession.create({
113
111
  data: {
114
- id: sessionId,
115
112
  terminalId,
116
113
  },
117
114
  })
@@ -4,12 +4,17 @@ import { constants } from "node:fs"
4
4
  import { access, mkdtemp, rm } from "node:fs/promises"
5
5
  import { hostname, tmpdir } from "node:os"
6
6
  import { join } from "node:path"
7
- import { PrismaLibSQL } from "@prisma/adapter-libsql"
7
+ import { PrismaLibSql } from "@prisma/adapter-libsql"
8
8
  import { generateIdentity, identityToRecipient } from "age-encryption"
9
- import { type DatabaseManager, ensureWellKnownEntitiesCreated } from "../database"
9
+ import { createProjectLogger } from "../common"
10
+ import {
11
+ type DatabaseManager,
12
+ ensureWellKnownEntitiesCreated,
13
+ migrateDatabase,
14
+ migrationPacks,
15
+ } from "../database"
10
16
  import { PrismaClient as BackendPrismaClient } from "../database/_generated/backend/sqlite/client"
11
17
  import { getInitialBackendUnlockMethodMeta } from "../database/local/backend"
12
- import { migrateDatabase } from "../database/migrate"
13
18
  import {
14
19
  type BackendDatabase as BackendDatabaseClient,
15
20
  ProjectDatabase as ProjectDatabaseClient,
@@ -58,12 +63,15 @@ export class TestDatabaseManager implements DatabaseManager {
58
63
  async _forProject(projectId: string): Promise<ProjectDatabase> {
59
64
  const tempPath = await this.createTempPath()
60
65
  const projectUrl = `file:${join(tempPath, `${projectId}.db`)}`
66
+ const logger = createProjectLogger(this.logger, projectId)
61
67
 
62
- await migrateDatabase(projectUrl, "project", undefined, this.logger)
63
-
64
- return new ProjectDatabaseClient({
65
- adapter: new PrismaLibSQL({ url: projectUrl }),
68
+ const client = new ProjectDatabaseClient({
69
+ adapter: new PrismaLibSql({ url: projectUrl }),
66
70
  })
71
+
72
+ await migrateDatabase(client, migrationPacks.project, 0, () => Promise.resolve(), logger)
73
+
74
+ return client
67
75
  }
68
76
 
69
77
  static async create(
@@ -75,20 +83,26 @@ export class TestDatabaseManager implements DatabaseManager {
75
83
  const tempPath = await mkdtemp(join(tempRoot, "highstate"))
76
84
  const backendUrl = `file:${join(tempPath, "backend.db")}`
77
85
 
78
- await migrateDatabase(backendUrl, "backend/sqlite", undefined, logger)
79
-
80
- const backend = new BackendPrismaClient({
81
- adapter: new PrismaLibSQL({ url: backendUrl }),
86
+ const backendClient = new BackendPrismaClient({
87
+ adapter: new PrismaLibSql({ url: backendUrl }),
82
88
  }) as BackendDatabaseClient
83
89
 
84
- await ensureWellKnownEntitiesCreated(backend)
90
+ await migrateDatabase(
91
+ backendClient,
92
+ migrationPacks["backend/sqlite"],
93
+ 0,
94
+ () => Promise.resolve(),
95
+ logger,
96
+ )
97
+
98
+ await ensureWellKnownEntitiesCreated(backendClient)
85
99
 
86
100
  if (isEncryptionEnabled) {
87
101
  const identity = await generateIdentity()
88
102
  const recipient = await identityToRecipient(identity)
89
103
  const meta = getInitialBackendUnlockMethodMeta(hostname())
90
104
 
91
- await backend.backendUnlockMethod.create({
105
+ await backendClient.backendUnlockMethod.create({
92
106
  data: {
93
107
  recipient,
94
108
  meta,
@@ -96,7 +110,7 @@ export class TestDatabaseManager implements DatabaseManager {
96
110
  })
97
111
  }
98
112
 
99
- return new TestDatabaseManager(backend, logger, isEncryptionEnabled)
113
+ return new TestDatabaseManager(backendClient, logger, isEncryptionEnabled)
100
114
  }
101
115
 
102
116
  private async createTempPath(): Promise<string> {