Skip to content

Commit d5f8730

Browse files
Merge pull request #247 from contentstack/cl-7761
feat(variables): send variables set and unset through the PATCH endpoint
2 parents 455848e + 23b71d1 commit d5f8730

15 files changed

Lines changed: 431 additions & 481 deletions

‎.talismanrc‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ fileignoreconfig:
231231
- "a project name passed on argv to the project resolver"
232232
- "expect\\(Object.keys\\(DEPENDENCIES\\)\\).toEqual\\(\\['project', 'env'\\]\\)"
233233
- filename: AGENTS.md
234-
checksum: ace82b5dc26aa4b681378ef313ed3e5eeb2a5d7f3ab47d6305dc9c97e885f244
234+
checksum: 66b94ec2bcb34947bcda15e6364dceee7edb50bfe851e26e6c737425315e81da
235235
allowed_patterns:
236236
- "Do not add an exemption to a guard to make a new file pass"
237237
- "passes its own error wording to `client.request\\(req, MESSAGES\\)`"
@@ -315,7 +315,7 @@ fileignoreconfig:
315315
- filename: src/commands/launch/env/delete.test.ts
316316
checksum: 14116789eb5f9000f2a7d6f569cd15aabf2a1ec3b127ed4427afe13fd515f0c2
317317
- filename: src/environments/environment.errors.ts
318-
checksum: 37256331379dfda26ae10a3bc46399a716fb2740151d47b34f0f8eaeeeef48da
318+
checksum: 3e17db3577ba944633f97063d208bdec57c833d4b6c38e203796a9fea43baacc
319319
- filename: src/core/prompt.ts
320320
checksum: 3285bdbccc31cba5b50eafe8ed8be4750af6339078a3eaee6df1e0630979aa2c
321321
- filename: src/core/prompt.test.ts
@@ -337,9 +337,9 @@ fileignoreconfig:
337337
- filename: src/commands/launch/variables/list.test.ts
338338
checksum: 87ccb762f4931326fc50bca1c01aeb4a16d552e4dfa141d00370fdfafa4363be
339339
- filename: src/commands/launch/variables/set.test.ts
340-
checksum: 5f8029b2d49410dd59576b6bda5a2ad5cb452703fa1bf6fd4690f2ac356abea5
340+
checksum: a6fc222527e946aee1c0015e3961bc3dff13aabec40c2162e7d2d3f2bb2d2cde
341341
- filename: src/commands/launch/variables/unset.test.ts
342-
checksum: 857da0c6e10243477aa78cd112a126770f679b75b7b67eab929eeb2f76a41c9d
342+
checksum: 709e849b1ef834bdc433936438110ff3f79b8cc0e7bec0f505c56ffd4f67bd8b
343343
- filename: src/commands/launch/variables/unset.ts
344344
checksum: b25c2b1547a1ed5b216c57b26bc18744ad04746e01efcc40e9469d1a5e472537
345345
- filename: src/environments/environment.errors.test.ts
@@ -356,6 +356,8 @@ fileignoreconfig:
356356
checksum: c3d5e33189736bb42b7c4fac7375cf0307681b34feca4830b49331f55a130f78
357357
- filename: src/variables/variable.dotenv.ts
358358
checksum: 337273d2ce8d987a6e7cf6a0f743df5031c24ac719cbe31e99fff16d7ffefa79
359+
- filename: src/variables/variable.errors.ts
360+
checksum: f66b960365b83561f70b5fcef521bcc8a00d99b4415fb6a5df5bcf752a91d480
359361
- filename: src/variables/variable.files.test.ts
360362
checksum: d8b0efd6bb448c4d501ba52c47119b352ef29d6b5b66cec0e7d373a0ce91a36c
361363
- filename: src/variables/variable.inputs.test.ts
@@ -377,7 +379,7 @@ fileignoreconfig:
377379
- filename: src/variables/variable.presenter.ts
378380
checksum: ee8e4dcf75ad7e849d1e8e64ce6a6549d13fc62dc9aa322d96eb2e725d629db5
379381
- filename: src/variables/variable.set.test.ts
380-
checksum: 68bd428a7f0a26e495cd9f79779cbc9fb5017cdee684594e90a983ea14303a62
382+
checksum: df7d36ff83d1489301afad5418ebe76338adb7bcad51d1e738f989362be46903
381383
- filename: src/variables/variable.sources.test.ts
382384
checksum: 63ad95e998cbc6d33fabe4ce76797e8788b17abec3833b4fed2360eab01a6f79
383385
- filename: src/variables/variable.sources.ts
@@ -391,25 +393,25 @@ fileignoreconfig:
391393
- filename: src/variables/variable.summary.ts
392394
checksum: 0da287557a5c4280074d1a26aeff4f7d95b205e4266ff47c18b62665134f5bdc
393395
- filename: src/variables/variable.unset.test.ts
394-
checksum: ef31b77f66913a10f1b865b7a6568bef71ff02803c6636971605b840b489671f
396+
checksum: d449ba8483eeb932c155b2b210ce2088785d0961680eaa3efd036553c46579e2
395397
- filename: src/variables/variable.unset.ts
396-
checksum: 250ce39c694d745370717771dc16d20e2f6b9516f43043900d4a40bde7fc04db
398+
checksum: 0d21f5878aa1f05dcafc864dd7dcb23e3155d3d24b97e248e62ad9bf5bcd7745
397399
- filename: src/variables/variable.values.test.ts
398400
checksum: 56725066fdabce3afea373c70bee10a6db4c1409e3a8f6f1e7f191ea6e602419
399401
- filename: src/variables/variable.values.ts
400402
checksum: 70f23d8c993b0f0baf6522a918b2f011485a5d9c038548f06dea6ea1d0e83e0e
401403
- filename: src/variables/variable.writer.test.ts
402-
checksum: 7dcd68d3c1b0df258fa86455b1d02547255be8fe77b656b3a7188824225220ab
404+
checksum: 7991a4ba0c58f187dfebf01092642459ce89b297f453159ca8e0953a2ee7e2b8
403405
- filename: src/variables/variable.writer.ts
404-
checksum: a6fc266af2bae734ff1bb716c854a154b5c3e858a0ba2798fc17d5bdbdfde1b9
406+
checksum: 11acfccfad30b66a44954e97127d2a7bcebdb80f92299da6d645ac847c97192a
405407
- filename: src/variables/variables.api.test.ts
406-
checksum: 33d41fb8383e0908260ca3df1d126227e614330acde0c6f195b877245e51cfd6
408+
checksum: e716f85c2d97aadf95088cf7cf49cb46354d2581f8ed6e194cb0af876c65fb01
407409
- filename: src/variables/variables.api.ts
408-
checksum: 2fca1f8fc784d5f859b13a24bd3cd09bda7aa92e7b3d65c0ecc92d4c0de954fa
410+
checksum: 1b5a93e17fc1cc338afd5a3fc24a6ee444f551037c54797453481855e3300807
409411
- filename: test/fixtures/cms-environment-variables.json
410412
checksum: cb2cbb2d1d858b1cf2115c218a0ecca04ce04baf054fe11fe6123589c1afce27
411413
- filename: test/integration/variables-list.test.ts
412414
checksum: c0ccb0bc394c9013f75b69c4354cc5d52b2f6916a35af902de36bef1cda1648c
413415
- filename: test/integration/variables-set-unset.test.ts
414-
checksum: f211ebcf35f39b26e5a55d81f468287670ade819899c6635a579b4eae68b1c50
416+
checksum: 25ae86d9a50f02c5209823a91ae0ff1433e5a0bff3cfa27f84e48274d0e9c571
415417
version: "1.0"

‎AGENTS.md‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ src/
144144
stacks/ the Contentstack stack and delivery-token lookups behind --from-stack, through the
145145
CmaSession (no Launch endpoint)
146146
variables/ the shared variable-source resolver (--var, --dotenv, --from-stack, a detected .env file), the
147-
read-modify-write writer behind launch:variables:set / :unset, and the masked
148-
presenter behind :list. It has no repository of its own: it reads and writes through
149-
environments, and reads the stack template through its own VariablesApi. EnvironmentBuilder calls back into it for the create commands, so the
150-
two folders each name the other in the layering allow-list
147+
writer behind launch:variables:set / :unset, and the masked presenter behind :list. Its
148+
VariablesApi owns the per-variable PATCH and the stack template read; the
149+
pre-confirmation read for unset and the list read go through environments.
150+
EnvironmentBuilder calls back into it for the create commands, so the two folders
151+
each name the other in the layering allow-list
151152
deployments/ the deployment repository, the status classification, the
152153
wait/stream loop and LogTail that projects:create, deployments:create
153154
and logs:get all drive, and the DeploymentFollower that watches one
@@ -321,14 +322,16 @@ the yellow `Project not deleted.` and exits 3 (`EXIT_CANCELLED`) without the `Er
321322
`CancelledError` would print; Ctrl-C at the question is still a `CancelledError`. The success line is
322323
the green `✔ Project deleted successfully.`, matching `projects:update`.
323324

324-
**Environment variables (`launch:variables:*`).** There is no per-variable endpoint, so `set` and `unset`
325-
are `GET .../environments/:uid`, merge locally, then `PUT` the complete `environmentVariables` array and
326-
nothing else: the update body is partial on the server, so no other environment field can be dropped by it.
327-
Every source is resolved and every value prompted for before the `GET`, so no user interaction sits between
328-
the read and the write; `unset` reads once to validate the keys and name the environment, confirms, then
329-
reads again immediately before the `PUT`. Two writers racing lose one change - the API offers no ETag and no
330-
partial update (CL-7179 is the proposed `PATCH`) - and FR34 asks that this be documented rather than treated
331-
as a defect.
325+
**Environment variables (`launch:variables:*`).** `set` and `unset` call the per-variable
326+
`PATCH .../environments/:uid/variables` (`VariablesApi.patch`, CL-7179) with a body of `{set: [{key, value}]}`
327+
or `{unset: [key]}`, never both, so the CLI no longer reads the environment to merge and cannot overwrite a
328+
concurrent change to another key. The server merges under a compare-and-swap and its response carries keys
329+
only, which the CLI ignores. `unset` still reads the environment once, before it asks, to name it in the
330+
question and to refuse a key that is not set with exit 2 before any prompt; a key that disappears after that
331+
read comes back as 404 `KEY.NOT_FOUND`, which `VariablesApi.patch` rewords to the same not-set usage error
332+
(`variable.errors.ts`). A 409 `CONCURRENT_UPDATE` (the server gave up after its retries) exits 1 with advice to run the
333+
command again; `PATCH_EMPTY` and `CONFLICTING_KEYS` cannot be sent by this CLI and read as a CLI bug. A PUT or
334+
a UI save racing a PATCH is still last-writer-wins on the whole array, by design on the server.
332335

333336
`collectVariables` in `src/variables/variable.sources.ts` is the one resolver: stack (`--from-stack`), then
334337
`--dotenv`, then `--var`, later wins, and the summary line names every source's new keys and every override.
@@ -339,7 +342,7 @@ prints a yellow `Skipped adding variables.` and sends nothing (sources that supp
339342
under `--data-dir`, always printing the file it chose, and fails with exit 2 when there is none. A bare `--var KEY` is a hidden prompt on a terminal, a stdin read for exactly one bare key without one, and
340343
exit 2 naming `--dotenv -` for two or more. The API refuses an empty value, so `KEY=` is a usage error naming
341344
the key, and an empty value inside a dotenv file is skipped with its key reported. No value reaches stdout, a
342-
prompt message, a confirmation or an error: `list` masks through `redactedColumn`, and a failed `PUT` is worded from
345+
prompt message, a confirmation or an error: `list` masks through `redactedColumn`, and a failed `PATCH` is worded from
343346
`ENVIRONMENT_ERROR_MESSAGES`, never from the API's own text.
344347

345348
`projects:create` and `env:create` take `--var` and `--dotenv` and reach the same resolver through

‎src/commands/launch/variables/set.test.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,9 @@ function commandUnderTest(resolved: Record<string, unknown>, files: Record<strin
2323
isTTY: false,
2424
outputIsTTY: false,
2525
api: {
26-
environments: {
27-
get: async () => ({ environmentVariables: [] }),
28-
update: async (params: unknown) => {
26+
variables: {
27+
patch: async (params: unknown) => {
2928
updates.push(params);
30-
return {};
3129
},
3230
},
3331
},
@@ -38,13 +36,13 @@ function commandUnderTest(resolved: Record<string, unknown>, files: Record<strin
3836
}
3937

4038
describe('launch:variables:set', () => {
41-
it('sets the --var pairs through the environment endpoints and prints the provenance', async () => {
39+
it('sets the --var pairs through the variables patch and prints the provenance', async () => {
4240
const { command, lines, updates } = commandUnderTest({ org: 'o', project: 'p', env: 'e', var: ['A=1', 'B=2'] });
4341

4442
await command.run();
4543

4644
expect(updates).toEqual([
47-
{ org: 'o', project: 'p', environment: 'e', update: { environmentVariables: [{ key: 'A', value: '1' }, { key: 'B', value: '2' }] } },
45+
{ org: 'o', project: 'p', environment: 'e', set: [{ key: 'A', value: '1' }, { key: 'B', value: '2' }] },
4846
]);
4947
expect(lines).toEqual(['✔ 2 variables set — 2 from --var']);
5048
});

‎src/commands/launch/variables/unset.test.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ function commandUnderTest(harness: Harness) {
4747
calls.push('get');
4848
return { name: 'staging', environmentVariables: [{ key: 'API_KEY', value: 'never-printed-value' }, { key: 'DEBUG', value: 'x' }] };
4949
},
50-
update: async (params: unknown) => {
51-
calls.push('update');
50+
},
51+
variables: {
52+
patch: async (params: unknown) => {
53+
calls.push('patch');
5254
updates.push(params);
5355

5456
if (harness.updateFails) {
5557
throw harness.updateFails;
5658
}
57-
58-
return {};
5959
},
6060
},
6161
},
@@ -74,8 +74,8 @@ describe('launch:variables:unset', () => {
7474
expect(inquired).toEqual([
7575
{ type: 'confirm', name: 'confirm', message: 'Are you sure you want to remove API_KEY from "staging"?', default: false },
7676
]);
77-
expect(calls).toEqual(['get', 'get', 'update']);
78-
expect(updates).toEqual([{ ...SCOPE, update: { environmentVariables: [{ key: 'DEBUG', value: 'x' }] } }]);
77+
expect(calls).toEqual(['get', 'patch']);
78+
expect(updates).toEqual([{ ...SCOPE, unset: ['API_KEY'] }]);
7979
expect(lines).toEqual(['✔ API_KEY removed from staging.']);
8080
expect(lines.join('')).not.toContain('never-printed-value');
8181
});
@@ -86,7 +86,7 @@ describe('launch:variables:unset', () => {
8686
await command.run();
8787

8888
expect(inquired).toEqual([]);
89-
expect(calls).toEqual(['get', 'get', 'update']);
89+
expect(calls).toEqual(['get', 'patch']);
9090
expect(lines).toEqual(['✔ API_KEY removed from staging.']);
9191
});
9292

‎src/environments/environment.errors.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ErrorMessages } from '../transport/errors';
22

3+
const CLI_BUG_MESSAGE = 'The CLI sent an update the Launch API could not accept. This is a bug in the CLI.';
4+
35
export const ENVIRONMENT_ERROR_MESSAGES: ErrorMessages = {
46
'launch.ENVIRONMENT.NOT_FOUND': 'No environment found with that name or UID.',
57
'launch.ENVIRONMENT.DUPLICATE_NAME': 'An environment with that name already exists in this project.',
@@ -10,6 +12,11 @@ export const ENVIRONMENT_ERROR_MESSAGES: ErrorMessages = {
1012
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.MAX_COUNT_REACHED':
1113
'This environment has reached its limit on the number of variables.',
1214
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.DUPLICATE_KEYS': 'The same variable key appears more than once.',
15+
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.CONCURRENT_UPDATE':
16+
"Another change to this environment's variables landed at the same moment, so nothing was changed. " +
17+
'Run the command again.',
18+
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.PATCH_EMPTY': CLI_BUG_MESSAGE,
19+
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.CONFLICTING_KEYS': CLI_BUG_MESSAGE,
1320
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.KEY.NOT_EMPTY': 'A variable key cannot be empty.',
1421
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.KEY.NOT_STRING': 'A variable key must be text.',
1522
'launch.ENVIRONMENT.ENVIRONMENT_VARIABLES.KEY.TOO_LONG': 'A variable key is too long.',

‎src/variables/variable.errors.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { UsageError } from '../core/errors';
2+
3+
export function notSetError(keys: string[]): UsageError {
4+
return new UsageError(`${keys.join(', ')} ${keys.length === 1 ? 'is' : 'are'} not set in this environment.`);
5+
}
6+
7+
export function someNotSetError(keys: string[]): UsageError {
8+
return keys.length === 1
9+
? notSetError(keys)
10+
: new UsageError(`At least one of ${keys.join(', ')} is not set in this environment.`);
11+
}

0 commit comments

Comments
 (0)