@reventlessdev/reventless-aws 3.0.0-alpha.346 → 3.0.0-alpha.348

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 (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +17 -13
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/scripts/BakeManifest.res +395 -0
  7. package/scripts/BakeManifest.res.mjs +487 -0
  8. package/scripts/DeployApp.res +708 -0
  9. package/scripts/DeployApp.res.mjs +1015 -0
  10. package/scripts/DeployManifest.res +84 -0
  11. package/scripts/DeployManifest.res.mjs +112 -0
  12. package/scripts/ProvisionAccounts.res +60 -46
  13. package/scripts/ProvisionAccounts.res.mjs +51 -37
  14. package/scripts/PulumiCli.res +43 -0
  15. package/scripts/PulumiCli.res.mjs +106 -0
  16. package/src/Platform.res +19 -2
  17. package/src/Platform.res.mjs +12 -4
  18. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  19. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  20. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  21. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  22. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  23. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  24. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  25. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  28. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  29. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  30. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  31. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  32. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  33. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  34. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  35. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  36. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  37. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  38. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  39. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  41. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  42. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  43. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  44. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  45. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  46. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  47. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  48. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  49. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  50. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  51. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  52. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  53. package/src/components/Api/AppSync_SdlDecorate.res +4 -2
  54. package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
  55. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  57. package/src/plugin/stack/Plugin_Stack.res +4 -0
  58. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  59. package/src/util/Util_DeadLetterQueue.res +1 -5
  60. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  61. package/src/util/Util_StoreLayout.res +15 -2
  62. package/src/util/Util_StoreLayout.res.mjs +11 -3
  63. package/tests/BakeManifestTest.res +204 -0
  64. package/tests/BakeManifestTest.res.mjs +259 -0
  65. package/tests/DcbColdStartPartitionTest.res +32 -0
  66. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  67. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  68. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  69. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  70. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  71. package/tests/DeployAppTest.res +90 -0
  72. package/tests/DeployAppTest.res.mjs +133 -0
  73. package/tests/LambdaLayerLookupTest.res +104 -0
  74. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  75. package/tests/ProvisionAccountsTest.res +26 -0
  76. package/tests/ProvisionAccountsTest.res.mjs +31 -0
  77. package/tests/Util_StoreLayoutTest.res +6 -0
  78. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  79. package/tests/dcbColdStartPartition.mjs +66 -0
  80. package/tests/setup/layerArn.cjs +5 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,40 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.348 (2026-09-20)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.347 (2026-09-16)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **aws:** make the demo accounts work on a pool that signs in by email ([97b036f](https://github.com/ReventlessDev/reventless-core/commit/97b036f86979ba55dbbf93d460fe8076c35abf1a))
19
+ * **aws:** stop a plugin stack that does not name its platform ([8ba09da](https://github.com/ReventlessDev/reventless-core/commit/8ba09da55b72d06b19ca6b78dd9abbc744a9b167))
20
+ * **aws:** the anonymous-field refusal recommended the door that was ruled out ([062c0a0](https://github.com/ReventlessDev/reventless-core/commit/062c0a08b5db239a86cc6b77d29e942f527cadae))
21
+ * fix(aws)!: stop a deploy that finds no Lambda layer ([62957f4](https://github.com/ReventlessDev/reventless-core/commit/62957f444ab2120f672b9ac0564f786d64090864))
22
+ * feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
23
+ ### Features
24
+
25
+ * **aws:** bake the component manifest with a command, in CI and by hand ([4b0dccc](https://github.com/ReventlessDev/reventless-core/commit/4b0dccc8fc3b99874108c82db282d73cc93a48e9))
26
+ * **aws:** deploy and remove a whole app with deploy-app up and down ([b7d0587](https://github.com/ReventlessDev/reventless-core/commit/b7d0587bd02a4b067a3a9fa21b5f77ea5210d792))
27
+ * **aws:** let a stack declare itself disposable ([a8a8f29](https://github.com/ReventlessDev/reventless-core/commit/a8a8f29bccb7ed9994dea11aefb059953be4b095))
28
+ * **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
29
+
30
+ ### BREAKING CHANGES
31
+
32
+ * PulumiAws.Lambda.reventlessLayerArn is now a function that
33
+ returns the ARN and throws when there is none. Use Lambda.reventlessLayers()
34
+ for a function's layers argument.
35
+ * DcbTag.derivePartitionTag is removed; a boundary whose
36
+ partition cannot be inferred no longer deploys.
37
+
38
+
39
+
6
40
  # 3.0.0-alpha.346 (2026-09-11)
7
41
 
8
42
  ### Bug Fixes
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.346",
3
+ "version": "3.0.0-alpha.348",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
7
7
  "provision-identity": "./run-provision-identity.mjs",
8
8
  "provision-admin": "./run-provision-admin.mjs",
9
- "provision-accounts": "./run-provision-accounts.mjs"
9
+ "provision-accounts": "./run-provision-accounts.mjs",
10
+ "bake-manifest": "./run-bake-manifest.mjs",
11
+ "deploy-app": "./run-deploy-app.mjs"
10
12
  },
11
13
  "dependencies": {
12
14
  "@aws-sdk/client-appsync": "3.970.0",
@@ -18,18 +20,19 @@
18
20
  "sury": "11.0.0-rc.2",
19
21
  "uuid": "^13.0.0",
20
22
  "yaml": "^2.8.3",
21
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.18",
22
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
23
- "@reventlessdev/rescript-node": "2.0.0-alpha.11",
24
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.13",
25
23
  "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
26
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.20",
24
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
25
+ "@reventlessdev/rescript-node": "2.0.0-alpha.12",
26
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.14",
27
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.19",
27
28
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
28
- "@reventlessdev/reventless-core": "3.0.0-alpha.262",
29
- "@reventlessdev/reventless-interop": "3.0.0-alpha.36",
30
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.126",
31
- "@reventlessdev/reventless-spec": "3.0.0-alpha.135",
32
- "@reventlessdev/reventless-infra": "3.0.0-alpha.163"
29
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
30
+ "@reventlessdev/rescript-web": "1.0.0-alpha.4",
31
+ "@reventlessdev/reventless-core": "3.0.0-alpha.264",
32
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.165",
33
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.137",
34
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.37",
35
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.128"
33
36
  },
34
37
  "devDependencies": {
35
38
  "rescript": "12.3.0",
@@ -40,7 +43,8 @@
40
43
  },
41
44
  "jest": {
42
45
  "setupFiles": [
43
- "<rootDir>/jest.setup.cjs"
46
+ "<rootDir>/jest.setup.cjs",
47
+ "<rootDir>/tests/setup/layerArn.cjs"
44
48
  ],
45
49
  "testMatch": [
46
50
  "<rootDir>/tests/**/*Test.res.mjs"
package/rescript.json CHANGED
@@ -38,7 +38,8 @@
38
38
  "@reventlessdev/reventless-interop",
39
39
  "@reventlessdev/reventless-spec",
40
40
  "@reventlessdev/reventless-core",
41
- "@reventlessdev/reventless-postgres"
41
+ "@reventlessdev/reventless-postgres",
42
+ "@reventlessdev/rescript-web"
42
43
  ],
43
44
  "compiler-flags": [],
44
45
  "suffix": ".res.mjs"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./scripts/BakeManifest.res.mjs"
3
+ await main()
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./scripts/DeployApp.res.mjs"
3
+ await main()
@@ -0,0 +1,395 @@
1
+ /***
2
+ Bake the component manifest — the file that tells the web app which pages to
3
+ show — once every plugin stack of a deployment is up.
4
+
5
+ ```
6
+ pnpm exec bake-manifest --stack alpha
7
+ ```
8
+
9
+ The manifest describes the whole deployment, so no single stack's deploy is the
10
+ moment it is settled. Without it every user but an administrator sees an empty
11
+ web app: the discovery query the file stands in for is Admin-gated.
12
+
13
+ Registration is asynchronous — a plugin stack publishes a re-detect, the plugin
14
+ answers, the Plugin read model projects — so the bake function refuses to write
15
+ until each plugin's row holds the structure its stack exported, and this command
16
+ asks again while one is still on its way.
17
+
18
+ A deployment that declares no bake exports no `bakedManifestFunction`, and the
19
+ command does nothing.
20
+ */
21
+
22
+ module Lambda = AwsSdk.Lambda
23
+
24
+ // ── Arguments ────────────────────────────────────────────────────────────────
25
+
26
+ type args = {
27
+ manifest: option<string>,
28
+ stack: option<string>,
29
+ since: option<string>,
30
+ help: bool,
31
+ }
32
+
33
+ let parseArgs = (argv: array<string>): result<args, string> => {
34
+ let acc = ref(Ok({manifest: None, stack: None, since: None, help: false}))
35
+ let i = ref(0)
36
+ let count = argv->Array.length
37
+ while i.contents < count {
38
+ let flag = argv->Array.getUnsafe(i.contents)
39
+ let value = argv->Array.get(i.contents + 1)
40
+ switch (acc.contents, flag, value) {
41
+ | (Error(_), _, _) => i := count
42
+ | (Ok(a), "--manifest", Some(v)) =>
43
+ acc := Ok({...a, manifest: Some(v)})
44
+ i := i.contents + 2
45
+ | (Ok(a), "--stack", Some(v)) =>
46
+ acc := Ok({...a, stack: Some(v)})
47
+ i := i.contents + 2
48
+ | (Ok(a), "--since", Some(v)) =>
49
+ acc := Ok({...a, since: v == "" ? None : Some(v)})
50
+ i := i.contents + 2
51
+ | (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
52
+ acc := Ok({...a, help: true})
53
+ i := i.contents + 1
54
+ | (Ok(_), "--manifest", None) | (Ok(_), "--stack", None) | (Ok(_), "--since", None) =>
55
+ acc := Error(`${flag} needs a value`)
56
+ | (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
57
+ }
58
+ }
59
+ acc.contents
60
+ }
61
+
62
+ let usage = `
63
+ Bake the component manifest of a deployed platform.
64
+
65
+ --manifest <path> The deploy manifest. Defaults to ${DeployManifest.defaultFile}
66
+ in the working directory.
67
+ --stack <name> The stack to bake. Defaults to the stack selected in the
68
+ platform's folder.
69
+ --since <instant> When this deploy started (ISO 8601). Lets the report tell a
70
+ plugin that re-registered from one that was unchanged.
71
+
72
+ Reads the bake function from the platform stack and each plugin's structure key
73
+ from its stack, then asks the function to bake — again every
74
+ 15 seconds, up to 20 times, while a plugin's registration has
75
+ not arrived. A registration this deploy wrote with another key cannot arrive by
76
+ waiting, so that stops the run at once.
77
+ `
78
+
79
+ // ── What the stacks say ──────────────────────────────────────────────────────
80
+
81
+ type target = {functionName: string, bucket: string, key: string}
82
+
83
+ /** `None` when the platform declares no bake. */
84
+ let targetOf = (outputs: dict<JSON.t>): option<target> =>
85
+ switch (
86
+ outputs->PulumiCli.stringOutput("bakedManifestFunction"),
87
+ outputs->PulumiCli.stringOutput("bakedManifestBucket"),
88
+ ) {
89
+ | (Some(functionName), Some(bucket)) =>
90
+ Some({
91
+ functionName,
92
+ bucket,
93
+ key: outputs->PulumiCli.stringOutput("bakedManifestKey")->Option.getOr(""),
94
+ })
95
+ | _ => None
96
+ }
97
+
98
+ /** The plugin name and structure key a plugin stack exported. A stack deployed
99
+ before this output existed exports nothing and is not waited for. */
100
+ let structureRefOf = (outputs: dict<JSON.t>): option<(string, string)> =>
101
+ switch outputs->Dict.get("pluginStructureRef")->Option.flatMap(JSON.Decode.object) {
102
+ | Some(ref) =>
103
+ switch (
104
+ ref->Dict.get("plugin")->Option.flatMap(JSON.Decode.string),
105
+ ref->Dict.get("key")->Option.flatMap(JSON.Decode.string),
106
+ ) {
107
+ | (Some(plugin), Some(key)) => Some((plugin, key))
108
+ | _ => None
109
+ }
110
+ | None => None
111
+ }
112
+
113
+ /** The key names the default file; a deployment that curates a surface per
114
+ audience writes one file per journey beside it, under keys the function
115
+ already knows. */
116
+ let payload = (~target: target, ~expect: dict<string>, ~since: option<string>): JSON.t => {
117
+ let fields = [
118
+ ("bake", JSON.Encode.bool(true)),
119
+ ("bucket", JSON.Encode.string(target.bucket)),
120
+ ("key", JSON.Encode.string(target.key)),
121
+ ("expect", expect->Dict.mapValues(JSON.Encode.string)->JSON.Encode.object),
122
+ ]
123
+ since->Option.forEach(s => fields->Array.push(("since", JSON.Encode.string(s))))
124
+ fields->Dict.fromArray->JSON.Encode.object
125
+ }
126
+
127
+ // ── What the bake function answers ───────────────────────────────────────────
128
+
129
+ type registration = {
130
+ plugin: string,
131
+ state: string,
132
+ expected: string,
133
+ found: option<string>,
134
+ writtenAt: option<string>,
135
+ }
136
+
137
+ type summary = {plugins: int, registered: int, unchanged: int, matched: int}
138
+
139
+ type written = {bucket: string, key: string, bytes: int, group: option<string>}
140
+
141
+ type answer =
142
+ | Baked({written: array<written>, summary: option<summary>, registrations: array<registration>})
143
+ | Pending({pending: array<string>, registrations: array<registration>})
144
+
145
+ let _string = (o, name) => o->Dict.get(name)->Option.flatMap(JSON.Decode.string)
146
+ let _int = (o, name) =>
147
+ o->Dict.get(name)->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt)
148
+
149
+ let _registrations = (o: dict<JSON.t>): array<registration> =>
150
+ o
151
+ ->Dict.get("registrations")
152
+ ->Option.flatMap(JSON.Decode.array)
153
+ ->Option.getOr([])
154
+ ->Array.filterMap(JSON.Decode.object)
155
+ ->Array.map(r => {
156
+ plugin: r->_string("plugin")->Option.getOr("?"),
157
+ state: r->_string("state")->Option.getOr("?"),
158
+ expected: r->_string("expected")->Option.getOr("?"),
159
+ found: r->_string("found"),
160
+ writtenAt: r->_string("writtenAt"),
161
+ })
162
+
163
+ let readAnswer = (json: JSON.t): result<answer, string> => {
164
+ let entries = json->JSON.Decode.array->Option.getOr([])->Array.filterMap(JSON.Decode.object)
165
+ switch entries->Array.get(0) {
166
+ | None => Error(`the bake function answered ${JSON.stringify(json)}`)
167
+ | Some(first) if first->Dict.get("baked") == Some(JSON.Boolean(false)) =>
168
+ Ok(
169
+ Pending({
170
+ pending: first
171
+ ->Dict.get("pending")
172
+ ->Option.flatMap(JSON.Decode.array)
173
+ ->Option.getOr([])
174
+ ->Array.filterMap(JSON.Decode.string),
175
+ registrations: _registrations(first),
176
+ }),
177
+ )
178
+ | Some(_) =>
179
+ let summaryEntry = entries->Array.find(e => e->Dict.get("summary") == Some(JSON.Boolean(true)))
180
+ Ok(
181
+ Baked({
182
+ written: entries
183
+ ->Array.filter(e => e->Dict.get("baked") == Some(JSON.Boolean(true)))
184
+ ->Array.map(e => {
185
+ bucket: e->_string("bucket")->Option.getOr("?"),
186
+ key: e->_string("key")->Option.getOr("?"),
187
+ bytes: e->_int("bytes"),
188
+ group: e->_string("group"),
189
+ }),
190
+ summary: summaryEntry->Option.map(s => {
191
+ plugins: s->_int("plugins"),
192
+ registered: s->_int("registered"),
193
+ unchanged: s->_int("unchanged"),
194
+ matched: s->_int("matched"),
195
+ }),
196
+ registrations: summaryEntry->Option.mapOr([], _registrations),
197
+ }),
198
+ )
199
+ }
200
+ }
201
+
202
+ /** Both halves of a plugin's comparison and the row's date — what separates the
203
+ causes on sight. */
204
+ let registrationLine = (r: registration): string =>
205
+ ` ${r.plugin}: ${r.state} — expected ${r.expected}, row holds ${r.found->Option.getOr(
206
+ "no row",
207
+ )} (written ${r.writtenAt->Option.getOr("never")})`
208
+
209
+ /** A registration this deploy already wrote with another key. Waiting does not
210
+ change it. `missing` is waited for: on a first deploy no plugin has a row
211
+ until its registration lands. */
212
+ let unreachable = (registrations: array<registration>): array<registration> =>
213
+ registrations->Array.filter(r => r.state == "diverged")
214
+
215
+ // ── Asking until it is settled ───────────────────────────────────────────────
216
+
217
+ type settled = {
218
+ written: array<written>,
219
+ summary: option<summary>,
220
+ registrations: array<registration>,
221
+ }
222
+
223
+ /** Asks `invoke` until it bakes, a registration becomes unreachable, or the
224
+ attempts run out. Every attempt's registrations are printed through `log`. */
225
+ let converge = async (
226
+ ~invoke: unit => promise<result<JSON.t, string>>,
227
+ ~sleep: unit => promise<unit>,
228
+ ~log: string => unit,
229
+ ~attempts: int,
230
+ ): result<settled, string> => {
231
+ let rec attempt = async n =>
232
+ switch await invoke() {
233
+ | Error(_) as e => e
234
+ | Ok(json) =>
235
+ switch readAnswer(json) {
236
+ | Error(_) as e => e
237
+ | Ok(Baked({written, summary, registrations})) => Ok({written, summary, registrations})
238
+ | Ok(Pending({pending, registrations})) =>
239
+ registrations->Array.forEach(r => log(registrationLine(r)))
240
+ switch unreachable(registrations) {
241
+ | [] if n < attempts =>
242
+ log(
243
+ `Attempt ${n->Int.toString}: registration has not caught up yet — ${pending->Array.join(
244
+ ", ",
245
+ )}`,
246
+ )
247
+ await sleep()
248
+ await attempt(n + 1)
249
+ | [] =>
250
+ Error(
251
+ `registration never caught up with this deploy (${pending->Array.join(
252
+ ", ",
253
+ )}); the manifest would describe the previous one, so nothing was written`,
254
+ )
255
+ | diverged =>
256
+ Error(
257
+ `${diverged
258
+ ->Array.map(r => r.plugin)
259
+ ->Array.join(
260
+ ", ",
261
+ )} registered during this deploy with a different structure than its stack exported; waiting will not change that, so nothing was written`,
262
+ )
263
+ }
264
+ }
265
+ }
266
+ await attempt(1)
267
+ }
268
+
269
+ // ── Main ─────────────────────────────────────────────────────────────────────
270
+
271
+ let attempts = 20
272
+ let intervalMs = 15_000
273
+
274
+ let inGitHubActions = () => NodeProcess.env->Dict.get("GITHUB_ACTIONS") == Some("true")
275
+
276
+ let invokeWith = (client: Lambda.client, ~functionName: string, ~payload: JSON.t) =>
277
+ async () =>
278
+ switch await Lambda.InvokeCommand.make({
279
+ functionName,
280
+ payload: payload->JSON.stringify->NodeBuffer.fromStringUtf8,
281
+ })->Lambda.InvokeCommand.send(client, _) {
282
+ | output =>
283
+ let text =
284
+ output.payload->Option.mapOr("", bytes =>
285
+ bytes->NodeBuffer.fromBytes->NodeBuffer.toStringUtf8
286
+ )
287
+ switch output.functionError {
288
+ | Some(_) => Error(`baking the component manifest failed: ${text}`)
289
+ | None =>
290
+ switch JSON.parseOrThrow(text) {
291
+ | json => Ok(json)
292
+ | exception _ => Error(`the bake function answered something that is not JSON: ${text}`)
293
+ }
294
+ }
295
+ | exception exn => Error(`could not invoke ${functionName}: ${Util_AwsError.describe(exn)}`)
296
+ }
297
+
298
+ let bake = async (~manifest: DeployManifest.resolved, ~stack: string, ~since: option<string>) =>
299
+ switch PulumiCli.stackOutputs(~dir=manifest.platform.dir, ~stack) {
300
+ | Error(_) as e => e
301
+ | Ok(outputs) =>
302
+ switch targetOf(outputs) {
303
+ | None =>
304
+ Console.log("This platform declares no baked manifest — nothing to bake.")
305
+ Ok()
306
+ | Some(target) =>
307
+ let refs = []
308
+ manifest.plugins->Array.forEach(p =>
309
+ if PulumiCli.hasStackFile(~dir=p.dir, ~stack) {
310
+ switch PulumiCli.stackOutputs(~dir=p.dir, ~stack) {
311
+ | Ok(o) => o->structureRefOf->Option.forEach(r => refs->Array.push(r))
312
+ | Error(message) => Console.error(`${p.name}: ${message} — not waited for`)
313
+ }
314
+ }
315
+ )
316
+ let expect = refs->Dict.fromArray
317
+ Console.log(
318
+ `Expecting: ${expect
319
+ ->Dict.mapValues(JSON.Encode.string)
320
+ ->JSON.Encode.object
321
+ ->JSON.stringify}`,
322
+ )
323
+ Console.log(`Baking ${target.bucket}/${target.key} via ${target.functionName}`)
324
+ let client = Lambda.client(~region=?manifest.region, ())
325
+ switch await converge(
326
+ ~invoke=invokeWith(
327
+ client,
328
+ ~functionName=target.functionName,
329
+ ~payload=payload(~target, ~expect, ~since),
330
+ ),
331
+ ~sleep=() =>
332
+ Promise.make((resolve, _) => {
333
+ let _ = setTimeout(() => resolve(), intervalMs)
334
+ }),
335
+ ~log=Console.log,
336
+ ~attempts,
337
+ ) {
338
+ | Error(_) as e => e
339
+ | Ok({written, summary, registrations}) =>
340
+ written->Array.forEach(w =>
341
+ Console.log(
342
+ `Wrote ${w.bucket}/${w.key} (${w.bytes->Int.toString} bytes${w.group->Option.mapOr(
343
+ "",
344
+ g => `, for ${g}`,
345
+ )})`,
346
+ )
347
+ )
348
+ registrations->Array.forEach(r => Console.log(registrationLine(r)))
349
+ summary->Option.forEach(s => {
350
+ Console.log(
351
+ `${s.registered->Int.toString}/${s.plugins->Int.toString} plugin(s) re-registered during this deploy (${s.unchanged->Int.toString} unchanged, ${s.matched->Int.toString} undated).`,
352
+ )
353
+
354
+ // A bake where nothing re-registered proved the manifest current and
355
+ // nothing about the chain that keeps it current.
356
+ if s.registered == 0 {
357
+ let note = "The manifest is current, but no plugin exercised the registration chain on this deploy — it is unverified, not proven working."
358
+ Console.log(inGitHubActions() ? `::notice::${note}` : `note: ${note}`)
359
+ }
360
+ })
361
+ Ok()
362
+ }
363
+ }
364
+ }
365
+
366
+ let run = async (): result<unit, string> =>
367
+ switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
368
+ | Error(_) as e => e
369
+ | Ok(args) if args.help =>
370
+ Console.log(usage)
371
+ Ok()
372
+ | Ok(args) =>
373
+ switch DeployManifest.load(args.manifest->Option.getOr(DeployManifest.defaultFile)) {
374
+ | Error(_) as e => e
375
+ | Ok(manifest) =>
376
+ switch args.stack->Option.orElse(PulumiCli.selectedStack(~dir=manifest.platform.dir)) {
377
+ | None => Error(`no stack selected in ${manifest.platform.dir} — pass --stack`)
378
+ | Some(stack) => await bake(~manifest, ~stack, ~since=args.since)
379
+ }
380
+ }
381
+ }
382
+
383
+ let main = async () =>
384
+ switch await run() {
385
+ | Ok() => ()
386
+ | Error(message) =>
387
+ Console.error(inGitHubActions() ? `::error::${message}` : `bake-manifest: ${message}`)
388
+ NodeProcess.exit(1)
389
+ | exception exn =>
390
+ Console.error(`bake-manifest: ${Util_AwsError.describe(exn)}`)
391
+ NodeProcess.exit(1)
392
+ }
393
+
394
+ // No top-level call: `../run-bake-manifest.mjs` invokes [main], so a test can
395
+ // import this module.