Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): asset bundling skipped when using --exclusively with custom stack name #21248

7 changes: 3 additions & 4 deletions packages/@aws-cdk/core/lib/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1184,12 +1184,11 @@ export class Stack extends Construct implements ITaggable {
* Indicates whether the stack requires bundling or not
*/
public get bundlingRequired() {
const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['*'];
const bundlingStacks: string[] = this.node.tryGetContext(cxapi.BUNDLING_STACKS) ?? ['**'];

// bundlingStacks is of the form `Stage/Stack`, convert it to `Stage-Stack` before comparing to stack name
return bundlingStacks.some(pattern => minimatch(
this.stackName,
pattern.replace('/', '-'),
this.node.path, // use the same value for pattern matching as the aws-cdk CLI (displayName / hierarchicalId)
pattern,
));
}
}
Expand Down
151 changes: 150 additions & 1 deletion packages/@aws-cdk/core/test/staging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,155 @@ describe('staging', () => {
expect(dockerStubInput).not.toMatch(DockerStubCommand.MULTIPLE_FILES);
});

test('correctly skips bundling with stack under stage and custom stack name', () => {
// GIVEN
const app = new App();

const stage = new Stage(app, 'Stage');
stage.node.setContext(cxapi.BUNDLING_STACKS, ['Stage/Stack1']);

const stack1 = new Stack(stage, 'Stack1', { stackName: 'unrelated-stack1-name' });
const stack2 = new Stack(stage, 'Stack2', { stackName: 'unrelated-stack2-name' });
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');

// WHEN
new AssetStaging(stack1, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});

new AssetStaging(stack2, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.MULTIPLE_FILES],
},
});

// THEN
const dockerStubInput = readDockerStubInputConcat();
// Docker ran for the asset in Stack1
expect(dockerStubInput).toMatch(DockerStubCommand.SUCCESS);
// Docker did not run for the asset in Stack2
expect(dockerStubInput).not.toMatch(DockerStubCommand.MULTIPLE_FILES);
});

test('correctly bundles with stack under stage and the default stack pattern', () => {
// GIVEN
const app = new App();

const stage = new Stage(app, 'Stage');

const stack1 = new Stack(stage, 'Stack1');
const stack2 = new Stack(stage, 'Stack2');
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');

// WHEN
new AssetStaging(stack1, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});

new AssetStaging(stack2, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.MULTIPLE_FILES],
},
});

// THEN
const dockerStubInput = readDockerStubInputConcat();
// Docker ran for the asset in Stack1
expect(dockerStubInput).toMatch(DockerStubCommand.SUCCESS);
// Docker ran for the asset in Stack2
expect(dockerStubInput).toMatch(DockerStubCommand.MULTIPLE_FILES);
});

test('correctly bundles with stack under stage and partial globstar wildcard', () => {
// GIVEN
const app = new App();

const stage = new Stage(app, 'Stage');
stage.node.setContext(cxapi.BUNDLING_STACKS, ['**/Stack1']); // a single wildcard prefix ('*Stack1') won't match

const stack1 = new Stack(stage, 'Stack1');
const stack2 = new Stack(stage, 'Stack2');
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');

// WHEN
new AssetStaging(stack1, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});

new AssetStaging(stack2, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.MULTIPLE_FILES],
},
});

// THEN
const dockerStubInput = readDockerStubInputConcat();
// Docker ran for the asset in Stack1
expect(dockerStubInput).toMatch(DockerStubCommand.SUCCESS);
// Docker did not run for the asset in Stack2
expect(dockerStubInput).not.toMatch(DockerStubCommand.MULTIPLE_FILES);
});

test('correctly bundles selected stacks nested in Stack/Stage/Stack', () => {
// GIVEN
const app = new App();

const topStack = new Stack(app, 'TopStack');
topStack.node.setContext(cxapi.BUNDLING_STACKS, ['TopStack/MiddleStage/BottomStack']);

const middleStage = new Stage(topStack, 'MiddleStage');
const bottomStack = new Stack(middleStage, 'BottomStack');
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');

// WHEN
new AssetStaging(bottomStack, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.SUCCESS],
},
});
new AssetStaging(topStack, 'Asset', {
sourcePath: directory,
assetHashType: AssetHashType.OUTPUT,
bundling: {
image: DockerImage.fromRegistry('alpine'),
command: [DockerStubCommand.MULTIPLE_FILES],
},
});

const dockerStubInput = readDockerStubInputConcat();
// Docker ran for the asset in BottomStack
expect(dockerStubInput).toMatch(DockerStubCommand.SUCCESS);
// Docker did not run for the asset in TopStack
expect(dockerStubInput).not.toMatch(DockerStubCommand.MULTIPLE_FILES);
});

test('bundling still occurs with partial wildcard', () => {
// GIVEN
const app = new App();
Expand All @@ -954,7 +1103,7 @@ describe('staging', () => {
expect(asset.assetHash).toEqual('33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); // hash of MyStack/Asset
});

test('bundling still occurs with full wildcard', () => {
test('bundling still occurs with a single wildcard', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ are written to the same output file where each stack artifact ID is a key in the


```console
$ cdk deploy '*' --outputs-file "/Users/code/myproject/outputs.json"
$ cdk deploy '**' --outputs-file "/Users/code/myproject/outputs.json"
```

Example `outputs.json` after deployment of multiple stacks
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/cxapp/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true;
}

const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['*'];
const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['**'];
context[cxapi.BUNDLING_STACKS] = bundlingStacks;

debug('context:', context);
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ export class Settings {
// If we deploy, diff, synth or watch a list of stacks exclusively we skip
// bundling for all other stacks.
bundlingStacks = argv.exclusively
? argv.STACKS ?? ['*']
: ['*'];
? argv.STACKS ?? ['**']
: ['**'];
} else { // Skip bundling for all stacks
bundlingStacks = [];
}
Expand Down
10 changes: 5 additions & 5 deletions packages/aws-cdk/test/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,24 @@ test('bundling stacks defaults to an empty list', () => {
expect(settings.get(['bundlingStacks'])).toEqual([]);
});

test('bundling stacks defaults to * for deploy', () => {
test('bundling stacks defaults to ** for deploy', () => {
// GIVEN
const settings = Settings.fromCommandLineArguments({
_: [Command.DEPLOY],
});

// THEN
expect(settings.get(['bundlingStacks'])).toEqual(['*']);
expect(settings.get(['bundlingStacks'])).toEqual(['**']);
});

test('bundling stacks defaults to * for watch', () => {
test('bundling stacks defaults to ** for watch', () => {
// GIVEN
const settings = Settings.fromCommandLineArguments({
_: [Command.WATCH],
});

// THEN
expect(settings.get(['bundlingStacks'])).toEqual(['*']);
expect(settings.get(['bundlingStacks'])).toEqual(['**']);
});

test('bundling stacks with deploy exclusively', () => {
Expand Down Expand Up @@ -154,4 +154,4 @@ test('providing a build arg', () => {

// THEN
expect(settings.get(['build'])).toEqual('mvn package');
});
});