@friggframework/devtools 2.0.0--canary.625.0de0db4.0 → 2.0.0--canary.625.6bef7de.0

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.
@@ -163,21 +163,59 @@ async function pushOffloadedParameters(appDefinition, stage, options = {}) {
163
163
  options
164
164
  );
165
165
 
166
- for (const key of skipped) {
167
- console.warn(
168
- `⚠️ Skipping ${key}: no value in the environment (--allow-empty)`
169
- );
170
- }
171
-
172
- if (specs.length === 0) {
166
+ if (specs.length === 0 && skipped.length === 0) {
173
167
  return { pushed: [], skipped };
174
168
  }
175
169
 
176
- const { SSMClient, PutParameterCommand } = require('@aws-sdk/client-ssm');
170
+ const {
171
+ SSMClient,
172
+ PutParameterCommand,
173
+ GetParametersCommand,
174
+ } = require('@aws-sdk/client-ssm');
177
175
  const client = new SSMClient({
178
176
  region: resolvePushRegion(options),
179
177
  });
180
178
 
179
+ // --allow-empty only rotates keys that already exist. A skipped key still
180
+ // ships in FRIGG_SSM_OFFLOADED_KEYS, so if its parameter does not exist the
181
+ // runtime fails fast at cold start on every function — a green deploy that
182
+ // bricks the fleet. Verify existence and abort instead.
183
+ if (skipped.length > 0) {
184
+ const prefix = resolveParameterPrefix(appDefinition, stage);
185
+ const names = skipped.map((key) => `${prefix}/${key}`);
186
+ const existing = new Set();
187
+ for (let i = 0; i < names.length; i += 10) {
188
+ const { Parameters = [] } = await client.send(
189
+ new GetParametersCommand({ Names: names.slice(i, i + 10) })
190
+ );
191
+ for (const param of Parameters) {
192
+ existing.add(param.Name);
193
+ }
194
+ }
195
+ const orphaned = skipped.filter(
196
+ (key) => !existing.has(`${prefix}/${key}`)
197
+ );
198
+ if (orphaned.length > 0) {
199
+ throw new Error(
200
+ `--allow-empty skipped ${orphaned.join(
201
+ ', '
202
+ )}, but no such parameter exists in Parameter Store. --allow-empty ` +
203
+ `only rotates keys that already have a value; a skipped key with no ` +
204
+ `parameter stays in FRIGG_SSM_OFFLOADED_KEYS and fails every function ` +
205
+ `at cold start. Set a value and push without --allow-empty.`
206
+ );
207
+ }
208
+ for (const key of skipped) {
209
+ console.warn(
210
+ `⚠️ Skipping ${key}: no value in the environment; keeping the existing Parameter Store value (--allow-empty)`
211
+ );
212
+ }
213
+ }
214
+
215
+ if (specs.length === 0) {
216
+ return { pushed: [], skipped };
217
+ }
218
+
181
219
  const pushed = [];
182
220
  for (const spec of specs) {
183
221
  const input = {
@@ -1,5 +1,9 @@
1
1
  const { mockClient } = require('aws-sdk-client-mock');
2
- const { SSMClient, PutParameterCommand } = require('@aws-sdk/client-ssm');
2
+ const {
3
+ SSMClient,
4
+ PutParameterCommand,
5
+ GetParametersCommand,
6
+ } = require('@aws-sdk/client-ssm');
3
7
  const { pushOffloadedParameters, resolvePushRegion } = require('./index');
4
8
 
5
9
  describe('ssm-command pushOffloadedParameters', () => {
@@ -95,8 +99,11 @@ describe('ssm-command pushOffloadedParameters', () => {
95
99
  expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
96
100
  });
97
101
 
98
- it('skips missing values with allowEmpty instead of throwing', async () => {
102
+ it('skips missing values with allowEmpty when the parameter already exists', async () => {
99
103
  delete process.env.MY_SECRET;
104
+ ssmMock.on(GetParametersCommand).resolves({
105
+ Parameters: [{ Name: '/frigg/my-app/dev/MY_SECRET' }],
106
+ });
100
107
 
101
108
  const result = await pushOffloadedParameters(appDefinition, 'dev', {
102
109
  allowEmpty: true,
@@ -105,6 +112,16 @@ describe('ssm-command pushOffloadedParameters', () => {
105
112
  expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(1);
106
113
  });
107
114
 
115
+ it('aborts when allowEmpty skips a key that does not exist in Parameter Store', async () => {
116
+ delete process.env.MY_SECRET;
117
+ ssmMock.on(GetParametersCommand).resolves({ Parameters: [] });
118
+
119
+ await expect(
120
+ pushOffloadedParameters(appDefinition, 'dev', { allowEmpty: true })
121
+ ).rejects.toThrow(/MY_SECRET.*no such parameter|--allow-empty/s);
122
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
123
+ });
124
+
108
125
  it('rejects values over the standard tier limit and suggests advanced', async () => {
109
126
  process.env.MY_CONFIG = 'x'.repeat(4097);
110
127
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.625.0de0db4.0",
4
+ "version": "2.0.0--canary.625.6bef7de.0",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -26,9 +26,9 @@
26
26
  "@babel/eslint-parser": "^7.18.9",
27
27
  "@babel/parser": "^7.25.3",
28
28
  "@babel/traverse": "^7.25.3",
29
- "@friggframework/core": "2.0.0--canary.625.0de0db4.0",
30
- "@friggframework/schemas": "2.0.0--canary.625.0de0db4.0",
31
- "@friggframework/test": "2.0.0--canary.625.0de0db4.0",
29
+ "@friggframework/core": "2.0.0--canary.625.6bef7de.0",
30
+ "@friggframework/schemas": "2.0.0--canary.625.6bef7de.0",
31
+ "@friggframework/test": "2.0.0--canary.625.6bef7de.0",
32
32
  "@hapi/boom": "^10.0.1",
33
33
  "@inquirer/prompts": "^5.3.8",
34
34
  "axios": "^1.18.0",
@@ -56,8 +56,8 @@
56
56
  "validate-npm-package-name": "^5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@friggframework/eslint-config": "2.0.0--canary.625.0de0db4.0",
60
- "@friggframework/prettier-config": "2.0.0--canary.625.0de0db4.0",
59
+ "@friggframework/eslint-config": "2.0.0--canary.625.6bef7de.0",
60
+ "@friggframework/prettier-config": "2.0.0--canary.625.6bef7de.0",
61
61
  "aws-sdk-client-mock": "^4.1.0",
62
62
  "aws-sdk-client-mock-jest": "^4.1.0",
63
63
  "jest": "^30.1.3",
@@ -89,5 +89,5 @@
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  },
92
- "gitHead": "0de0db43a6403eb99de205e24fcde7e9761ba4f6"
92
+ "gitHead": "6bef7de84e4deea31c8dea40c177e2f7343eea4c"
93
93
  }