@reventlessdev/reventless-aws 3.0.0-alpha.371 → 3.0.0-alpha.373
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 +18 -0
- package/package.json +7 -6
- package/run-clear-pending-creates.mjs +3 -0
- package/scripts/BakeManifest.res +31 -10
- package/scripts/BakeManifest.res.mjs +20 -8
- package/scripts/ClearPendingCreates.res +142 -0
- package/scripts/ClearPendingCreates.res.mjs +187 -0
- package/scripts/DeployApp.res +25 -2
- package/scripts/DeployApp.res.mjs +19 -3
- package/scripts/DeployManifest.res +16 -1
- package/scripts/DeployManifest.res.mjs +9 -3
- package/tests/BakeManifestTest.res +52 -2
- package/tests/BakeManifestTest.res.mjs +46 -3
- package/tests/ClearPendingCreatesTest.res +93 -0
- package/tests/ClearPendingCreatesTest.res.mjs +99 -0
- package/tests/DeployAppTest.res +11 -0
- package/tests/DeployAppTest.res.mjs +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.373 (2026-10-05)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **deploy:** the bake waits for a review's plugins, and never follows a cancelled deploy ([84bdfbd](https://github.com/ReventlessDev/reventless-core/commit/84bdfbd13a892ceb9741a573e7928328e45f9d79))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.372 (2026-10-05)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **deploy:** the pending-creates recovery runs from reventless-aws, and works ([b9e749e](https://github.com/ReventlessDev/reventless-core/commit/b9e749eb09354a559505fa6facc7f58e38afd2a9))
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **deploy:** a plugin names its own region, and can stay out of review environments ([7edd318](https://github.com/ReventlessDev/reventless-core/commit/7edd3187f51f74e57c331b1cb417b17de8067b58))
|
|
21
|
+
* **deploy:** the deploy workflow names its state backend ([a370346](https://github.com/ReventlessDev/reventless-core/commit/a3703461bae9dc22c098315a09bbabcf0c5816b1))
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.371 (2026-10-01)
|
|
7
25
|
|
|
8
26
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.373",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"provision-accounts": "./run-provision-accounts.mjs",
|
|
10
10
|
"bake-manifest": "./run-bake-manifest.mjs",
|
|
11
11
|
"deploy-app": "./run-deploy-app.mjs",
|
|
12
|
-
"resolve-environment": "./run-resolve-environment.mjs"
|
|
12
|
+
"resolve-environment": "./run-resolve-environment.mjs",
|
|
13
|
+
"clear-pending-creates": "./run-clear-pending-creates.mjs"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@aws-sdk/client-appsync": "3.970.0",
|
|
@@ -22,18 +23,18 @@
|
|
|
22
23
|
"uuid": "^13.0.0",
|
|
23
24
|
"yaml": "^2.8.3",
|
|
24
25
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.28",
|
|
26
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
25
27
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
26
28
|
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
27
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
28
29
|
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.23",
|
|
29
30
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
30
31
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
31
32
|
"@reventlessdev/rescript-web": "1.0.0-alpha.7",
|
|
32
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.185",
|
|
33
33
|
"@reventlessdev/reventless-core": "3.0.0-alpha.287",
|
|
34
34
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
|
|
35
|
-
"@reventlessdev/reventless-
|
|
36
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.39"
|
|
35
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.185",
|
|
36
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.39",
|
|
37
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.157"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"rescript": "12.3.1",
|
package/scripts/BakeManifest.res
CHANGED
|
@@ -27,21 +27,23 @@ type args = {
|
|
|
27
27
|
manifest: option<string>,
|
|
28
28
|
stack: option<string>,
|
|
29
29
|
since: option<string>,
|
|
30
|
+
review: bool,
|
|
30
31
|
help: bool,
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
34
|
-
Reventless.CliArgs.parse(~strings=["manifest", "stack", "since"], argv)
|
|
35
|
+
Reventless.CliArgs.parse(~strings=["manifest", "stack", "since"], ~bools=["review"], argv)
|
|
35
36
|
->Result.flatMap(Reventless.CliArgs.noPositionals)
|
|
36
37
|
->Result.map(a => {
|
|
37
38
|
manifest: a->Reventless.CliArgs.string("manifest"),
|
|
38
39
|
stack: a->Reventless.CliArgs.string("stack"),
|
|
39
40
|
since: a->Reventless.CliArgs.string("since")->Option.flatMap(v => v == "" ? None : Some(v)),
|
|
41
|
+
review: a->Reventless.CliArgs.bool("review"),
|
|
40
42
|
help: a->Reventless.CliArgs.help,
|
|
41
43
|
})
|
|
42
44
|
|
|
43
45
|
let usage = `
|
|
44
|
-
Usage: bake-manifest [--manifest <path>] [--stack <name>] [--since <instant>]
|
|
46
|
+
Usage: bake-manifest [--manifest <path>] [--stack <name>] [--since <instant>] [--review]
|
|
45
47
|
|
|
46
48
|
Bake the component manifest of a deployed platform.
|
|
47
49
|
|
|
@@ -51,6 +53,9 @@ Bake the component manifest of a deployed platform.
|
|
|
51
53
|
platform's folder.
|
|
52
54
|
--since <instant> When this deploy started (ISO 8601). Lets the report tell a
|
|
53
55
|
plugin that re-registered from one that was unchanged.
|
|
56
|
+
--review The stack is a pull request's review environment. Its stacks'
|
|
57
|
+
settings are not in this checkout, so every plugin with a
|
|
58
|
+
review stack is waited for, not only those with a file here.
|
|
54
59
|
|
|
55
60
|
Reads the bake function from the platform stack and each plugin's structure key
|
|
56
61
|
from its stack, then asks the function to bake — again every
|
|
@@ -61,6 +66,17 @@ waiting, so that stops the run at once.
|
|
|
61
66
|
|
|
62
67
|
// ── What the stacks say ──────────────────────────────────────────────────────
|
|
63
68
|
|
|
69
|
+
/** The plugins whose registration the bake waits for. A stack with no settings
|
|
70
|
+
file here was never deployed, except in a review environment, whose settings
|
|
71
|
+
exist only in the runner that created it: there it is every plugin with a
|
|
72
|
+
review stack. */
|
|
73
|
+
let pluginsToRead = (
|
|
74
|
+
plugins: array<DeployManifest.project>,
|
|
75
|
+
~stack: string,
|
|
76
|
+
~review: bool,
|
|
77
|
+
~hasStackFile: (~dir: string, ~stack: string) => bool,
|
|
78
|
+
) => plugins->Array.filter(p => review ? p.review : hasStackFile(~dir=p.dir, ~stack))
|
|
79
|
+
|
|
64
80
|
type target = {functionName: string, bucket: string, key: string}
|
|
65
81
|
|
|
66
82
|
/** `None` when the platform declares no bake. */
|
|
@@ -278,7 +294,12 @@ let invokeWith = (client: Lambda.client, ~functionName: string, ~payload: JSON.t
|
|
|
278
294
|
| exception exn => Error(`could not invoke ${functionName}: ${Util_AwsError.describe(exn)}`)
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
let bake = async (
|
|
297
|
+
let bake = async (
|
|
298
|
+
~manifest: DeployManifest.resolved,
|
|
299
|
+
~stack: string,
|
|
300
|
+
~since: option<string>,
|
|
301
|
+
~review: bool,
|
|
302
|
+
) =>
|
|
282
303
|
switch PulumiCli.stackOutputs(~dir=manifest.platform.dir, ~stack) {
|
|
283
304
|
| Error(_) as e => e
|
|
284
305
|
| Ok(outputs) =>
|
|
@@ -288,12 +309,12 @@ let bake = async (~manifest: DeployManifest.resolved, ~stack: string, ~since: op
|
|
|
288
309
|
Ok()
|
|
289
310
|
| Some(target) =>
|
|
290
311
|
let refs = []
|
|
291
|
-
manifest.plugins
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
manifest.plugins
|
|
313
|
+
->pluginsToRead(~stack, ~review, ~hasStackFile=PulumiCli.hasStackFile)
|
|
314
|
+
->Array.forEach(p =>
|
|
315
|
+
switch PulumiCli.stackOutputs(~dir=p.dir, ~stack) {
|
|
316
|
+
| Ok(o) => o->structureRefOf->Option.forEach(r => refs->Array.push(r))
|
|
317
|
+
| Error(message) => Console.error(`${p.name}: ${message} — not waited for`)
|
|
297
318
|
}
|
|
298
319
|
)
|
|
299
320
|
let expect = refs->Dict.fromArray
|
|
@@ -352,7 +373,7 @@ let run = async (args: args): result<unit, string> =>
|
|
|
352
373
|
| Ok(manifest) =>
|
|
353
374
|
switch args.stack->Option.orElse(PulumiCli.selectedStack(~dir=manifest.platform.dir)) {
|
|
354
375
|
| None => Error(`no stack selected in ${manifest.platform.dir} — pass --stack`)
|
|
355
|
-
| Some(stack) => await bake(~manifest, ~stack, ~since=args.since)
|
|
376
|
+
| Some(stack) => await bake(~manifest, ~stack, ~since=args.since, ~review=args.review)
|
|
356
377
|
}
|
|
357
378
|
}
|
|
358
379
|
|
|
@@ -19,7 +19,7 @@ function parseArgs(argv) {
|
|
|
19
19
|
"manifest",
|
|
20
20
|
"stack",
|
|
21
21
|
"since"
|
|
22
|
-
],
|
|
22
|
+
], ["review"], undefined, undefined, argv), CliArgs$Reventless.noPositionals), a => ({
|
|
23
23
|
manifest: CliArgs$Reventless.string(a, "manifest"),
|
|
24
24
|
stack: CliArgs$Reventless.string(a, "stack"),
|
|
25
25
|
since: Stdlib_Option.flatMap(CliArgs$Reventless.string(a, "since"), v => {
|
|
@@ -29,12 +29,13 @@ function parseArgs(argv) {
|
|
|
29
29
|
return v;
|
|
30
30
|
}
|
|
31
31
|
}),
|
|
32
|
+
review: CliArgs$Reventless.bool(a, "review"),
|
|
32
33
|
help: CliArgs$Reventless.help(a)
|
|
33
34
|
}));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
let usage = `
|
|
37
|
-
Usage: bake-manifest [--manifest <path>] [--stack <name>] [--since <instant>]
|
|
38
|
+
Usage: bake-manifest [--manifest <path>] [--stack <name>] [--since <instant>] [--review]
|
|
38
39
|
|
|
39
40
|
Bake the component manifest of a deployed platform.
|
|
40
41
|
|
|
@@ -44,6 +45,9 @@ Bake the component manifest of a deployed platform.
|
|
|
44
45
|
platform's folder.
|
|
45
46
|
--since <instant> When this deploy started (ISO 8601). Lets the report tell a
|
|
46
47
|
plugin that re-registered from one that was unchanged.
|
|
48
|
+
--review The stack is a pull request's review environment. Its stacks'
|
|
49
|
+
settings are not in this checkout, so every plugin with a
|
|
50
|
+
review stack is waited for, not only those with a file here.
|
|
47
51
|
|
|
48
52
|
Reads the bake function from the platform stack and each plugin's structure key
|
|
49
53
|
from its stack, then asks the function to bake — again every
|
|
@@ -52,6 +56,16 @@ not arrived. A registration this deploy wrote with another key cannot arrive by
|
|
|
52
56
|
waiting, so that stops the run at once.
|
|
53
57
|
`;
|
|
54
58
|
|
|
59
|
+
function pluginsToRead(plugins, stack, review, hasStackFile) {
|
|
60
|
+
return plugins.filter(p => {
|
|
61
|
+
if (review) {
|
|
62
|
+
return p.review;
|
|
63
|
+
} else {
|
|
64
|
+
return hasStackFile(p.dir, stack);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
function targetOf(outputs) {
|
|
56
70
|
let match = PulumiCli$ReventlessAws.stringOutput(outputs, "bakedManifestFunction");
|
|
57
71
|
let match$1 = PulumiCli$ReventlessAws.stringOutput(outputs, "bakedManifestBucket");
|
|
@@ -269,7 +283,7 @@ function invokeWith(client, functionName, payload) {
|
|
|
269
283
|
};
|
|
270
284
|
}
|
|
271
285
|
|
|
272
|
-
async function bake(manifest, stack, since) {
|
|
286
|
+
async function bake(manifest, stack, since, review) {
|
|
273
287
|
let e = PulumiCli$ReventlessAws.stackOutputs(manifest.platform.dir, stack);
|
|
274
288
|
if (e.TAG !== "Ok") {
|
|
275
289
|
return e;
|
|
@@ -277,10 +291,7 @@ async function bake(manifest, stack, since) {
|
|
|
277
291
|
let target = targetOf(e._0);
|
|
278
292
|
if (target !== undefined) {
|
|
279
293
|
let refs = [];
|
|
280
|
-
manifest.plugins.forEach(p => {
|
|
281
|
-
if (!PulumiCli$ReventlessAws.hasStackFile(p.dir, stack)) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
294
|
+
pluginsToRead(manifest.plugins, stack, review, PulumiCli$ReventlessAws.hasStackFile).forEach(p => {
|
|
284
295
|
let o = PulumiCli$ReventlessAws.stackOutputs(p.dir, stack);
|
|
285
296
|
if (o.TAG === "Ok") {
|
|
286
297
|
return Stdlib_Option.forEach(structureRefOf(o._0), r => {
|
|
@@ -336,7 +347,7 @@ async function run(args) {
|
|
|
336
347
|
let manifest = e._0;
|
|
337
348
|
let stack = Stdlib_Option.orElse(args.stack, PulumiCli$ReventlessAws.selectedStack(manifest.platform.dir));
|
|
338
349
|
if (stack !== undefined) {
|
|
339
|
-
return await bake(manifest, stack, args.since);
|
|
350
|
+
return await bake(manifest, stack, args.since, args.review);
|
|
340
351
|
} else {
|
|
341
352
|
return {
|
|
342
353
|
TAG: "Error",
|
|
@@ -381,6 +392,7 @@ export {
|
|
|
381
392
|
Lambda,
|
|
382
393
|
parseArgs,
|
|
383
394
|
usage,
|
|
395
|
+
pluginsToRead,
|
|
384
396
|
targetOf,
|
|
385
397
|
structureRefOf,
|
|
386
398
|
payload,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Before `pulumi up`, recovers a stack an interrupted deploy left behind: releases a
|
|
3
|
+
stale lock, drops the pending creates known to have nothing in AWS behind them,
|
|
4
|
+
and refreshes. Any other pending operation stops it: `--clear-pending-creates`
|
|
5
|
+
drops them all, and would orphan a function or a role that does exist.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Resource types with nothing in AWS behind them, so a pending create of one is
|
|
9
|
+
safe to drop. Add one only when that holds; a refusal costs one look, an
|
|
10
|
+
orphan stays until someone finds it. */
|
|
11
|
+
let safeTypes = [
|
|
12
|
+
// ACM's wait for DNS validation, which gives up after 1h15m when the record is late.
|
|
13
|
+
"aws:acm/certificateValidation:CertificateValidation",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
type pending = {urn: string, resourceType: string, operation: string}
|
|
17
|
+
|
|
18
|
+
let pendingOperations = (export: JSON.t): array<pending> => {
|
|
19
|
+
let field = (json, name) => json->JSON.Decode.object->Option.flatMap(o => o->Dict.get(name))
|
|
20
|
+
let text = (json, name) => json->field(name)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
21
|
+
export
|
|
22
|
+
->field("deployment")
|
|
23
|
+
->Option.flatMap(deployment => deployment->field("pending_operations"))
|
|
24
|
+
->Option.flatMap(JSON.Decode.array)
|
|
25
|
+
->Option.getOr([])
|
|
26
|
+
->Array.map(operation => {
|
|
27
|
+
let resource = operation->field("resource")->Option.getOr(JSON.Null)
|
|
28
|
+
{
|
|
29
|
+
urn: resource->text("urn"),
|
|
30
|
+
resourceType: resource->text("type"),
|
|
31
|
+
operation: operation->text("type"),
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let isSafe = (pending: pending) => safeTypes->Array.includes(pending.resourceType)
|
|
37
|
+
|
|
38
|
+
type args = {stack: string, cwd: string}
|
|
39
|
+
|
|
40
|
+
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
41
|
+
Reventless.CliArgs.parse(~strings=["stack", "cwd"], argv)
|
|
42
|
+
->Result.flatMap(Reventless.CliArgs.noPositionals)
|
|
43
|
+
->Result.flatMap(a =>
|
|
44
|
+
switch a->Reventless.CliArgs.string("stack") {
|
|
45
|
+
| Some(stack) =>
|
|
46
|
+
Ok({stack, cwd: a->Reventless.CliArgs.string("cwd")->Option.getOr(NodeProcess.cwd())})
|
|
47
|
+
| None => Error("--stack names the stack to recover")
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
let usage = `
|
|
52
|
+
Usage: clear-pending-creates --stack <name> [--cwd <dir>]
|
|
53
|
+
|
|
54
|
+
Recovers a stack before \`pulumi up\`: cancels a stale update, drops the pending
|
|
55
|
+
creates of types with nothing in AWS behind them, and refreshes. Any other pending
|
|
56
|
+
operation fails the command, for someone to resolve by hand.
|
|
57
|
+
|
|
58
|
+
--stack <name> The stack.
|
|
59
|
+
--cwd <dir> The Pulumi project. Defaults to the working directory.
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
let pulumi = (args: args, command: array<string>) =>
|
|
63
|
+
NodeChildProcess.spawnSync(
|
|
64
|
+
"pulumi",
|
|
65
|
+
command->Array.concat(["--stack", args.stack, "--cwd", args.cwd, "--non-interactive"]),
|
|
66
|
+
{encoding: "utf8", maxBuffer: 256 * 1024 * 1024},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
let succeeded = (result: NodeChildProcess.spawnSyncResult) =>
|
|
70
|
+
result.status->Nullable.toOption == Some(0)
|
|
71
|
+
|
|
72
|
+
let stderrOf = (result: NodeChildProcess.spawnSyncResult) =>
|
|
73
|
+
result.stderr->Nullable.toOption->Option.getOr("")->String.trim
|
|
74
|
+
|
|
75
|
+
let run = (args: args): result<unit, array<string>> => {
|
|
76
|
+
let cancel = pulumi(args, ["cancel", "--yes"])
|
|
77
|
+
switch stderrOf(cancel) {
|
|
78
|
+
| message
|
|
79
|
+
if !succeeded(cancel) &&
|
|
80
|
+
message != "" &&
|
|
81
|
+
!(message->String.toLowerCase->String.includes("no update in progress")) =>
|
|
82
|
+
Console.error(`::warning::pulumi cancel: ${message}`)
|
|
83
|
+
| _ => ()
|
|
84
|
+
}
|
|
85
|
+
let exported = pulumi(args, ["stack", "export"])
|
|
86
|
+
let state = switch exported.stdout->Nullable.toOption {
|
|
87
|
+
| Some(stdout) if succeeded(exported) =>
|
|
88
|
+
try Ok(JSON.parseOrThrow(stdout)) catch {
|
|
89
|
+
| _ => Error([`the export of stack ${args.stack} is not JSON`])
|
|
90
|
+
}
|
|
91
|
+
| _ => Error([`pulumi stack export failed: ${stderrOf(exported)}`])
|
|
92
|
+
}
|
|
93
|
+
state->Result.flatMap(state => {
|
|
94
|
+
let pending = pendingOperations(state)
|
|
95
|
+
let refused = pending->Array.filter(p => !isSafe(p))
|
|
96
|
+
if pending == [] {
|
|
97
|
+
Console.log(`No pending operations on stack ${args.stack}.`)
|
|
98
|
+
} else {
|
|
99
|
+
Console.log(
|
|
100
|
+
`${pending->Array.length->Int.toString} pending operation(s) on stack ${args.stack}.`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if refused != [] {
|
|
104
|
+
Error(
|
|
105
|
+
refused->Array.map(p =>
|
|
106
|
+
`Pending ${p.operation} of ${p.urn} (${p.resourceType}): not cleared, because something may exist in AWS behind it. Resolve it by hand, or, if the type has nothing behind it, add it to ClearPendingCreates.safeTypes.`
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
} else {
|
|
110
|
+
// Every pending operation left is safe, so the refresh may drop them all.
|
|
111
|
+
// `pulumi state delete` cannot: a pending create has no resource in the state.
|
|
112
|
+
pending->Array.forEach(p =>
|
|
113
|
+
Console.log(`::warning::Clearing pending ${p.operation} of ${p.urn} (${p.resourceType})`)
|
|
114
|
+
)
|
|
115
|
+
let clear = pending == [] ? [] : ["--clear-pending-creates"]
|
|
116
|
+
// A failed refresh is usually a stale provider serialization, which the
|
|
117
|
+
// `pulumi up` after this heals.
|
|
118
|
+
let refreshed = pulumi(args, ["refresh", "--yes", "--skip-preview"]->Array.concat(clear))
|
|
119
|
+
if !succeeded(refreshed) {
|
|
120
|
+
Console.error(
|
|
121
|
+
`::warning::pulumi refresh had errors; pulumi up runs anyway. ${stderrOf(refreshed)}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
Ok()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let cli: Reventless.CliArgs.cli<args> = {bin: "clear-pending-creates", usage, parse: parseArgs}
|
|
130
|
+
|
|
131
|
+
let main = () =>
|
|
132
|
+
Reventless.CliArgs.run(cli, async args =>
|
|
133
|
+
switch run(args) {
|
|
134
|
+
| Ok() => ()
|
|
135
|
+
| Error(messages) =>
|
|
136
|
+
messages->Array.forEach(message => Console.error(`::error::${message}`))
|
|
137
|
+
NodeProcess.exit(1)
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// No top-level call: `../run-clear-pending-creates.mjs` invokes [main], so a test
|
|
142
|
+
// can import this module.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
|
+
import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
|
|
9
|
+
import * as Nodechild_process from "node:child_process";
|
|
10
|
+
|
|
11
|
+
let safeTypes = ["aws:acm/certificateValidation:CertificateValidation"];
|
|
12
|
+
|
|
13
|
+
function pendingOperations($$export) {
|
|
14
|
+
let field = (json, name) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[name]);
|
|
15
|
+
let text = (json, name) => Stdlib_Option.getOr(Stdlib_Option.flatMap(field(json, name), Stdlib_JSON.Decode.string), "");
|
|
16
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(field($$export, "deployment"), deployment => field(deployment, "pending_operations")), Stdlib_JSON.Decode.array), []).map(operation => {
|
|
17
|
+
let resource = Stdlib_Option.getOr(field(operation, "resource"), null);
|
|
18
|
+
return {
|
|
19
|
+
urn: text(resource, "urn"),
|
|
20
|
+
resourceType: text(resource, "type"),
|
|
21
|
+
operation: text(operation, "type")
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSafe(pending) {
|
|
27
|
+
return safeTypes.includes(pending.resourceType);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
return Stdlib_Result.flatMap(Stdlib_Result.flatMap(CliArgs$Reventless.parse([
|
|
32
|
+
"stack",
|
|
33
|
+
"cwd"
|
|
34
|
+
], undefined, undefined, undefined, argv), CliArgs$Reventless.noPositionals), a => {
|
|
35
|
+
let stack = CliArgs$Reventless.string(a, "stack");
|
|
36
|
+
if (stack !== undefined) {
|
|
37
|
+
return {
|
|
38
|
+
TAG: "Ok",
|
|
39
|
+
_0: {
|
|
40
|
+
stack: stack,
|
|
41
|
+
cwd: Stdlib_Option.getOr(CliArgs$Reventless.string(a, "cwd"), process.cwd())
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
return {
|
|
46
|
+
TAG: "Error",
|
|
47
|
+
_0: "--stack names the stack to recover"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let usage = `
|
|
54
|
+
Usage: clear-pending-creates --stack <name> [--cwd <dir>]
|
|
55
|
+
|
|
56
|
+
Recovers a stack before \`pulumi up\`: cancels a stale update, drops the pending
|
|
57
|
+
creates of types with nothing in AWS behind them, and refreshes. Any other pending
|
|
58
|
+
operation fails the command, for someone to resolve by hand.
|
|
59
|
+
|
|
60
|
+
--stack <name> The stack.
|
|
61
|
+
--cwd <dir> The Pulumi project. Defaults to the working directory.
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
function pulumi(args, command) {
|
|
65
|
+
return Nodechild_process.spawnSync("pulumi", command.concat([
|
|
66
|
+
"--stack",
|
|
67
|
+
args.stack,
|
|
68
|
+
"--cwd",
|
|
69
|
+
args.cwd,
|
|
70
|
+
"--non-interactive"
|
|
71
|
+
]), {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
maxBuffer: 268435456
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function succeeded(result) {
|
|
78
|
+
return Primitive_object.equal(Primitive_option.fromNullable(result.status), 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stderrOf(result) {
|
|
82
|
+
return Stdlib_Option.getOr(Primitive_option.fromNullable(result.stderr), "").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function run(args) {
|
|
86
|
+
let cancel = pulumi(args, [
|
|
87
|
+
"cancel",
|
|
88
|
+
"--yes"
|
|
89
|
+
]);
|
|
90
|
+
let message = stderrOf(cancel);
|
|
91
|
+
if (!succeeded(cancel) && message !== "" && !message.toLowerCase().includes("no update in progress")) {
|
|
92
|
+
console.error(`::warning::pulumi cancel: ` + message);
|
|
93
|
+
}
|
|
94
|
+
let exported = pulumi(args, [
|
|
95
|
+
"stack",
|
|
96
|
+
"export"
|
|
97
|
+
]);
|
|
98
|
+
let stdout = exported.stdout;
|
|
99
|
+
let state;
|
|
100
|
+
let exit = 0;
|
|
101
|
+
if ((stdout == null) || !succeeded(exported)) {
|
|
102
|
+
exit = 1;
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
state = {
|
|
106
|
+
TAG: "Ok",
|
|
107
|
+
_0: JSON.parse(stdout)
|
|
108
|
+
};
|
|
109
|
+
} catch (exn) {
|
|
110
|
+
state = {
|
|
111
|
+
TAG: "Error",
|
|
112
|
+
_0: [`the export of stack ` + args.stack + ` is not JSON`]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (exit === 1) {
|
|
117
|
+
state = {
|
|
118
|
+
TAG: "Error",
|
|
119
|
+
_0: [`pulumi stack export failed: ` + stderrOf(exported)]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return Stdlib_Result.flatMap(state, state => {
|
|
123
|
+
let pending = pendingOperations(state);
|
|
124
|
+
let refused = pending.filter(p => !safeTypes.includes(p.resourceType));
|
|
125
|
+
if (Primitive_object.equal(pending, [])) {
|
|
126
|
+
console.log(`No pending operations on stack ` + args.stack + `.`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(pending.length.toString() + ` pending operation(s) on stack ` + args.stack + `.`);
|
|
129
|
+
}
|
|
130
|
+
if (Primitive_object.notequal(refused, [])) {
|
|
131
|
+
return {
|
|
132
|
+
TAG: "Error",
|
|
133
|
+
_0: refused.map(p => `Pending ` + p.operation + ` of ` + p.urn + ` (` + p.resourceType + `): not cleared, because something may exist in AWS behind it. Resolve it by hand, or, if the type has nothing behind it, add it to ClearPendingCreates.safeTypes.`)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
pending.forEach(p => {
|
|
137
|
+
console.log(`::warning::Clearing pending ` + p.operation + ` of ` + p.urn + ` (` + p.resourceType + `)`);
|
|
138
|
+
});
|
|
139
|
+
let clear = Primitive_object.equal(pending, []) ? [] : ["--clear-pending-creates"];
|
|
140
|
+
let refreshed = pulumi(args, [
|
|
141
|
+
"refresh",
|
|
142
|
+
"--yes",
|
|
143
|
+
"--skip-preview"
|
|
144
|
+
].concat(clear));
|
|
145
|
+
if (!succeeded(refreshed)) {
|
|
146
|
+
console.error(`::warning::pulumi refresh had errors; pulumi up runs anyway. ` + stderrOf(refreshed));
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
TAG: "Ok",
|
|
150
|
+
_0: undefined
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let cli = {
|
|
156
|
+
bin: "clear-pending-creates",
|
|
157
|
+
usage: usage,
|
|
158
|
+
parse: parseArgs
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function main() {
|
|
162
|
+
return CliArgs$Reventless.run(cli, undefined, undefined, async args => {
|
|
163
|
+
let messages = run(args);
|
|
164
|
+
if (messages.TAG === "Ok") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
messages._0.forEach(message => {
|
|
168
|
+
console.error(`::error::` + message);
|
|
169
|
+
});
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
safeTypes,
|
|
176
|
+
pendingOperations,
|
|
177
|
+
isSafe,
|
|
178
|
+
parseArgs,
|
|
179
|
+
usage,
|
|
180
|
+
pulumi,
|
|
181
|
+
succeeded,
|
|
182
|
+
stderrOf,
|
|
183
|
+
run,
|
|
184
|
+
cli,
|
|
185
|
+
main,
|
|
186
|
+
}
|
|
187
|
+
/* CliArgs-Reventless Not a pure module */
|
package/scripts/DeployApp.res
CHANGED
|
@@ -86,6 +86,7 @@ type project = {
|
|
|
86
86
|
projectName: string,
|
|
87
87
|
program: string,
|
|
88
88
|
stackDefaults: dict<string>,
|
|
89
|
+
region: option<string>,
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
@@ -111,6 +112,7 @@ let projectOf = (p: DeployManifest.project): result<project, string> => {
|
|
|
111
112
|
projectName,
|
|
112
113
|
program: NodePath.join([p.dir, field("main")->Option.getOr("index.js")]),
|
|
113
114
|
stackDefaults: p.stackDefaults,
|
|
115
|
+
region: p.region,
|
|
114
116
|
})
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -150,6 +152,19 @@ let backendNeedsPassphrase = (~url: option<string>, ~env: dict<string>): bool =>
|
|
|
150
152
|
| _ => false
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/** A manifest that puts a stack in another region than the one `up` deploys
|
|
156
|
+
every stack to. */
|
|
157
|
+
let regionProblems = (~projects: array<project>, ~region: string): array<string> =>
|
|
158
|
+
projects->Array.filterMap(p =>
|
|
159
|
+
switch p.region {
|
|
160
|
+
| Some(own) if own != region =>
|
|
161
|
+
Some(
|
|
162
|
+
`${p.label} deploys to ${own} in the manifest, and deploy-app deploys every stack to one region, ${region}. Deploy it with the deploy workflow, or from a manifest without that region.`,
|
|
163
|
+
)
|
|
164
|
+
| _ => None
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
153
168
|
let unbuilt = (projects: array<project>): array<project> =>
|
|
154
169
|
projects->Array.filter(p => !NodeFs.existsSync(p.program))
|
|
155
170
|
|
|
@@ -201,7 +216,7 @@ let check = async (~projects: array<project>): result<ready, array<string>> => {
|
|
|
201
216
|
}
|
|
202
217
|
| exception _ =>
|
|
203
218
|
problems->Array.push(
|
|
204
|
-
"Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud) or `pulumi login --local`, and try again.",
|
|
219
|
+
"Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud), `pulumi login s3://<bucket>` (a bucket of your own) or `pulumi login --local`, and try again.",
|
|
205
220
|
)
|
|
206
221
|
}
|
|
207
222
|
| exception _ =>
|
|
@@ -210,6 +225,9 @@ let check = async (~projects: array<project>): result<ready, array<string>> => {
|
|
|
210
225
|
)
|
|
211
226
|
}
|
|
212
227
|
}
|
|
228
|
+
region->Option.forEach(region =>
|
|
229
|
+
regionProblems(~projects, ~region)->Array.forEach(p => problems->Array.push(p))
|
|
230
|
+
)
|
|
213
231
|
unbuilt(projects)->Array.forEach(p =>
|
|
214
232
|
problems->Array.push(
|
|
215
233
|
`${p.label} is not built: ${p.program} is missing. Run \`pnpm run build\` and try again.`,
|
|
@@ -552,7 +570,12 @@ let up = async (~manifest: DeployManifest.resolved, ~stack: string): result<unit
|
|
|
552
570
|
| Error(_) as e => e
|
|
553
571
|
| Ok() =>
|
|
554
572
|
Console.log("\n── Baking the component manifest ──\n")
|
|
555
|
-
switch await BakeManifest.bake(
|
|
573
|
+
switch await BakeManifest.bake(
|
|
574
|
+
~manifest,
|
|
575
|
+
~stack,
|
|
576
|
+
~since=Some(startedAt),
|
|
577
|
+
~review=false,
|
|
578
|
+
) {
|
|
556
579
|
| Error(_) as e => e
|
|
557
580
|
| Ok() =>
|
|
558
581
|
let accounts = switch (
|
|
@@ -134,7 +134,8 @@ function projectOf(p) {
|
|
|
134
134
|
dir: p.dir,
|
|
135
135
|
projectName: projectName,
|
|
136
136
|
program: Nodepath.join(p.dir, Stdlib_Option.getOr(field("main"), "index.js")),
|
|
137
|
-
stackDefaults: p.stackDefaults
|
|
137
|
+
stackDefaults: p.stackDefaults,
|
|
138
|
+
region: p.region
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
} else {
|
|
@@ -183,6 +184,15 @@ function backendNeedsPassphrase(url, env) {
|
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
function regionProblems(projects, region) {
|
|
188
|
+
return Stdlib_Array.filterMap(projects, p => {
|
|
189
|
+
let own = p.region;
|
|
190
|
+
if (own !== undefined && own !== region) {
|
|
191
|
+
return p.label + ` deploys to ` + own + ` in the manifest, and deploy-app deploys every stack to one region, ` + region + `. Deploy it with the deploy workflow, or from a manifest without that region.`;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
function unbuilt(projects) {
|
|
187
197
|
return projects.filter(p => !Nodefs.existsSync(p.program));
|
|
188
198
|
}
|
|
@@ -243,7 +253,7 @@ async function check(projects) {
|
|
|
243
253
|
who = await workspace.whoAmI();
|
|
244
254
|
exit$3 = 2;
|
|
245
255
|
} catch (exn$3) {
|
|
246
|
-
problems.push("Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud) or `pulumi login --local`, and try again.");
|
|
256
|
+
problems.push("Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud), `pulumi login s3://<bucket>` (a bucket of your own) or `pulumi login --local`, and try again.");
|
|
247
257
|
}
|
|
248
258
|
if (exit$3 === 2) {
|
|
249
259
|
if (backendNeedsPassphrase(who.url, process.env)) {
|
|
@@ -252,6 +262,11 @@ async function check(projects) {
|
|
|
252
262
|
}
|
|
253
263
|
}
|
|
254
264
|
}
|
|
265
|
+
Stdlib_Option.forEach(region, region => {
|
|
266
|
+
regionProblems(projects, region).forEach(p => {
|
|
267
|
+
problems.push(p);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
255
270
|
unbuilt(projects).forEach(p => {
|
|
256
271
|
problems.push(p.label + ` is not built: ` + p.program + ` is missing. Run \`pnpm run build\` and try again.`);
|
|
257
272
|
});
|
|
@@ -703,7 +718,7 @@ async function up(manifest, stack) {
|
|
|
703
718
|
return e$4;
|
|
704
719
|
}
|
|
705
720
|
console.log("\n── Baking the component manifest ──\n");
|
|
706
|
-
let e$5 = await BakeManifest$ReventlessAws.bake(manifest, stack, startedAt);
|
|
721
|
+
let e$5 = await BakeManifest$ReventlessAws.bake(manifest, stack, startedAt, false);
|
|
707
722
|
if (e$5.TAG !== "Ok") {
|
|
708
723
|
return e$5;
|
|
709
724
|
}
|
|
@@ -917,6 +932,7 @@ export {
|
|
|
917
932
|
requiredNodeMajor,
|
|
918
933
|
checkNode,
|
|
919
934
|
backendNeedsPassphrase,
|
|
935
|
+
regionProblems,
|
|
920
936
|
unbuilt,
|
|
921
937
|
check,
|
|
922
938
|
layerName,
|
|
@@ -7,6 +7,8 @@ it here. Paths in the file are relative to the file's own folder.
|
|
|
7
7
|
|
|
8
8
|
`stack-defaults` on an entry are the settings `deploy-app` gives a stack it
|
|
9
9
|
creates for that folder — what a new stack of this app cannot deploy without.
|
|
10
|
+
A plugin's `region` overrides the file's for that stack; `review: false` keeps it
|
|
11
|
+
out of a pull request's review environment.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
@@ -24,6 +26,8 @@ type plugin = {
|
|
|
24
26
|
path: string,
|
|
25
27
|
@as("depends-on") dependsOn?: array<string>,
|
|
26
28
|
@as("stack-defaults") stackDefaults?: dict<string>,
|
|
29
|
+
region?: string,
|
|
30
|
+
review?: bool,
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
@schema
|
|
@@ -34,7 +38,14 @@ type t = {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** One stack folder, with its path made absolute. */
|
|
37
|
-
type project = {
|
|
41
|
+
type project = {
|
|
42
|
+
name: string,
|
|
43
|
+
dir: string,
|
|
44
|
+
stackDefaults: dict<string>,
|
|
45
|
+
region: option<string>,
|
|
46
|
+
/** Whether a review environment deploys it: false only on a plugin marked so. */
|
|
47
|
+
review: bool,
|
|
48
|
+
}
|
|
38
49
|
|
|
39
50
|
type resolved = {
|
|
40
51
|
file: string,
|
|
@@ -60,6 +71,8 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
60
71
|
name: manifest.platform.name->Option.getOr("platform"),
|
|
61
72
|
dir: NodePath.resolve([base, manifest.platform.path]),
|
|
62
73
|
stackDefaults: manifest.platform.stackDefaults->Option.getOr(Dict.make()),
|
|
74
|
+
region: None,
|
|
75
|
+
review: true,
|
|
63
76
|
},
|
|
64
77
|
plugins: manifest.plugins
|
|
65
78
|
->Option.getOr([])
|
|
@@ -67,6 +80,8 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
67
80
|
name: p.name,
|
|
68
81
|
dir: NodePath.resolve([base, p.path]),
|
|
69
82
|
stackDefaults: p.stackDefaults->Option.getOr(Dict.make()),
|
|
83
|
+
region: p.region,
|
|
84
|
+
review: p.review->Option.getOr(true),
|
|
70
85
|
}),
|
|
71
86
|
}
|
|
72
87
|
}
|
|
@@ -18,7 +18,9 @@ let pluginSchema = Sury.$schema(s => ({
|
|
|
18
18
|
name: s.m(Sury.string),
|
|
19
19
|
path: s.m(Sury.string),
|
|
20
20
|
"depends-on": s.m(Sury.$option(Sury.array(Sury.string))),
|
|
21
|
-
"stack-defaults": s.m(Sury.$option(Sury.record(Sury.string)))
|
|
21
|
+
"stack-defaults": s.m(Sury.$option(Sury.record(Sury.string))),
|
|
22
|
+
region: s.m(Sury.$option(Sury.string)),
|
|
23
|
+
review: s.m(Sury.$option(Sury.boolean))
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
let schema = Sury.$schema(s => ({
|
|
@@ -57,12 +59,16 @@ function resolve(manifest, file) {
|
|
|
57
59
|
platform: {
|
|
58
60
|
name: Stdlib_Option.getOr(manifest.platform.name, "platform"),
|
|
59
61
|
dir: Nodepath.resolve(base, manifest.platform.path),
|
|
60
|
-
stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {})
|
|
62
|
+
stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {}),
|
|
63
|
+
region: undefined,
|
|
64
|
+
review: true
|
|
61
65
|
},
|
|
62
66
|
plugins: Stdlib_Option.getOr(manifest.plugins, []).map(p => ({
|
|
63
67
|
name: p.name,
|
|
64
68
|
dir: Nodepath.resolve(base, p.path),
|
|
65
|
-
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {})
|
|
69
|
+
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {}),
|
|
70
|
+
region: p.region,
|
|
71
|
+
review: Stdlib_Option.getOr(p.review, true)
|
|
66
72
|
}))
|
|
67
73
|
};
|
|
68
74
|
}
|
|
@@ -21,6 +21,8 @@ plugins:
|
|
|
21
21
|
depends-on: []
|
|
22
22
|
- name: ordering
|
|
23
23
|
path: ordering-aws
|
|
24
|
+
region: eu-central-1
|
|
25
|
+
review: false
|
|
24
26
|
`
|
|
25
27
|
|
|
26
28
|
testSync("resolves every folder against the manifest's own folder", () => {
|
|
@@ -36,10 +38,24 @@ plugins:
|
|
|
36
38
|
name: "shop-platform",
|
|
37
39
|
dir: "/repo/shop/platform-aws",
|
|
38
40
|
stackDefaults: Dict.fromArray([("platform:messagingEmailProvider", "log")]),
|
|
41
|
+
region: None,
|
|
42
|
+
review: true,
|
|
39
43
|
},
|
|
40
44
|
plugins: [
|
|
41
|
-
{
|
|
42
|
-
|
|
45
|
+
{
|
|
46
|
+
name: "catalog",
|
|
47
|
+
dir: "/repo/shop/catalog-aws",
|
|
48
|
+
stackDefaults: Dict.make(),
|
|
49
|
+
region: None,
|
|
50
|
+
review: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "ordering",
|
|
54
|
+
dir: "/repo/shop/ordering-aws",
|
|
55
|
+
stackDefaults: Dict.make(),
|
|
56
|
+
region: Some("eu-central-1"),
|
|
57
|
+
review: false,
|
|
58
|
+
},
|
|
43
59
|
],
|
|
44
60
|
}),
|
|
45
61
|
)
|
|
@@ -66,6 +82,7 @@ describe("BakeManifest.parseArgs", () => {
|
|
|
66
82
|
Bake.manifest: Some("m.yaml"),
|
|
67
83
|
stack: Some("alpha"),
|
|
68
84
|
since: Some("2026-09-16T10:00:00Z"),
|
|
85
|
+
review: false,
|
|
69
86
|
help: false,
|
|
70
87
|
}),
|
|
71
88
|
)
|
|
@@ -76,11 +93,44 @@ describe("BakeManifest.parseArgs", () => {
|
|
|
76
93
|
expect(Bake.parseArgs(["--since", ""])->Result.map(a => a.since))->toEqual(Ok(None))
|
|
77
94
|
)
|
|
78
95
|
|
|
96
|
+
testSync("--review says the stack is a review environment", () =>
|
|
97
|
+
expect(
|
|
98
|
+
Bake.parseArgs(["--stack", "review-pr-7", "--review"])->Result.map(a => a.review),
|
|
99
|
+
)->toEqual(Ok(true))
|
|
100
|
+
)
|
|
101
|
+
|
|
79
102
|
testSync("an unknown flag refuses", () =>
|
|
80
103
|
expect(Bake.parseArgs(["--stak", "alpha"]))->toEqual(Error(`unknown argument "--stak"`))
|
|
81
104
|
)
|
|
82
105
|
})
|
|
83
106
|
|
|
107
|
+
describe("BakeManifest.pluginsToRead", () => {
|
|
108
|
+
let plugin = (name, ~review) => {
|
|
109
|
+
DeployManifest.name,
|
|
110
|
+
dir: `/repo/shop/${name}-aws`,
|
|
111
|
+
stackDefaults: Dict.make(),
|
|
112
|
+
region: None,
|
|
113
|
+
review,
|
|
114
|
+
}
|
|
115
|
+
let plugins = [plugin("catalog", ~review=true), plugin("ingest", ~review=false)]
|
|
116
|
+
let names = ps => ps->Array.map((p: DeployManifest.project) => p.name)
|
|
117
|
+
|
|
118
|
+
testSync("outside a review, the plugins whose stack has a settings file here", () => {
|
|
119
|
+
let hasStackFile = (~dir, ~stack as _) => dir == "/repo/shop/ingest-aws"
|
|
120
|
+
expect(
|
|
121
|
+
plugins->Bake.pluginsToRead(~stack="alpha", ~review=false, ~hasStackFile)->names,
|
|
122
|
+
)->toEqual(["ingest"])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// A review stack's settings exist only in the runner that created it.
|
|
126
|
+
testSync("in a review, every plugin with a review stack, files or not", () => {
|
|
127
|
+
let hasStackFile = (~dir as _, ~stack as _) => false
|
|
128
|
+
expect(
|
|
129
|
+
plugins->Bake.pluginsToRead(~stack="review-pr-7", ~review=true, ~hasStackFile)->names,
|
|
130
|
+
)->toEqual(["catalog"])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
84
134
|
describe("BakeManifest reads the stacks", () => {
|
|
85
135
|
testSync("a platform without a bake function declares no bake", () =>
|
|
86
136
|
expect(
|
|
@@ -25,6 +25,8 @@ plugins:
|
|
|
25
25
|
depends-on: []
|
|
26
26
|
- name: ordering
|
|
27
27
|
path: ordering-aws
|
|
28
|
+
region: eu-central-1
|
|
29
|
+
review: false
|
|
28
30
|
`), m => DeployManifest$ReventlessAws.resolve(m, "/repo/shop/deploy-manifest.yaml"));
|
|
29
31
|
globalThis.expect(resolved).toEqual({
|
|
30
32
|
TAG: "Ok",
|
|
@@ -37,18 +39,24 @@ plugins:
|
|
|
37
39
|
stackDefaults: Object.fromEntries([[
|
|
38
40
|
"platform:messagingEmailProvider",
|
|
39
41
|
"log"
|
|
40
|
-
]])
|
|
42
|
+
]]),
|
|
43
|
+
region: undefined,
|
|
44
|
+
review: true
|
|
41
45
|
},
|
|
42
46
|
plugins: [
|
|
43
47
|
{
|
|
44
48
|
name: "catalog",
|
|
45
49
|
dir: "/repo/shop/catalog-aws",
|
|
46
|
-
stackDefaults: {}
|
|
50
|
+
stackDefaults: {},
|
|
51
|
+
region: undefined,
|
|
52
|
+
review: true
|
|
47
53
|
},
|
|
48
54
|
{
|
|
49
55
|
name: "ordering",
|
|
50
56
|
dir: "/repo/shop/ordering-aws",
|
|
51
|
-
stackDefaults: {}
|
|
57
|
+
stackDefaults: {},
|
|
58
|
+
region: "eu-central-1",
|
|
59
|
+
review: false
|
|
52
60
|
}
|
|
53
61
|
]
|
|
54
62
|
}
|
|
@@ -74,6 +82,7 @@ globalThis.describe("BakeManifest.parseArgs", () => {
|
|
|
74
82
|
manifest: "m.yaml",
|
|
75
83
|
stack: "alpha",
|
|
76
84
|
since: "2026-09-16T10:00:00Z",
|
|
85
|
+
review: false,
|
|
77
86
|
help: false
|
|
78
87
|
}
|
|
79
88
|
});
|
|
@@ -87,6 +96,16 @@ globalThis.describe("BakeManifest.parseArgs", () => {
|
|
|
87
96
|
_0: undefined
|
|
88
97
|
});
|
|
89
98
|
});
|
|
99
|
+
globalThis.test("--review says the stack is a review environment", () => {
|
|
100
|
+
globalThis.expect(Stdlib_Result.map(BakeManifest$ReventlessAws.parseArgs([
|
|
101
|
+
"--stack",
|
|
102
|
+
"review-pr-7",
|
|
103
|
+
"--review"
|
|
104
|
+
]), a => a.review)).toEqual({
|
|
105
|
+
TAG: "Ok",
|
|
106
|
+
_0: true
|
|
107
|
+
});
|
|
108
|
+
});
|
|
90
109
|
globalThis.test("an unknown flag refuses", () => {
|
|
91
110
|
globalThis.expect(BakeManifest$ReventlessAws.parseArgs([
|
|
92
111
|
"--stak",
|
|
@@ -98,6 +117,30 @@ globalThis.describe("BakeManifest.parseArgs", () => {
|
|
|
98
117
|
});
|
|
99
118
|
});
|
|
100
119
|
|
|
120
|
+
globalThis.describe("BakeManifest.pluginsToRead", () => {
|
|
121
|
+
let plugin = (name, review) => ({
|
|
122
|
+
name: name,
|
|
123
|
+
dir: `/repo/shop/` + name + `-aws`,
|
|
124
|
+
stackDefaults: {},
|
|
125
|
+
region: undefined,
|
|
126
|
+
review: review
|
|
127
|
+
});
|
|
128
|
+
let plugins = [
|
|
129
|
+
plugin("catalog", true),
|
|
130
|
+
plugin("ingest", false)
|
|
131
|
+
];
|
|
132
|
+
globalThis.test("outside a review, the plugins whose stack has a settings file here", () => {
|
|
133
|
+
let hasStackFile = (dir, param) => dir === "/repo/shop/ingest-aws";
|
|
134
|
+
let ps = BakeManifest$ReventlessAws.pluginsToRead(plugins, "alpha", false, hasStackFile);
|
|
135
|
+
globalThis.expect(ps.map(p => p.name)).toEqual(["ingest"]);
|
|
136
|
+
});
|
|
137
|
+
globalThis.test("in a review, every plugin with a review stack, files or not", () => {
|
|
138
|
+
let hasStackFile = (param, param$1) => false;
|
|
139
|
+
let ps = BakeManifest$ReventlessAws.pluginsToRead(plugins, "review-pr-7", true, hasStackFile);
|
|
140
|
+
globalThis.expect(ps.map(p => p.name)).toEqual(["catalog"]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
101
144
|
globalThis.describe("BakeManifest reads the stacks", () => {
|
|
102
145
|
globalThis.test("a platform without a bake function declares no bake", () => {
|
|
103
146
|
globalThis.expect(BakeManifest$ReventlessAws.targetOf(Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(`{"bakedManifestBucket": "b", "bakedManifestFunction": ""}`)), {}))).toEqual(undefined);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Which pending operations the deploy workflow's recovery step drops and which
|
|
4
|
+
// stop it. The pulumi calls around them are checked against a real backend by
|
|
5
|
+
// hand; what is tested here is the decision.
|
|
6
|
+
|
|
7
|
+
module Clear = ClearPendingCreates
|
|
8
|
+
|
|
9
|
+
let export = (operations: string) =>
|
|
10
|
+
JSON.parseOrThrow(
|
|
11
|
+
`{"version": 3, "deployment": {"resources": [], "pending_operations": ${operations}}}`,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
let operation = (~urn, ~type_) =>
|
|
15
|
+
`{"resource": {"urn": "${urn}", "type": "${type_}"}, "type": "creating"}`
|
|
16
|
+
|
|
17
|
+
let validation = operation(
|
|
18
|
+
~urn="urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert",
|
|
19
|
+
~type_="aws:acm/certificateValidation:CertificateValidation",
|
|
20
|
+
)
|
|
21
|
+
let lambda = operation(
|
|
22
|
+
~urn="urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
23
|
+
~type_="aws:lambda/function:Function",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
let refused = (state: JSON.t) =>
|
|
27
|
+
state
|
|
28
|
+
->Clear.pendingOperations
|
|
29
|
+
->Array.filter(p => !Clear.isSafe(p))
|
|
30
|
+
->Array.map(p => p.resourceType)
|
|
31
|
+
|
|
32
|
+
describe("ClearPendingCreates.pendingOperations", () => {
|
|
33
|
+
testSync("a stack with none, or with no deployment yet, has none", () => {
|
|
34
|
+
expect(Clear.pendingOperations(export("[]")))->toEqual([])
|
|
35
|
+
expect(
|
|
36
|
+
Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3, "deployment": {}}`)),
|
|
37
|
+
)->toEqual([])
|
|
38
|
+
expect(Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3}`)))->toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
testSync("each operation with its resource and kind", () =>
|
|
42
|
+
expect(Clear.pendingOperations(export(`[${lambda}]`)))->toEqual([
|
|
43
|
+
{
|
|
44
|
+
Clear.urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
45
|
+
resourceType: "aws:lambda/function:Function",
|
|
46
|
+
operation: "creating",
|
|
47
|
+
},
|
|
48
|
+
])
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("ClearPendingCreates.isSafe", () => {
|
|
53
|
+
testSync("a certificate validation is dropped", () =>
|
|
54
|
+
expect(refused(export(`[${validation}]`)))->toEqual([])
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
testSync("a resource that may exist in AWS stops the deploy", () =>
|
|
58
|
+
expect(refused(export(`[${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
testSync("in a mix, only the unsafe one is refused", () =>
|
|
62
|
+
expect(refused(export(`[${validation}, ${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
testSync("the type must match whole, not as a prefix", () =>
|
|
66
|
+
expect(
|
|
67
|
+
refused(
|
|
68
|
+
export(
|
|
69
|
+
`[${operation(~urn="u", ~type_="aws:acm/certificateValidation:CertificateValidationX")}]`,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
)->toEqual(["aws:acm/certificateValidation:CertificateValidationX"])
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("ClearPendingCreates.parseArgs", () => {
|
|
77
|
+
testSync("the stack, and the project directory", () =>
|
|
78
|
+
expect(Clear.parseArgs(["--stack", "alpha", "--cwd", "platform-aws"]))->toEqual(
|
|
79
|
+
Ok({Clear.stack: "alpha", cwd: "platform-aws"}),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
testSync("the project directory defaults to the working directory", () =>
|
|
84
|
+
expect(Clear.parseArgs(["--stack", "alpha"])->Result.map(a => a.cwd))->toEqual(
|
|
85
|
+
Ok(NodeProcess.cwd()),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
test("no stack is a usage mistake", async () => {
|
|
90
|
+
let observed = await Reventless.CliArgs.observe(Clear.cli, [])
|
|
91
|
+
expect((observed.exitCode, observed.reachedMain))->toEqual((Some(2), false))
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
4
|
+
import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
|
|
5
|
+
import * as ClearPendingCreates$ReventlessAws from "../scripts/ClearPendingCreates.res.mjs";
|
|
6
|
+
|
|
7
|
+
function $$export(operations) {
|
|
8
|
+
return JSON.parse(`{"version": 3, "deployment": {"resources": [], "pending_operations": ` + operations + `}}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function operation(urn, type_) {
|
|
12
|
+
return `{"resource": {"urn": "` + urn + `", "type": "` + type_ + `"}, "type": "creating"}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let validation = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidation" + `"}, "type": "creating"}`;
|
|
16
|
+
|
|
17
|
+
let lambda = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:lambda/function:Function::handler" + `", "type": "` + "aws:lambda/function:Function" + `"}, "type": "creating"}`;
|
|
18
|
+
|
|
19
|
+
function refused(state) {
|
|
20
|
+
return ClearPendingCreates$ReventlessAws.pendingOperations(state).filter(p => !ClearPendingCreates$ReventlessAws.isSafe(p)).map(p => p.resourceType);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
globalThis.describe("ClearPendingCreates.pendingOperations", () => {
|
|
24
|
+
globalThis.test("a stack with none, or with no deployment yet, has none", () => {
|
|
25
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export("[]"))).toEqual([]);
|
|
26
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3, "deployment": {}}`))).toEqual([]);
|
|
27
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3}`))).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
globalThis.test("each operation with its resource and kind", () => {
|
|
30
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export(`[` + lambda + `]`))).toEqual([{
|
|
31
|
+
urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
32
|
+
resourceType: "aws:lambda/function:Function",
|
|
33
|
+
operation: "creating"
|
|
34
|
+
}]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
globalThis.describe("ClearPendingCreates.isSafe", () => {
|
|
39
|
+
globalThis.test("a certificate validation is dropped", () => {
|
|
40
|
+
globalThis.expect(refused($$export(`[` + validation + `]`))).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
globalThis.test("a resource that may exist in AWS stops the deploy", () => {
|
|
43
|
+
globalThis.expect(refused($$export(`[` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("in a mix, only the unsafe one is refused", () => {
|
|
46
|
+
globalThis.expect(refused($$export(`[` + validation + `, ` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
|
|
47
|
+
});
|
|
48
|
+
globalThis.test("the type must match whole, not as a prefix", () => {
|
|
49
|
+
globalThis.expect(refused($$export(`[` + (`{"resource": {"urn": "` + "u" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidationX" + `"}, "type": "creating"}`) + `]`))).toEqual(["aws:acm/certificateValidation:CertificateValidationX"]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalThis.describe("ClearPendingCreates.parseArgs", () => {
|
|
54
|
+
globalThis.test("the stack, and the project directory", () => {
|
|
55
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.parseArgs([
|
|
56
|
+
"--stack",
|
|
57
|
+
"alpha",
|
|
58
|
+
"--cwd",
|
|
59
|
+
"platform-aws"
|
|
60
|
+
])).toEqual({
|
|
61
|
+
TAG: "Ok",
|
|
62
|
+
_0: {
|
|
63
|
+
stack: "alpha",
|
|
64
|
+
cwd: "platform-aws"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
globalThis.test("the project directory defaults to the working directory", () => {
|
|
69
|
+
globalThis.expect(Stdlib_Result.map(ClearPendingCreates$ReventlessAws.parseArgs([
|
|
70
|
+
"--stack",
|
|
71
|
+
"alpha"
|
|
72
|
+
]), a => a.cwd)).toEqual({
|
|
73
|
+
TAG: "Ok",
|
|
74
|
+
_0: process.cwd()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
globalThis.test("no stack is a usage mistake", async () => {
|
|
78
|
+
let observed = await CliArgs$Reventless.observe(ClearPendingCreates$ReventlessAws.cli, []);
|
|
79
|
+
globalThis.expect([
|
|
80
|
+
observed.exitCode,
|
|
81
|
+
observed.reachedMain
|
|
82
|
+
]).toEqual([
|
|
83
|
+
2,
|
|
84
|
+
false
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let Clear;
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
Clear,
|
|
93
|
+
$$export,
|
|
94
|
+
operation,
|
|
95
|
+
validation,
|
|
96
|
+
lambda,
|
|
97
|
+
refused,
|
|
98
|
+
}
|
|
99
|
+
/* Not a pure module */
|
package/tests/DeployAppTest.res
CHANGED
|
@@ -55,8 +55,19 @@ describe("DeployApp stacks and layer", () => {
|
|
|
55
55
|
projectName: "shop-platform-aws",
|
|
56
56
|
program: "/app/platform-aws/src/Main.res.mjs",
|
|
57
57
|
stackDefaults: Dict.fromArray([("aws:region", "us-east-1"), ("platform:x", "y")]),
|
|
58
|
+
region: None,
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
testSync("a stack the manifest puts in another region is refused", () => {
|
|
62
|
+
let ingest = {...project, label: "ingest", region: Some("eu-central-1")}
|
|
63
|
+
expect(
|
|
64
|
+
DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-west-1")->Array.length,
|
|
65
|
+
)->toBe(1)
|
|
66
|
+
expect(
|
|
67
|
+
DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-central-1"),
|
|
68
|
+
)->toEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
60
71
|
// The region and the try-out flag come last, so an app default cannot turn a
|
|
61
72
|
// try-out into a stack `down` refuses to remove.
|
|
62
73
|
testSync("a new stack gets the app's defaults, then the region and the try-out flag", () =>
|
|
@@ -79,8 +79,29 @@ globalThis.describe("DeployApp stacks and layer", () => {
|
|
|
79
79
|
dir: "/app/platform-aws",
|
|
80
80
|
projectName: "shop-platform-aws",
|
|
81
81
|
program: "/app/platform-aws/src/Main.res.mjs",
|
|
82
|
-
stackDefaults: project_stackDefaults
|
|
82
|
+
stackDefaults: project_stackDefaults,
|
|
83
|
+
region: undefined
|
|
83
84
|
};
|
|
85
|
+
globalThis.test("a stack the manifest puts in another region is refused", () => {
|
|
86
|
+
let ingest_stackDefaults = project_stackDefaults;
|
|
87
|
+
let ingest_region = "eu-central-1";
|
|
88
|
+
let ingest = {
|
|
89
|
+
label: "ingest",
|
|
90
|
+
dir: "/app/platform-aws",
|
|
91
|
+
projectName: "shop-platform-aws",
|
|
92
|
+
program: "/app/platform-aws/src/Main.res.mjs",
|
|
93
|
+
stackDefaults: ingest_stackDefaults,
|
|
94
|
+
region: ingest_region
|
|
95
|
+
};
|
|
96
|
+
globalThis.expect(DeployApp$ReventlessAws.regionProblems([
|
|
97
|
+
project,
|
|
98
|
+
ingest
|
|
99
|
+
], "eu-west-1").length).toBe(1);
|
|
100
|
+
globalThis.expect(DeployApp$ReventlessAws.regionProblems([
|
|
101
|
+
project,
|
|
102
|
+
ingest
|
|
103
|
+
], "eu-central-1")).toEqual([]);
|
|
104
|
+
});
|
|
84
105
|
globalThis.test("a new stack gets the app's defaults, then the region and the try-out flag", () => {
|
|
85
106
|
globalThis.expect(DeployApp$ReventlessAws.newStackConfig(project, "eu-west-1")).toEqual([
|
|
86
107
|
[
|