Skip to content

Commit 1fe2f09

Browse files
authored
feat(integ-runner): integ-runner --watch (#26087)
This PR adds a new option `--watch` that runs a single integration test in watch mode. See README for more details - Full deploy ![watch-demo1](https://github.com/aws/aws-cdk/assets/43035978/2c1af717-acec-4761-8a1e-2362f5c8bc89) - Deploy without logs ![watch-demo2](https://github.com/aws/aws-cdk/assets/43035978/b859f1d3-634c-44dc-bd3d-54a0e48dc9a1) - Deploy on file change ![watch-demo3](https://github.com/aws/aws-cdk/assets/43035978/b283ddd5-7e33-4c61-8477-12f25f4996ee) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3b6143b commit 1fe2f09

16 files changed

+912
-41
lines changed

packages/@aws-cdk/cdk-cli-wrapper/lib/cdk-wrapper.ts

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { DefaultCdkOptions, DeployOptions, DestroyOptions, SynthOptions, ListOptions, StackActivityProgress } from './commands';
2-
import { exec } from './utils';
1+
import { ChildProcess } from 'child_process';
2+
import { DefaultCdkOptions, DeployOptions, DestroyOptions, SynthOptions, ListOptions, StackActivityProgress, HotswapMode } from './commands';
3+
import { exec, watch } from './utils';
34

45
/**
56
* AWS CDK CLI operations
@@ -30,6 +31,11 @@ export interface ICdk {
3031
* cdk synth fast
3132
*/
3233
synthFast(options: SynthFastOptions): void;
34+
35+
/**
36+
* cdk watch
37+
*/
38+
watch(options: DeployOptions): ChildProcess;
3339
}
3440

3541
/**
@@ -176,6 +182,7 @@ export class CdkCliWrapper implements ICdk {
176182
...options.changeSetName ? ['--change-set-name', options.changeSetName] : [],
177183
...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [],
178184
...options.progress ? ['--progress', options.progress] : ['--progress', StackActivityProgress.EVENTS],
185+
...options.deploymentMethod ? ['--method', options.deploymentMethod] : [],
179186
...this.createDefaultArguments(options),
180187
];
181188

@@ -186,6 +193,50 @@ export class CdkCliWrapper implements ICdk {
186193
});
187194
}
188195

196+
public watch(options: DeployOptions): ChildProcess {
197+
let hotswap: string;
198+
switch (options.hotswap) {
199+
case HotswapMode.FALL_BACK:
200+
hotswap = '--hotswap-fallback';
201+
break;
202+
case HotswapMode.HOTSWAP_ONLY:
203+
hotswap = '--hotswap';
204+
break;
205+
default:
206+
hotswap = '--hotswap-fallback';
207+
break;
208+
}
209+
const deployCommandArgs: string[] = [
210+
'--watch',
211+
...renderBooleanArg('ci', options.ci),
212+
...renderBooleanArg('execute', options.execute),
213+
...renderBooleanArg('exclusively', options.exclusively),
214+
...renderBooleanArg('force', options.force),
215+
...renderBooleanArg('previous-parameters', options.usePreviousParameters),
216+
...renderBooleanArg('rollback', options.rollback),
217+
...renderBooleanArg('staging', options.staging),
218+
...renderBooleanArg('logs', options.traceLogs),
219+
hotswap,
220+
...options.reuseAssets ? renderArrayArg('--reuse-assets', options.reuseAssets) : [],
221+
...options.notificationArns ? renderArrayArg('--notification-arns', options.notificationArns) : [],
222+
...options.parameters ? renderMapArrayArg('--parameters', options.parameters) : [],
223+
...options.outputsFile ? ['--outputs-file', options.outputsFile] : [],
224+
...options.requireApproval ? ['--require-approval', options.requireApproval] : [],
225+
...options.changeSetName ? ['--change-set-name', options.changeSetName] : [],
226+
...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [],
227+
...options.progress ? ['--progress', options.progress] : ['--progress', StackActivityProgress.EVENTS],
228+
...options.deploymentMethod ? ['--method', options.deploymentMethod] : [],
229+
...this.createDefaultArguments(options),
230+
];
231+
232+
return watch([this.cdk, 'deploy', ...deployCommandArgs], {
233+
cwd: this.directory,
234+
verbose: this.showOutput,
235+
env: this.env,
236+
});
237+
238+
}
239+
189240
/**
190241
* cdk destroy
191242
*/

packages/@aws-cdk/cdk-cli-wrapper/lib/commands/deploy.ts

+47
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,53 @@ export interface DeployOptions extends DefaultCdkOptions {
105105
* @default StackActivityProgress.EVENTS
106106
*/
107107
readonly progress?: StackActivityProgress;
108+
109+
/**
110+
* Whether this 'deploy' command should actually delegate to the 'watch' command.
111+
*
112+
* @default false
113+
*/
114+
readonly watch?: boolean;
115+
116+
/**
117+
* Whether to perform a 'hotswap' deployment.
118+
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
119+
* and update the affected resources like Lambda functions directly.
120+
*
121+
* @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments
122+
*/
123+
readonly hotswap?: HotswapMode;
124+
125+
/**
126+
* Whether to show CloudWatch logs for hotswapped resources
127+
* locally in the users terminal
128+
*
129+
* @default - false
130+
*/
131+
readonly traceLogs?: boolean;
132+
133+
/**
134+
* Deployment method
135+
*/
136+
readonly deploymentMethod?: DeploymentMethod;
137+
}
138+
export type DeploymentMethod = 'direct' | 'change-set';
139+
140+
export enum HotswapMode {
141+
/**
142+
* Will fall back to CloudFormation when a non-hotswappable change is detected
143+
*/
144+
FALL_BACK = 'fall-back',
145+
146+
/**
147+
* Will not fall back to CloudFormation when a non-hotswappable change is detected
148+
*/
149+
HOTSWAP_ONLY = 'hotswap-only',
150+
151+
/**
152+
* Will not attempt to hotswap anything and instead go straight to CloudFormation
153+
*/
154+
FULL_DEPLOYMENT = 'full-deployment',
108155
}
109156

110157
/**

packages/@aws-cdk/cdk-cli-wrapper/lib/utils.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Helper functions for CDK Exec
2-
import { spawnSync } from 'child_process';
2+
import { spawn, spawnSync } from 'child_process';
33

44
/**
55
* Our own execute function which doesn't use shells and strings.
@@ -37,3 +37,22 @@ export function exec(commandLine: string[], options: { cwd?: string, json?: bool
3737
throw new Error('Command output is not JSON');
3838
}
3939
}
40+
41+
/**
42+
* For use with `cdk deploy --watch`
43+
*/
44+
export function watch(commandLine: string[], options: { cwd?: string, verbose?: boolean, env?: any } = { }) {
45+
const proc = spawn(commandLine[0], commandLine.slice(1), {
46+
stdio: ['ignore', 'pipe', options.verbose ? 'inherit' : 'pipe'], // inherit STDERR in verbose mode
47+
env: {
48+
...process.env,
49+
...options.env,
50+
},
51+
cwd: options.cwd,
52+
});
53+
proc.on('error', (err: Error) => {
54+
throw err;
55+
});
56+
57+
return proc;
58+
}

packages/@aws-cdk/cdk-cli-wrapper/test/cdk-wrapper.test.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as child_process from 'child_process';
22
import { CdkCliWrapper } from '../lib/cdk-wrapper';
33
import { RequireApproval, StackActivityProgress } from '../lib/commands';
44
let spawnSyncMock: jest.SpyInstance;
5+
let spawnMock: jest.SpyInstance;
56

67
beforeEach(() => {
78
spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValue({
@@ -12,6 +13,11 @@ beforeEach(() => {
1213
output: ['stdout', 'stderr'],
1314
signal: null,
1415
});
16+
spawnMock = jest.spyOn(child_process, 'spawn').mockImplementation(jest.fn(() => {
17+
return {
18+
on: jest.fn(() => {}),
19+
} as unknown as child_process.ChildProcess;
20+
}));
1521
});
1622

1723
afterEach(() => {
@@ -317,7 +323,33 @@ test('default synth', () => {
317323
);
318324
});
319325

320-
test('synth arguments', () => {
326+
test('watch arguments', () => {
327+
// WHEN
328+
const cdk = new CdkCliWrapper({
329+
directory: '/project',
330+
env: {
331+
KEY: 'value',
332+
},
333+
});
334+
cdk.watch({
335+
app: 'node bin/my-app.js',
336+
stacks: ['test-stack1'],
337+
});
338+
339+
// THEN
340+
expect(spawnMock).toHaveBeenCalledWith(
341+
expect.stringMatching(/cdk/),
342+
['deploy', '--watch', '--hotswap-fallback', '--progress', 'events', '--app', 'node bin/my-app.js', 'test-stack1'],
343+
expect.objectContaining({
344+
env: expect.objectContaining({
345+
KEY: 'value',
346+
}),
347+
cwd: '/project',
348+
}),
349+
);
350+
});
351+
352+
test('destroy arguments', () => {
321353
// WHEN
322354
const cdk = new CdkCliWrapper({
323355
directory: '/project',

packages/@aws-cdk/integ-runner/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ to be a self contained CDK app. The runner will execute the following for each f
7575
- `--test-regex`
7676
Detect integration test files matching this JavaScript regex pattern. If used multiple times, all files matching any one of the patterns are detected.
7777

78+
- `--watch`
79+
Run a single integration test in watch mode. In watch mode the integ-runner
80+
will not save any snapshots.
81+
7882
Use together with `--app` to fully customize how tests are run, or use with a single `--language` preset to change which files are detected for this language.
7983
- `--language`
8084
The language presets to use. You can discover and run tests written in multiple languages by passing this flag multiple times (`--language typescript --language python`). Defaults to all supported languages. Currently supported language presets are:
@@ -221,6 +225,56 @@ integ-runner --update-on-failed --disable-update-workflow integ.new-test.js
221225

222226
This is because for a new test we do not need to test the update workflow (there is nothing to update).
223227

228+
### watch
229+
230+
It can be useful to run an integration test in watch mode when you are iterating
231+
on a specific test.
232+
233+
```console
234+
integ-runner integ.new-test.js --watch
235+
```
236+
237+
In watch mode the integ test will run similar to `cdk deploy --watch` with the
238+
addition of also displaying the assertion results. By default the output will
239+
only show the assertion results.
240+
241+
- To show the console output from watch run with `-v`
242+
- To also stream the CloudWatch logs (i.e. `cdk deploy --watch --logs`) run with `-vv`
243+
244+
When running in watch mode most of the integ-runner functionality will be turned
245+
off.
246+
247+
- Snapshots will not be created
248+
- Update workflow will not be run
249+
- Stacks will not be cleaned up (you must manually clean up the stacks)
250+
- Only a single test can be run
251+
252+
Once you are done iterating using watch and want to create the snapshot you can
253+
run the integ test like normal to create the snapshot and clean up the test.
254+
255+
#### cdk.context.json
256+
257+
cdk watch depends on a `cdk.context.json` file existing with a `watch` key. The
258+
integ-runner will create a default `cdk.context.json` file if one does not
259+
exist.
260+
261+
```json
262+
{
263+
"watch": {}
264+
}
265+
```
266+
267+
You can further edit this file after it is created and add additional `watch`
268+
fields. For example:
269+
270+
```json
271+
{
272+
"watch": {
273+
"include": ["**/*.js"]
274+
}
275+
}
276+
```
277+
224278
### integ.json schema
225279

226280
See [@aws-cdk/cloud-assembly-schema/lib/integ-tests/schema.ts](../cloud-assembly-schema/lib/integ-tests/schema.ts)

0 commit comments

Comments
 (0)