@reventlessdev/reventless-aws 3.0.0-alpha.371 → 3.0.0-alpha.372
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 +11 -0
- package/package.json +8 -7
- package/run-clear-pending-creates.mjs +3 -0
- package/scripts/ClearPendingCreates.res +142 -0
- package/scripts/ClearPendingCreates.res.mjs +187 -0
- package/scripts/DeployApp.res +19 -1
- package/scripts/DeployApp.res.mjs +18 -2
- package/scripts/DeployManifest.res +7 -1
- package/scripts/DeployManifest.res.mjs +7 -3
- package/tests/BakeManifestTest.res +15 -2
- package/tests/BakeManifestTest.res.mjs +8 -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,17 @@
|
|
|
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.372 (2026-10-05)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **deploy:** the pending-creates recovery runs from reventless-aws, and works ([b9e749e](https://github.com/ReventlessDev/reventless-core/commit/b9e749eb09354a559505fa6facc7f58e38afd2a9))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **deploy:** a plugin names its own region, and can stay out of review environments ([7edd318](https://github.com/ReventlessDev/reventless-core/commit/7edd3187f51f74e57c331b1cb417b17de8067b58))
|
|
14
|
+
* **deploy:** the deploy workflow names its state backend ([a370346](https://github.com/ReventlessDev/reventless-core/commit/a3703461bae9dc22c098315a09bbabcf0c5816b1))
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# 3.0.0-alpha.371 (2026-10-01)
|
|
7
18
|
|
|
8
19
|
### 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.372",
|
|
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",
|
|
25
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
26
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
27
26
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
27
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
28
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
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
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.185",
|
|
34
35
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
|
|
35
|
-
"@reventlessdev/reventless-
|
|
36
|
-
"@reventlessdev/reventless-
|
|
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",
|
|
@@ -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.`,
|
|
@@ -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
|
});
|
|
@@ -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,7 @@ type t = {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** One stack folder, with its path made absolute. */
|
|
37
|
-
type project = {name: string, dir: string, stackDefaults: dict<string>}
|
|
41
|
+
type project = {name: string, dir: string, stackDefaults: dict<string>, region: option<string>}
|
|
38
42
|
|
|
39
43
|
type resolved = {
|
|
40
44
|
file: string,
|
|
@@ -60,6 +64,7 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
60
64
|
name: manifest.platform.name->Option.getOr("platform"),
|
|
61
65
|
dir: NodePath.resolve([base, manifest.platform.path]),
|
|
62
66
|
stackDefaults: manifest.platform.stackDefaults->Option.getOr(Dict.make()),
|
|
67
|
+
region: None,
|
|
63
68
|
},
|
|
64
69
|
plugins: manifest.plugins
|
|
65
70
|
->Option.getOr([])
|
|
@@ -67,6 +72,7 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
67
72
|
name: p.name,
|
|
68
73
|
dir: NodePath.resolve([base, p.path]),
|
|
69
74
|
stackDefaults: p.stackDefaults->Option.getOr(Dict.make()),
|
|
75
|
+
region: p.region,
|
|
70
76
|
}),
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -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,14 @@ 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
|
|
61
64
|
},
|
|
62
65
|
plugins: Stdlib_Option.getOr(manifest.plugins, []).map(p => ({
|
|
63
66
|
name: p.name,
|
|
64
67
|
dir: Nodepath.resolve(base, p.path),
|
|
65
|
-
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {})
|
|
68
|
+
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {}),
|
|
69
|
+
region: p.region
|
|
66
70
|
}))
|
|
67
71
|
};
|
|
68
72
|
}
|
|
@@ -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,21 @@ plugins:
|
|
|
36
38
|
name: "shop-platform",
|
|
37
39
|
dir: "/repo/shop/platform-aws",
|
|
38
40
|
stackDefaults: Dict.fromArray([("platform:messagingEmailProvider", "log")]),
|
|
41
|
+
region: None,
|
|
39
42
|
},
|
|
40
43
|
plugins: [
|
|
41
|
-
{
|
|
42
|
-
|
|
44
|
+
{
|
|
45
|
+
name: "catalog",
|
|
46
|
+
dir: "/repo/shop/catalog-aws",
|
|
47
|
+
stackDefaults: Dict.make(),
|
|
48
|
+
region: None,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "ordering",
|
|
52
|
+
dir: "/repo/shop/ordering-aws",
|
|
53
|
+
stackDefaults: Dict.make(),
|
|
54
|
+
region: Some("eu-central-1"),
|
|
55
|
+
},
|
|
43
56
|
],
|
|
44
57
|
}),
|
|
45
58
|
)
|
|
@@ -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,21 @@ plugins:
|
|
|
37
39
|
stackDefaults: Object.fromEntries([[
|
|
38
40
|
"platform:messagingEmailProvider",
|
|
39
41
|
"log"
|
|
40
|
-
]])
|
|
42
|
+
]]),
|
|
43
|
+
region: undefined
|
|
41
44
|
},
|
|
42
45
|
plugins: [
|
|
43
46
|
{
|
|
44
47
|
name: "catalog",
|
|
45
48
|
dir: "/repo/shop/catalog-aws",
|
|
46
|
-
stackDefaults: {}
|
|
49
|
+
stackDefaults: {},
|
|
50
|
+
region: undefined
|
|
47
51
|
},
|
|
48
52
|
{
|
|
49
53
|
name: "ordering",
|
|
50
54
|
dir: "/repo/shop/ordering-aws",
|
|
51
|
-
stackDefaults: {}
|
|
55
|
+
stackDefaults: {},
|
|
56
|
+
region: "eu-central-1"
|
|
52
57
|
}
|
|
53
58
|
]
|
|
54
59
|
}
|
|
@@ -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
|
[
|