@reventlessdev/reventless-aws 3.0.0-alpha.346 → 3.0.0-alpha.347
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/CHANGELOG.md +26 -0
- package/package.json +17 -13
- package/rescript.json +2 -1
- package/run-bake-manifest.mjs +3 -0
- package/run-deploy-app.mjs +3 -0
- package/scripts/BakeManifest.res +395 -0
- package/scripts/BakeManifest.res.mjs +487 -0
- package/scripts/DeployApp.res +708 -0
- package/scripts/DeployApp.res.mjs +1015 -0
- package/scripts/DeployManifest.res +84 -0
- package/scripts/DeployManifest.res.mjs +112 -0
- package/scripts/ProvisionAccounts.res +60 -46
- package/scripts/ProvisionAccounts.res.mjs +51 -37
- package/scripts/PulumiCli.res +43 -0
- package/scripts/PulumiCli.res.mjs +106 -0
- package/src/Platform.res +19 -2
- package/src/Platform.res.mjs +12 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
- package/src/adapter/Task/TaskBucket_S3.res +1 -3
- package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
- package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
- package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
- package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
- package/src/components/Api/AppSync_SdlDecorate.res +4 -2
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
- package/src/plugin/stack/Plugin_Stack.res +4 -0
- package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
- package/src/util/Util_DeadLetterQueue.res +1 -5
- package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
- package/src/util/Util_StoreLayout.res +15 -2
- package/src/util/Util_StoreLayout.res.mjs +11 -3
- package/tests/BakeManifestTest.res +204 -0
- package/tests/BakeManifestTest.res.mjs +259 -0
- package/tests/DcbColdStartPartitionTest.res +32 -0
- package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
- package/tests/DcbUnresolvedPartitionSlice.res +28 -0
- package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
- package/tests/DeployAppTest.res +90 -0
- package/tests/DeployAppTest.res.mjs +133 -0
- package/tests/LambdaLayerLookupTest.res +104 -0
- package/tests/LambdaLayerLookupTest.res.mjs +117 -0
- package/tests/ProvisionAccountsTest.res +26 -0
- package/tests/ProvisionAccountsTest.res.mjs +31 -0
- package/tests/Util_StoreLayoutTest.res +6 -0
- package/tests/Util_StoreLayoutTest.res.mjs +7 -4
- package/tests/dcbColdStartPartition.mjs +66 -0
- package/tests/setup/layerArn.cjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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.347 (2026-09-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** make the demo accounts work on a pool that signs in by email ([97b036f](https://github.com/ReventlessDev/reventless-core/commit/97b036f86979ba55dbbf93d460fe8076c35abf1a))
|
|
11
|
+
* **aws:** stop a plugin stack that does not name its platform ([8ba09da](https://github.com/ReventlessDev/reventless-core/commit/8ba09da55b72d06b19ca6b78dd9abbc744a9b167))
|
|
12
|
+
* **aws:** the anonymous-field refusal recommended the door that was ruled out ([062c0a0](https://github.com/ReventlessDev/reventless-core/commit/062c0a08b5db239a86cc6b77d29e942f527cadae))
|
|
13
|
+
* fix(aws)!: stop a deploy that finds no Lambda layer ([62957f4](https://github.com/ReventlessDev/reventless-core/commit/62957f444ab2120f672b9ac0564f786d64090864))
|
|
14
|
+
* feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **aws:** bake the component manifest with a command, in CI and by hand ([4b0dccc](https://github.com/ReventlessDev/reventless-core/commit/4b0dccc8fc3b99874108c82db282d73cc93a48e9))
|
|
18
|
+
* **aws:** deploy and remove a whole app with deploy-app up and down ([b7d0587](https://github.com/ReventlessDev/reventless-core/commit/b7d0587bd02a4b067a3a9fa21b5f77ea5210d792))
|
|
19
|
+
* **aws:** let a stack declare itself disposable ([a8a8f29](https://github.com/ReventlessDev/reventless-core/commit/a8a8f29bccb7ed9994dea11aefb059953be4b095))
|
|
20
|
+
* **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
|
|
21
|
+
|
|
22
|
+
### BREAKING CHANGES
|
|
23
|
+
|
|
24
|
+
* PulumiAws.Lambda.reventlessLayerArn is now a function that
|
|
25
|
+
returns the ARN and throws when there is none. Use Lambda.reventlessLayers()
|
|
26
|
+
for a function's layers argument.
|
|
27
|
+
* DcbTag.derivePartitionTag is removed; a boundary whose
|
|
28
|
+
partition cannot be inferred no longer deploys.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
# 3.0.0-alpha.346 (2026-09-11)
|
|
7
33
|
|
|
8
34
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.347",
|
|
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-
|
|
24
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.12",
|
|
25
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
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/
|
|
29
|
-
"@reventlessdev/
|
|
30
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
31
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
32
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
29
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
|
|
30
|
+
"@reventlessdev/rescript-web": "1.0.0-alpha.4",
|
|
31
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.127",
|
|
32
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.136",
|
|
33
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.164",
|
|
34
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.263",
|
|
35
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.37"
|
|
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,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.
|