@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.13 → 3.0.0-alpha.14

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 CHANGED
@@ -3,6 +3,18 @@
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.14 (2026-09-16)
7
+
8
+ * fix(aws)!: stop a deploy that finds no Lambda layer ([62957f4](https://github.com/ReventlessDev/reventless-core/commit/62957f444ab2120f672b9ac0564f786d64090864))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * PulumiAws.Lambda.reventlessLayerArn is now a function that
13
+ returns the ARN and throws when there is none. Use Lambda.reventlessLayers()
14
+ for a function's layers argument.
15
+
16
+
17
+
6
18
  # 3.0.0-alpha.13 (2026-09-11)
7
19
 
8
20
  **Note:** Version bump only for package @reventlessdev/rescript-pulumi-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/rescript-pulumi-aws",
3
- "version": "3.0.0-alpha.13",
3
+ "version": "3.0.0-alpha.14",
4
4
  "description": "ReScript bindings for @pulumi/aws",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -22,8 +22,9 @@
22
22
  "@pulumi/aws": "~7.19.0",
23
23
  "@pulumi/aws-native": "^1.62.0",
24
24
  "sury": "11.0.0-rc.2",
25
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.18",
26
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.20"
25
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.19",
26
+ "@reventlessdev/rescript-node": "2.0.0-alpha.12",
27
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21"
27
28
  },
28
29
  "devDependencies": {
29
30
  "esbuild": "^0.25.12",
package/rescript.json CHANGED
@@ -21,7 +21,8 @@
21
21
  "dependencies": [
22
22
  "sury",
23
23
  "@reventlessdev/rescript-pulumi-pulumi",
24
- "@reventlessdev/rescript-aws-sdk"
24
+ "@reventlessdev/rescript-aws-sdk",
25
+ "@reventlessdev/rescript-node"
25
26
  ],
26
27
  "compiler-flags": []
27
28
  }
@@ -46,53 +46,136 @@ type eventHandlerNoResult<'event> = eventHandler<'event, unit>
46
46
  @send
47
47
  external getRemainingTimeInMillis: context => int = "getRemainingTimeInMillis"
48
48
 
49
- @val @scope(("process", "env")) external _layerArnEnv: option<string> = "REVENTLESS_LAYER_ARN"
50
- @val @scope(("process", "env")) external _pulumiNodejsStack: option<string> = "PULUMI_NODEJS_STACK"
51
- @module("child_process") external _execSync: (string, {..}) => string = "execSync"
52
-
53
- // SSM fallback for local `pulumi up`. CI exports `REVENTLESS_LAYER_ARN`
54
- // directly (deploy-reventless-aws.yml reads `/reventless/layer-arn/{stack}`
55
- // via the AWS CLI and injects it as an env var); local devs typically don't.
56
- // When the env var is unset, shell out to the same SSM parameter from the
57
- // dev's `aws` CLI so local deploys attach the freshest layer automatically.
58
- //
59
- // Failures are swallowed (no `aws` on PATH, missing IAM `ssm:GetParameter`,
60
- // parameter absent, etc.) — Lambdas then deploy without the layer, which is
61
- // the same outcome as before this fallback existed.
62
- let _resolveLayerArnFromSsm = (): option<string> =>
63
- switch _pulumiNodejsStack {
64
- | None | Some("") => None
65
- | Some(stack) =>
66
- try {
67
- let cmd = `aws ssm get-parameter --name /reventless/layer-arn/${stack} --query Parameter.Value --output text 2>/dev/null`
68
- let raw = _execSync(cmd, {"encoding": "utf8"})
69
- let trimmed = raw->String.trim
70
- if trimmed === "" || trimmed === "None" {
71
- None
72
- } else {
73
- Some(trimmed)
74
- }
75
- } catch {
76
- | _ => None
49
+ /** The outcome of looking for the Reventless layer. `region` is `None` when
50
+ neither the stack nor the environment names one, and the AWS CLI's default
51
+ region was asked. */
52
+ type layerLookup =
53
+ | Found(string)
54
+ | NotFound({parameter: string, region: option<string>})
55
+ | CouldNotLook({parameter: string, region: option<string>, reason: string})
56
+
57
+ let layerParameter = stack => `/reventless/layer-arn/${stack}`
58
+
59
+ let _nonEmpty = value =>
60
+ switch value->Option.map(String.trim) {
61
+ | Some("") | None => None
62
+ | trimmed => trimmed
63
+ }
64
+
65
+ let _lookupRegion = () =>
66
+ switch Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->_nonEmpty {
67
+ | Some(_) as region => region
68
+ | None =>
69
+ switch NodeProcess.env->Dict.get("AWS_REGION")->_nonEmpty {
70
+ | Some(_) as region => region
71
+ | None => NodeProcess.env->Dict.get("AWS_DEFAULT_REGION")->_nonEmpty
77
72
  }
78
73
  }
79
74
 
80
- /**
81
- * Reventless Lambda layer ARN for the current Pulumi stack.
82
- *
83
- * Resolution order:
84
- * 1. `REVENTLESS_LAYER_ARN` env var — fast path, used by CI
85
- * (deploy-reventless-aws.yml reads SSM and exports it).
86
- * 2. AWS CLI SSM lookup at `/reventless/layer-arn/${PULUMI_NODEJS_STACK}` —
87
- * local-deploy convenience so devs don't need to export the env var
88
- * manually.
89
- * 3. `None` when both fail — Lambdas deploy without the layer.
90
- */
91
- let reventlessLayerArn: option<string> = switch _layerArnEnv {
92
- | Some(arn) if arn->String.trim->String.length > 0 => Some(arn->String.trim)
93
- | _ => _resolveLayerArnFromSsm()
75
+ let _firstLine = text =>
76
+ text
77
+ ->String.split("\n")
78
+ ->Array.map(String.trim)
79
+ ->Array.find(line => line !== "")
80
+ ->Option.getOr("the AWS CLI exited without saying why")
81
+
82
+ /** Reads the result of `aws ssm get-parameter`: only `ParameterNotFound` means
83
+ there is no layer; a missing CLI or a refused call means nobody could tell. */
84
+ let classifySsmLookup = (
85
+ ~parameter,
86
+ ~region,
87
+ result: NodeChildProcess.spawnSyncResult,
88
+ ): layerLookup => {
89
+ let stderr = result.stderr->Nullable.toOption->Option.getOr("")
90
+ switch (result.error->Nullable.toOption, result.status->Nullable.toOption) {
91
+ | (Some(error), _) =>
92
+ let message = error->JsExn.message->Option.getOr("unknown error")
93
+ CouldNotLook({parameter, region, reason: `the AWS CLI could not be run (${message})`})
94
+ | (None, Some(0)) =>
95
+ switch result.stdout->Nullable.toOption->_nonEmpty {
96
+ | None | Some("None") => NotFound({parameter, region})
97
+ | Some(arn) => Found(arn)
98
+ }
99
+ | (None, _) if stderr->String.includes("ParameterNotFound") => NotFound({parameter, region})
100
+ | (None, _) => CouldNotLook({parameter, region, reason: _firstLine(stderr)})
101
+ }
94
102
  }
95
103
 
104
+ let _lookupInSsm = (): layerLookup => {
105
+ let parameter = layerParameter(Pulumi.Pulumi.getStackName())
106
+ let region = _lookupRegion()
107
+ let regionArgs = region->Option.mapOr([], region => ["--region", region])
108
+ NodeChildProcess.spawnSync(
109
+ "aws",
110
+ [
111
+ "ssm",
112
+ "get-parameter",
113
+ "--name",
114
+ parameter,
115
+ "--query",
116
+ "Parameter.Value",
117
+ "--output",
118
+ "text",
119
+ ]->Array.concat(regionArgs),
120
+ {encoding: "utf8"},
121
+ )->classifySsmLookup(~parameter, ~region, _)
122
+ }
123
+
124
+ let _lookup: ref<option<layerLookup>> = ref(None)
125
+
126
+ /** Looks once per deploy: `REVENTLESS_LAYER_ARN` when set (CI exports it),
127
+ otherwise the stack's SSM parameter through the AWS CLI, in the stack's
128
+ `aws:region` when it sets one. */
129
+ let lookupLayer = (): layerLookup =>
130
+ switch _lookup.contents {
131
+ | Some(lookup) => lookup
132
+ | None =>
133
+ let lookup = switch NodeProcess.env->Dict.get("REVENTLESS_LAYER_ARN")->_nonEmpty {
134
+ | Some(arn) => Found(arn)
135
+ | None => _lookupInSsm()
136
+ }
137
+ _lookup := Some(lookup)
138
+ lookup
139
+ }
140
+
141
+ let _regionName = region =>
142
+ region->Option.mapOr("the AWS CLI's default region", region => `region ${region}`)
143
+
144
+ let _publishHint = `Publish the layer and store its ARN in that parameter, or set REVENTLESS_LAYER_ARN: https://docs.reventless.dev/infrastructure/aws/get-started#the-lambda-layer`
145
+
146
+ /** Why a deploy cannot go on without the layer, or `None` when it was found. */
147
+ let missingLayerMessage = lookup =>
148
+ switch lookup {
149
+ | Found(_) => None
150
+ | NotFound({parameter, region}) =>
151
+ Some(
152
+ `No Reventless Lambda layer: the SSM parameter ${parameter} does not exist in ${_regionName(
153
+ region,
154
+ )}. Without the layer every function fails with "Cannot find package" on its first run. ${_publishHint}`,
155
+ )
156
+ | CouldNotLook({parameter, region, reason}) =>
157
+ Some(
158
+ `Could not look up the Reventless Lambda layer in the SSM parameter ${parameter} (${_regionName(
159
+ region,
160
+ )}): ${reason}. ${_publishHint}`,
161
+ )
162
+ }
163
+
164
+ /** The layer's ARN. Throws with `missingLayerMessage` when there is none, so a
165
+ function is never created without the framework code it imports. */
166
+ let reventlessLayerArn = (): string =>
167
+ switch lookupLayer() {
168
+ | Found(arn) => arn
169
+ | lookup =>
170
+ JsError.throwWithMessage(
171
+ missingLayerMessage(lookup)->Option.getOr("No Reventless Lambda layer"),
172
+ )
173
+ }
174
+
175
+ /** The `layers` argument of every framework function. */
176
+ let reventlessLayers = (): Pulumi.Input.t<array<Pulumi.Input.t<string>>> =>
177
+ [reventlessLayerArn()->Pulumi.Input.make]->Pulumi.Input.make
178
+
96
179
  @val
97
180
  external environment: option<string> = "process.env.Environment"
98
181
 
@@ -150,10 +233,7 @@ module CallbackFunction = {
150
233
  ~memorySize=1024->Pulumi.Input.make,
151
234
  ~timeout=180->Pulumi.Input.make,
152
235
  ~runtime=NodeJs22,
153
- ~layers=reventlessLayerArn
154
- ->Option.map(arn => [arn->Pulumi.Input.make])
155
- ->Option.getOr([])
156
- ->Pulumi.Input.make,
236
+ ~layers=reventlessLayers(),
157
237
  ~vpcConfig=?,
158
238
  ~tags=?,
159
239
  ~environment={
@@ -1,43 +1,161 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
3
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
- import * as Child_process from "child_process";
5
5
  import * as Pulumi from "@pulumi/pulumi";
6
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
+ import * as Nodechild_process from "node:child_process";
6
9
  import * as PolicyDocument$PulumiAws from "../IAM/PolicyDocument.res.mjs";
7
10
 
8
- function _resolveLayerArnFromSsm() {
9
- let stack = process.env.PULUMI_NODEJS_STACK;
10
- if (stack === undefined) {
11
- return;
11
+ function layerParameter(stack) {
12
+ return `/reventless/layer-arn/` + stack;
13
+ }
14
+
15
+ function _nonEmpty(value) {
16
+ let trimmed = Stdlib_Option.map(value, prim => prim.trim());
17
+ if (trimmed !== undefined && trimmed !== "") {
18
+ return trimmed;
19
+ }
20
+ }
21
+
22
+ function _lookupRegion() {
23
+ let region = _nonEmpty(new Pulumi.Config("aws").get("region"));
24
+ if (region !== undefined) {
25
+ return region;
12
26
  }
13
- if (stack === "") {
14
- return;
27
+ let region$1 = _nonEmpty(process.env["AWS_REGION"]);
28
+ if (region$1 !== undefined) {
29
+ return region$1;
30
+ } else {
31
+ return _nonEmpty(process.env["AWS_DEFAULT_REGION"]);
15
32
  }
16
- try {
17
- let cmd = `aws ssm get-parameter --name /reventless/layer-arn/` + stack + ` --query Parameter.Value --output text 2>/dev/null`;
18
- let raw = Child_process.execSync(cmd, {
19
- encoding: "utf8"
20
- });
21
- let trimmed = raw.trim();
22
- if (trimmed === "" || trimmed === "None") {
23
- return;
33
+ }
34
+
35
+ function _firstLine(text) {
36
+ return Stdlib_Option.getOr(text.split("\n").map(prim => prim.trim()).find(line => line !== ""), "the AWS CLI exited without saying why");
37
+ }
38
+
39
+ function classifySsmLookup(parameter, region, result) {
40
+ let stderr = Stdlib_Option.getOr(Primitive_option.fromNullable(result.stderr), "");
41
+ let match = result.error;
42
+ let match$1 = result.status;
43
+ if (match == null) {
44
+ if (!(match$1 == null) && match$1 === 0) {
45
+ let arn = _nonEmpty(Primitive_option.fromNullable(result.stdout));
46
+ if (arn !== undefined && arn !== "None") {
47
+ return {
48
+ TAG: "Found",
49
+ _0: arn
50
+ };
51
+ } else {
52
+ return {
53
+ TAG: "NotFound",
54
+ parameter: parameter,
55
+ region: region
56
+ };
57
+ }
58
+ }
59
+ if (stderr.includes("ParameterNotFound")) {
60
+ return {
61
+ TAG: "NotFound",
62
+ parameter: parameter,
63
+ region: region
64
+ };
24
65
  } else {
25
- return trimmed;
66
+ return {
67
+ TAG: "CouldNotLook",
68
+ parameter: parameter,
69
+ region: region,
70
+ reason: _firstLine(stderr)
71
+ };
26
72
  }
27
- } catch (exn) {
28
- return;
29
73
  }
74
+ let message = Stdlib_Option.getOr(Stdlib_JsExn.message(match), "unknown error");
75
+ return {
76
+ TAG: "CouldNotLook",
77
+ parameter: parameter,
78
+ region: region,
79
+ reason: `the AWS CLI could not be run (` + message + `)`
80
+ };
30
81
  }
31
82
 
32
- let arn = process.env.REVENTLESS_LAYER_ARN;
83
+ function _lookupInSsm() {
84
+ let parameter = layerParameter(Pulumi.getStack());
85
+ let region = _lookupRegion();
86
+ let regionArgs = Stdlib_Option.mapOr(region, [], region => [
87
+ "--region",
88
+ region
89
+ ]);
90
+ return classifySsmLookup(parameter, region, Nodechild_process.spawnSync("aws", [
91
+ "ssm",
92
+ "get-parameter",
93
+ "--name",
94
+ parameter,
95
+ "--query",
96
+ "Parameter.Value",
97
+ "--output",
98
+ "text"
99
+ ].concat(regionArgs), {
100
+ encoding: "utf8"
101
+ }));
102
+ }
33
103
 
34
- let reventlessLayerArn = arn !== undefined && arn.trim().length > 0 ? arn.trim() : _resolveLayerArnFromSsm();
104
+ let _lookup = {
105
+ contents: undefined
106
+ };
107
+
108
+ function lookupLayer() {
109
+ let lookup = _lookup.contents;
110
+ if (lookup !== undefined) {
111
+ return lookup;
112
+ }
113
+ let arn = _nonEmpty(process.env["REVENTLESS_LAYER_ARN"]);
114
+ let lookup$1 = arn !== undefined ? ({
115
+ TAG: "Found",
116
+ _0: arn
117
+ }) : _lookupInSsm();
118
+ _lookup.contents = lookup$1;
119
+ return lookup$1;
120
+ }
121
+
122
+ function _regionName(region) {
123
+ return Stdlib_Option.mapOr(region, "the AWS CLI's default region", region => `region ` + region);
124
+ }
125
+
126
+ let _publishHint = `Publish the layer and store its ARN in that parameter, or set REVENTLESS_LAYER_ARN: https://docs.reventless.dev/infrastructure/aws/get-started#the-lambda-layer`;
127
+
128
+ function missingLayerMessage(lookup) {
129
+ switch (lookup.TAG) {
130
+ case "Found" :
131
+ return;
132
+ case "NotFound" :
133
+ return `No Reventless Lambda layer: the SSM parameter ` + lookup.parameter + ` does not exist in ` + _regionName(lookup.region) + `. Without the layer every function fails with "Cannot find package" on its first run. ` + _publishHint;
134
+ case "CouldNotLook" :
135
+ return `Could not look up the Reventless Lambda layer in the SSM parameter ` + lookup.parameter + ` (` + _regionName(lookup.region) + `): ` + lookup.reason + `. ` + _publishHint;
136
+ }
137
+ }
138
+
139
+ function reventlessLayerArn() {
140
+ let arn = lookupLayer();
141
+ switch (arn.TAG) {
142
+ case "Found" :
143
+ return arn._0;
144
+ case "NotFound" :
145
+ case "CouldNotLook" :
146
+ return Stdlib_JsError.throwWithMessage(Stdlib_Option.getOr(missingLayerMessage(arn), "No Reventless Lambda layer"));
147
+ }
148
+ }
149
+
150
+ function reventlessLayers() {
151
+ return [reventlessLayerArn()];
152
+ }
35
153
 
36
154
  function make(callback, role, policies, deadLetterConfig, tracingConfig, memorySizeOpt, timeoutOpt, runtimeOpt, layersOpt, vpcConfig, tags, environmentOpt) {
37
155
  let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
38
156
  let timeout = timeoutOpt !== undefined ? timeoutOpt : 180;
39
157
  let runtime = runtimeOpt !== undefined ? runtimeOpt : "nodejs22.x";
40
- let layers = layersOpt !== undefined ? layersOpt : Stdlib_Option.getOr(Stdlib_Option.map(reventlessLayerArn, arn => [arn]), []);
158
+ let layers = layersOpt !== undefined ? layersOpt : [reventlessLayerArn()];
41
159
  let environment = environmentOpt !== undefined ? environmentOpt : ({
42
160
  variables: Object.fromEntries([[
43
161
  "Environment",
@@ -86,12 +204,23 @@ let defaultLoggingPolicyDocument = PolicyDocument$PulumiAws.make(undefined, unde
86
204
  }]);
87
205
 
88
206
  export {
89
- _resolveLayerArnFromSsm,
207
+ layerParameter,
208
+ _nonEmpty,
209
+ _lookupRegion,
210
+ _firstLine,
211
+ classifySsmLookup,
212
+ _lookupInSsm,
213
+ _lookup,
214
+ lookupLayer,
215
+ _regionName,
216
+ _publishHint,
217
+ missingLayerMessage,
90
218
  reventlessLayerArn,
219
+ reventlessLayers,
91
220
  CallbackFunction,
92
221
  $$Function,
93
222
  Permission,
94
223
  Invocation,
95
224
  defaultLoggingPolicyDocument,
96
225
  }
97
- /* arn Not a pure module */
226
+ /* defaultLoggingPolicyDocument Not a pure module */