From 8dad596e88f8e51c1a1083898a66408dee51720f Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:41:08 +0900 Subject: [PATCH 01/33] init --- ...ublish-requires-unstable-flag.integtest.ts | 45 +++++++ .../toolkit-lib/lib/api/io/toolkit-action.ts | 1 + packages/aws-cdk/README.md | 61 +++++++++ packages/aws-cdk/lib/cli/cdk-toolkit.ts | 118 ++++++++++++++++++ packages/aws-cdk/lib/cli/cli-config.ts | 15 +++ .../aws-cdk/lib/cli/cli-type-registry.json | 40 ++++++ packages/aws-cdk/lib/cli/cli.ts | 15 +++ .../aws-cdk/lib/cli/convert-to-user-input.ts | 21 ++++ .../lib/cli/parse-command-line-arguments.ts | 37 ++++++ .../aws-cdk/lib/cli/user-configuration.ts | 1 + packages/aws-cdk/lib/cli/user-input.ts | 63 ++++++++++ .../aws-cdk/test/cli/cli-arguments.test.ts | 1 + packages/aws-cdk/test/cli/cli.test.ts | 118 ++++++++++++++++++ 13 files changed, 536 insertions(+) create mode 100644 packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts new file mode 100644 index 000000000..cfb241eb0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts @@ -0,0 +1,45 @@ +import { integTest, withAws, withSpecificCdkApp } from '../../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'publish command requires unstable flag', + withAws( + withSpecificCdkApp('simple-app', async (fixture) => { + // Should fail without unstable flag + await expect(fixture.cdk(['publish'])).rejects.toThrow(/unstable/); + }), + ), +); + +integTest( + 'publish command works with unstable flag', + withAws( + withSpecificCdkApp('simple-app', async (fixture) => { + // Bootstrap first + await fixture.cdk(['bootstrap', '--unstable=publish']); + + // Should succeed with unstable flag + const output = await fixture.cdk(['publish', '--unstable=publish']); + + // Expect success message or completion + expect(output).toBeTruthy(); + }), + ), +); + +integTest( + 'publish command respects --exclusively flag', + withAws( + withSpecificCdkApp('dependency-app', async (fixture) => { + // Bootstrap first + await fixture.cdk(['bootstrap', '--unstable=publish']); + + // Publish only specific stack + const output = await fixture.cdk(['publish', 'Stack1', '--unstable=publish', '--exclusively']); + + // Should publish only Stack1 assets + expect(output).toBeTruthy(); + }), + ), +); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts index a5abd05b2..35b8c2aee 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts @@ -10,6 +10,7 @@ export type ToolkitAction = | 'deploy' | 'drift' | 'rollback' +| 'publish' | 'watch' | 'destroy' | 'doctor' diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 5f32a584c..8c12b097d 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -19,6 +19,7 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used t | [`cdk synth`](#cdk-synth) | Synthesize a CDK app to CloudFormation template(s) | | [`cdk diff`](#cdk-diff) | Diff stacks against current state | | [`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account | +| [`cdk publish`](#cdk-publish) | Publish assets for stack(s) without deploying (unstable) | | [`cdk rollback`](#cdk-rollback) | Roll back a failed deployment | | [`cdk import`](#cdk-import) | Import existing AWS resources into a CDK stack | | [`cdk migrate`](#cdk-migrate) | Migrate AWS resources, CloudFormation stacks, and CloudFormation templates to CDK | @@ -575,6 +576,66 @@ For technical implementation details (function calls, file locations), see [docs ![Deploy flowchart](./images/deploy-flowchart.png) +### `cdk publish` + +> **Note:** This is an **unstable** feature. You must opt-in using `--unstable=publish`. + +Publishes assets (such as Docker images and file assets) for the specified stack(s) to their respective destinations (ECR repositories, S3 buckets) without performing a deployment. + +This is useful in CI/CD pipelines where you want to separate the build/publish phase from the deployment phase. For example: + +- Publish assets once in a build stage +- Deploy to multiple environments (dev, staging, prod) using the already-published assets +- Ensure all assets are available before starting deployment + +```console +$ # Publish assets for a single stack +$ cdk publish MyStack --unstable=publish + +$ # Publish assets for all stacks +$ cdk publish --all --unstable=publish + +$ # Publish with parallel asset operations +$ cdk publish MyStack --unstable=publish --asset-parallelism + +$ # Force re-publish even if assets already exist +$ cdk publish MyStack --unstable=publish --force +``` + +#### Options + +- `--all`: Publish assets for all stacks in the app +- `--exclusively` (`-e`): Only publish assets for the specified stack(s), don't include dependencies +- `--force` (`-f`): Always publish assets, even if they are already published +- `--asset-parallelism`: Whether to build/publish assets in parallel (default: true) +- `--concurrency N`: Maximum number of simultaneous asset publishing operations (default: 1) +- `--role-arn`: ARN of the IAM role to use for asset publishing + +#### Use Cases + +**CI/CD Pipeline Separation:** + +```bash +# Build stage +cdk publish --all --unstable=publish + +# Deploy stage (dev) +cdk deploy --all + +# Deploy stage (staging) - reuses already published assets +cdk deploy --all + +# Deploy stage (prod) - reuses already published assets +cdk deploy --all +``` + +**Debugging Asset Issues:** + +```bash +# Force re-publish a specific asset to debug upload issues +cdk publish MyStack --unstable=publish --force +``` + ### `cdk rollback` If a deployment performed using `cdk deploy --no-rollback` fails, your diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index b22eed419..c039e6379 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -774,6 +774,75 @@ export class CdkToolkit { } } + public async publish(options: PublishOptions) { + const startSynthTime = new Date().getTime(); + const stackCollection = await this.selectStacksForDeploy( + options.selector, + options.exclusively, + ); + const elapsedSynthTime = new Date().getTime() - startSynthTime; + await this.ioHost.asIoHelper().defaults.info(`\n✨ Synthesis time: ${formatTime(elapsedSynthTime)}s\n`); + + if (stackCollection.stackCount === 0) { + await this.ioHost.asIoHelper().defaults.error('No stacks selected'); + return; + } + + const buildAsset = async (assetNode: AssetBuildNode) => { + await this.props.deployments.buildSingleAsset( + assetNode.assetManifestArtifact, + assetNode.assetManifest, + assetNode.asset, + { + stack: assetNode.parentStack, + roleArn: options.roleArn, + stackName: assetNode.parentStack.stackName, + }, + ); + }; + + const publishAsset = async (assetNode: AssetPublishNode) => { + await this.props.deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { + stack: assetNode.parentStack, + roleArn: options.roleArn, + stackName: assetNode.parentStack.stackName, + forcePublish: options.force, + }); + }; + + const startPublishTime = new Date().getTime(); + const stacks = stackCollection.stackArtifacts; + const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [ + stack, + ...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)), + ]); + + const workGraph = new WorkGraphBuilder( + asIoHelper(this.ioHost, 'publish'), + true, // prebuild all assets + ).build(stacksAndTheirAssetManifests); + + await this.ioHost.asIoHelper().defaults.info('Publishing assets for %s stack(s)', chalk.bold(String(stackCollection.stackCount))); + + const graphConcurrency: Concurrency = { + 'stack': 1, + 'asset-build': 1, + 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, + }; + + await workGraph.doParallel(graphConcurrency, { + deployStack: async () => { + // No-op: we're only publishing assets, not deploying + }, + buildAsset, + publishAsset, + }); + + const elapsedPublishTime = new Date().getTime() - startPublishTime; + await this.ioHost.asIoHelper().defaults.info(chalk.green('\n✨ Assets published successfully')); + await this.ioHost.asIoHelper().defaults.info(`\n✨ Total time: ${formatTime(elapsedPublishTime)}s\n`); + } + public async watch(options: WatchOptions) { const rootDir = path.dirname(path.resolve(PROJECT_CONFIG)); const ioHelper = asIoHelper(this.ioHost, 'watch'); @@ -1825,6 +1894,55 @@ export interface RollbackOptions { readonly validateBootstrapStackVersion?: boolean; } +export interface PublishOptions { + /** + * Criteria for selecting stacks + */ + readonly selector: StackSelector; + + /** + * Only select the given stack + * + * @default false + */ + readonly exclusively?: boolean; + + /** + * Name of the toolkit stack to use/deploy + * + * @default CDKToolkit + */ + readonly toolkitStackName?: string; + + /** + * Role to pass to CloudFormation for deployment + * + * @default - Current role + */ + readonly roleArn?: string; + + /** + * Always publish assets, even if they are already published + * + * @default false + */ + readonly force?: boolean; + + /** + * Whether to build/publish assets in parallel + * + * @default true + */ + readonly assetParallelism?: boolean; + + /** + * Maximum number of simultaneous asset publishing operations + * + * @default 1 + */ + readonly concurrency?: number; +} + export interface ImportOptions extends CfnDeployOptions { /** * Build a physical resource mapping and write it to the given file, without performing the actual import operation diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 73f46cf7e..1c79c39ba 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -243,6 +243,21 @@ export async function makeConfig(): Promise { }, }, }, + 'publish': { + description: 'Publish assets for the given stack(s) without deploying', + arg: { + name: 'STACKS', + variadic: true, + }, + options: { + 'all': { type: 'boolean', desc: 'Publish assets for all available stacks', default: false }, + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only publish assets for requested stacks, don\'t include dependencies' }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the existing CDK toolkit stack', requiresArg: true }, + 'force': { type: 'boolean', alias: 'f', desc: 'Always publish assets, even if they are already published', default: false }, + 'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }, + 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', default: 1, requiresArg: true }, + }, + }, 'import': { description: 'Import existing resource(s) into the given STACK', arg: { diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 439316754..eb5a6ad3a 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -597,6 +597,46 @@ } } }, + "publish": { + "description": "Publish assets for the given stack(s) without deploying", + "arg": { + "name": "STACKS", + "variadic": true + }, + "options": { + "all": { + "type": "boolean", + "desc": "Publish assets for all available stacks", + "default": false + }, + "exclusively": { + "type": "boolean", + "alias": "e", + "desc": "Only publish assets for requested stacks, don't include dependencies" + }, + "toolkit-stack-name": { + "type": "string", + "desc": "The name of the existing CDK toolkit stack", + "requiresArg": true + }, + "force": { + "type": "boolean", + "alias": "f", + "desc": "Always publish assets, even if they are already published", + "default": false + }, + "asset-parallelism": { + "type": "boolean", + "desc": "Whether to build/publish assets in parallel" + }, + "concurrency": { + "type": "number", + "desc": "Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.", + "default": 1, + "requiresArg": true + } + } + }, "import": { "description": "Import existing resource(s) into the given STACK", "arg": { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 01ef7e138..1a0cd48bc 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -420,6 +420,21 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { requiresArg: true, }), ) + .command('publish [STACKS..]', 'Publish assets for the given stack(s) without deploying', (yargs: Argv) => + yargs + .option('all', { + default: false, + type: 'boolean', + desc: 'Publish assets for all available stacks', + }) + .option('exclusively', { + default: undefined, + type: 'boolean', + alias: 'e', + desc: "Only publish assets for requested stacks, don't include dependencies", + }) + .option('toolkit-stack-name', { + default: undefined, + type: 'string', + desc: 'The name of the existing CDK toolkit stack', + requiresArg: true, + }) + .option('force', { + default: false, + type: 'boolean', + alias: 'f', + desc: 'Always publish assets, even if they are already published', + }) + .option('asset-parallelism', { + default: undefined, + type: 'boolean', + desc: 'Whether to build/publish assets in parallel', + }) + .option('concurrency', { + default: 1, + type: 'number', + desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', + requiresArg: true, + }), + ) .command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs .option('execute', { diff --git a/packages/aws-cdk/lib/cli/user-configuration.ts b/packages/aws-cdk/lib/cli/user-configuration.ts index 733f56a83..b3526f960 100644 --- a/packages/aws-cdk/lib/cli/user-configuration.ts +++ b/packages/aws-cdk/lib/cli/user-configuration.ts @@ -28,6 +28,7 @@ export enum Command { GC = 'gc', FLAGS = 'flags', ROLLBACK = 'rollback', + PUBLISH = 'publish', IMPORT = 'import', ACKNOWLEDGE = 'acknowledge', ACK = 'ack', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 9d3f9cc11..2c2892581 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -60,6 +60,11 @@ export interface UserInput { */ readonly rollback?: RollbackOptions; + /** + * Publish assets for the given stack(s) without deploying + */ + readonly publish?: PublishOptions; + /** * Import existing resource(s) into the given STACK */ @@ -980,6 +985,64 @@ export interface RollbackOptions { readonly STACKS?: Array; } +/** + * Publish assets for the given stack(s) without deploying + * + * @struct + */ +export interface PublishOptions { + /** + * Publish assets for all available stacks + * + * @default - false + */ + readonly all?: boolean; + + /** + * Only publish assets for requested stacks, don't include dependencies + * + * aliases: e + * + * @default - undefined + */ + readonly exclusively?: boolean; + + /** + * The name of the existing CDK toolkit stack + * + * @default - undefined + */ + readonly toolkitStackName?: string; + + /** + * Always publish assets, even if they are already published + * + * aliases: f + * + * @default - false + */ + readonly force?: boolean; + + /** + * Whether to build/publish assets in parallel + * + * @default - undefined + */ + readonly assetParallelism?: boolean; + + /** + * Maximum number of simultaneous asset publishing operations (dependency permitting) to execute. + * + * @default - 1 + */ + readonly concurrency?: number; + + /** + * Positional argument for publish + */ + readonly STACKS?: Array; +} + /** * Import existing resource(s) into the given STACK * diff --git a/packages/aws-cdk/test/cli/cli-arguments.test.ts b/packages/aws-cdk/test/cli/cli-arguments.test.ts index 3c3269361..c4a63e767 100644 --- a/packages/aws-cdk/test/cli/cli-arguments.test.ts +++ b/packages/aws-cdk/test/cli/cli-arguments.test.ts @@ -133,6 +133,7 @@ describe('config', () => { metadata: expect.anything(), migrate: expect.anything(), rollback: expect.anything(), + publish: expect.anything(), synth: expect.anything(), watch: expect.anything(), notices: expect.anything(), diff --git a/packages/aws-cdk/test/cli/cli.test.ts b/packages/aws-cdk/test/cli/cli.test.ts index df753f36b..b0c3e846b 100644 --- a/packages/aws-cdk/test/cli/cli.test.ts +++ b/packages/aws-cdk/test/cli/cli.test.ts @@ -66,6 +66,20 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({ _: ['deploy'], parameters: [], }; + } else if (args.includes('publish')) { + result = { ...result, _: ['publish'] }; + + // Handle publish-specific flags + if (args.includes('--exclusively')) { + result = { ...result, exclusively: true }; + } + if (args.includes('--force')) { + result = { ...result, force: true }; + } + const concurrencyIndex = args.findIndex((arg: string) => arg === '--concurrency'); + if (concurrencyIndex !== -1 && args[concurrencyIndex + 1]) { + result = { ...result, concurrency: parseInt(args[concurrencyIndex + 1], 10) }; + } } else if (args.includes('flags')) { result = { ...result, _: ['flags'] }; } @@ -510,6 +524,110 @@ describe('gc command tests', () => { }); }); +describe('publish command tests', () => { + let originalCliIoHostInstance: any; + + beforeEach(() => { + jest.clearAllMocks(); + originalCliIoHostInstance = CliIoHost.instance; + }); + + afterEach(() => { + CliIoHost.instance = originalCliIoHostInstance; + }); + + test('should require unstable flag', async () => { + const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); + + (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn() }; + (ioHost as any).asIoHelper = () => ioHelper; + (ioHost as any).logLevel = 'info'; + jest.spyOn(CliIoHost, 'instance').mockReturnValue(ioHost as any); + + const mockConfig = { + loadConfigFiles: jest.fn().mockResolvedValue(undefined), + settings: { + get: jest.fn().mockImplementation((key: string[]) => { + if (key[0] === 'unstable') return []; // No unstable flags set + return []; + }), + }, + context: { + get: jest.fn().mockReturnValue([]), + }, + }; + + Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); + + await expect(exec(['publish'])).rejects.toThrow(/Unstable feature use.*publish.*unstable/); + expect(publishSpy).not.toHaveBeenCalled(); + }); + + test('should call publish when unstable flag is set', async () => { + const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); + + (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn(), info: jest.fn() }; + (ioHost as any).asIoHelper = () => ioHelper; + (ioHost as any).logLevel = 'info'; + jest.spyOn(CliIoHost, 'instance').mockReturnValue(ioHost as any); + + const mockConfig = { + loadConfigFiles: jest.fn().mockResolvedValue(undefined), + settings: { + get: jest.fn().mockImplementation((key: string[]) => { + if (key[0] === 'unstable') return ['publish']; + return []; + }), + }, + context: { + get: jest.fn().mockReturnValue([]), + }, + }; + + Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); + + await exec(['publish', '--unstable=publish']); + + expect(publishSpy).toHaveBeenCalled(); + }); + + test('should pass options to publish method', async () => { + const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); + + (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn(), info: jest.fn() }; + (ioHost as any).asIoHelper = () => ioHelper; + (ioHost as any).logLevel = 'info'; + jest.spyOn(CliIoHost, 'instance').mockReturnValue(ioHost as any); + + const mockConfig = { + loadConfigFiles: jest.fn().mockResolvedValue(undefined), + settings: { + get: jest.fn().mockImplementation((key: string[]) => { + if (key[0] === 'unstable') return ['publish']; + if (key[0] === 'assetParallelism') return true; + return []; + }), + }, + context: { + get: jest.fn().mockReturnValue([]), + }, + }; + + Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); + + await exec(['publish', '--unstable=publish', '--exclusively', '--force', '--concurrency', '4']); + + expect(publishSpy).toHaveBeenCalledWith( + expect.objectContaining({ + exclusively: true, + force: true, + assetParallelism: true, + concurrency: 4, + }), + ); + }); +}); + describe('--yes', () => { test('when --yes option is provided, CliIoHost is using autoRespond', async () => { // GIVEN From a26716048c79e1ead02dcec34e96822c7a207fe1 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:41:16 +0900 Subject: [PATCH 02/33] wip for assetParallelism --- ...ublish-requires-unstable-flag.integtest.ts | 46 +++++++------------ packages/aws-cdk/lib/cli/cli.ts | 2 +- packages/aws-cdk/test/cli/cli.test.ts | 11 +++-- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts index cfb241eb0..41c67fa77 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts @@ -1,45 +1,33 @@ -import { integTest, withAws, withSpecificCdkApp } from '../../../lib'; +import { integTest, withSpecificFixture } from '../../../lib'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime integTest( 'publish command requires unstable flag', - withAws( - withSpecificCdkApp('simple-app', async (fixture) => { - // Should fail without unstable flag - await expect(fixture.cdk(['publish'])).rejects.toThrow(/unstable/); - }), - ), + withSpecificFixture('simple-app', async (fixture) => { + // Should fail without unstable flag + await expect(fixture.cdk(['publish'])).rejects.toThrow(/unstable/); + }), ); integTest( 'publish command works with unstable flag', - withAws( - withSpecificCdkApp('simple-app', async (fixture) => { - // Bootstrap first - await fixture.cdk(['bootstrap', '--unstable=publish']); + withSpecificFixture('simple-app', async (fixture) => { + // Should succeed with unstable flag (may have no assets to publish) + const output = await fixture.cdk(['publish', '--unstable=publish']); - // Should succeed with unstable flag - const output = await fixture.cdk(['publish', '--unstable=publish']); - - // Expect success message or completion - expect(output).toBeTruthy(); - }), - ), + // Expect completion without error + expect(output).toBeTruthy(); + }), ); integTest( 'publish command respects --exclusively flag', - withAws( - withSpecificCdkApp('dependency-app', async (fixture) => { - // Bootstrap first - await fixture.cdk(['bootstrap', '--unstable=publish']); - - // Publish only specific stack - const output = await fixture.cdk(['publish', 'Stack1', '--unstable=publish', '--exclusively']); + withSpecificFixture('dependency-app', async (fixture) => { + // Publish only specific stack + const output = await fixture.cdk(['publish', 'Stack1', '--unstable=publish', '--exclusively']); - // Should publish only Stack1 assets - expect(output).toBeTruthy(); - }), - ), + // Should complete without error + expect(output).toBeTruthy(); + }), ); diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 1a0cd48bc..3801633ff 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -431,7 +431,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise ({ if (args.includes('--force')) { result = { ...result, force: true }; } + if (args.includes('--asset-parallelism')) { + result = { ...result, assetParallelism: true }; + } const concurrencyIndex = args.findIndex((arg: string) => arg === '--concurrency'); if (concurrencyIndex !== -1 && args[concurrencyIndex + 1]) { result = { ...result, concurrency: parseInt(args[concurrencyIndex + 1], 10) }; @@ -604,8 +607,9 @@ describe('publish command tests', () => { settings: { get: jest.fn().mockImplementation((key: string[]) => { if (key[0] === 'unstable') return ['publish']; - if (key[0] === 'assetParallelism') return true; - return []; + // assetParallelism can come from configuration settings as fallback + if (key[0] === 'assetParallelism') return false; // Will be overridden by CLI arg + return undefined; }), }, context: { @@ -615,12 +619,13 @@ describe('publish command tests', () => { Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); - await exec(['publish', '--unstable=publish', '--exclusively', '--force', '--concurrency', '4']); + await exec(['publish', '--unstable=publish', '--exclusively', '--force', '--asset-parallelism', '--concurrency', '4']); expect(publishSpy).toHaveBeenCalledWith( expect.objectContaining({ exclusively: true, force: true, + // assetParallelism from CLI arg takes precedence over configuration.settings assetParallelism: true, concurrency: 4, }), From 4196cccbe180c5a5bdf742db8ce0c51c326a8aca Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:55:21 +0900 Subject: [PATCH 03/33] modify --- packages/aws-cdk/lib/cli/cli.ts | 2 +- packages/aws-cdk/test/cli/cli.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 3801633ff..1a0cd48bc 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -431,7 +431,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { settings: { get: jest.fn().mockImplementation((key: string[]) => { if (key[0] === 'unstable') return ['publish']; - // assetParallelism can come from configuration settings as fallback - if (key[0] === 'assetParallelism') return false; // Will be overridden by CLI arg + // configuration.settings.get() merges CLI args, cdk.json, and ~/.cdk.json + // with CLI args having highest priority + if (key[0] === 'assetParallelism') return true; // From --asset-parallelism flag return undefined; }), }, @@ -625,7 +626,7 @@ describe('publish command tests', () => { expect.objectContaining({ exclusively: true, force: true, - // assetParallelism from CLI arg takes precedence over configuration.settings + // assetParallelism from configuration.settings (which includes CLI args) assetParallelism: true, concurrency: 4, }), From 8be83e6751ab0156a76a7b69076625cc5de432b5 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:59:50 +0900 Subject: [PATCH 04/33] integ --- ...ublish-requires-unstable-flag.integtest.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts index 41c67fa77..57ff410de 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts @@ -1,33 +1,21 @@ -import { integTest, withSpecificFixture } from '../../../lib'; +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../../lib'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime integTest( - 'publish command requires unstable flag', - withSpecificFixture('simple-app', async (fixture) => { - // Should fail without unstable flag - await expect(fixture.cdk(['publish'])).rejects.toThrow(/unstable/); - }), -); + 'publish command', + withDefaultFixture(async (fixture) => { + const stackName = 'lambda'; + const fullStackName = fixture.fullStackName(stackName); -integTest( - 'publish command works with unstable flag', - withSpecificFixture('simple-app', async (fixture) => { - // Should succeed with unstable flag (may have no assets to publish) - const output = await fixture.cdk(['publish', '--unstable=publish']); + const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']); - // Expect completion without error - expect(output).toBeTruthy(); - }), -); + expect(output).toMatch('Assets published successfully'); -integTest( - 'publish command respects --exclusively flag', - withSpecificFixture('dependency-app', async (fixture) => { - // Publish only specific stack - const output = await fixture.cdk(['publish', 'Stack1', '--unstable=publish', '--exclusively']); - // Should complete without error - expect(output).toBeTruthy(); + // assert the stack wan not deployed + await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: fullStackName }))) + .rejects.toThrow(/does not exist/); }), ); From ef1e0623f3c0c8b719ffd40fd67187dfe518f2de Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:00:57 +0900 Subject: [PATCH 05/33] tweak --- .../publish/cdk-publish-requires-unstable-flag.integtest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts index 57ff410de..e571669ad 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts @@ -9,11 +9,9 @@ integTest( const stackName = 'lambda'; const fullStackName = fixture.fullStackName(stackName); - const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']); - + const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']);1 expect(output).toMatch('Assets published successfully'); - // assert the stack wan not deployed await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: fullStackName }))) .rejects.toThrow(/does not exist/); From 524032515cfe959becfe4c2de153e993d7e85e16 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:20:03 +0900 Subject: [PATCH 06/33] tweak --- packages/aws-cdk/test/cli/cli.test.ts | 35 ++------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/aws-cdk/test/cli/cli.test.ts b/packages/aws-cdk/test/cli/cli.test.ts index 5f00a4ba9..d75eed4f6 100644 --- a/packages/aws-cdk/test/cli/cli.test.ts +++ b/packages/aws-cdk/test/cli/cli.test.ts @@ -539,7 +539,7 @@ describe('publish command tests', () => { CliIoHost.instance = originalCliIoHostInstance; }); - test('should require unstable flag', async () => { + test('throw error when unstable flag is not set', async () => { const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn() }; @@ -566,34 +566,6 @@ describe('publish command tests', () => { expect(publishSpy).not.toHaveBeenCalled(); }); - test('should call publish when unstable flag is set', async () => { - const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); - - (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn(), info: jest.fn() }; - (ioHost as any).asIoHelper = () => ioHelper; - (ioHost as any).logLevel = 'info'; - jest.spyOn(CliIoHost, 'instance').mockReturnValue(ioHost as any); - - const mockConfig = { - loadConfigFiles: jest.fn().mockResolvedValue(undefined), - settings: { - get: jest.fn().mockImplementation((key: string[]) => { - if (key[0] === 'unstable') return ['publish']; - return []; - }), - }, - context: { - get: jest.fn().mockReturnValue([]), - }, - }; - - Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); - - await exec(['publish', '--unstable=publish']); - - expect(publishSpy).toHaveBeenCalled(); - }); - test('should pass options to publish method', async () => { const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); @@ -607,9 +579,7 @@ describe('publish command tests', () => { settings: { get: jest.fn().mockImplementation((key: string[]) => { if (key[0] === 'unstable') return ['publish']; - // configuration.settings.get() merges CLI args, cdk.json, and ~/.cdk.json - // with CLI args having highest priority - if (key[0] === 'assetParallelism') return true; // From --asset-parallelism flag + if (key[0] === 'assetParallelism') return true; return undefined; }), }, @@ -626,7 +596,6 @@ describe('publish command tests', () => { expect.objectContaining({ exclusively: true, force: true, - // assetParallelism from configuration.settings (which includes CLI args) assetParallelism: true, concurrency: 4, }), From 7ca3fdf9e0e59c1efb61dc595210f32b8d7fdb44 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:31:30 +0900 Subject: [PATCH 07/33] README --- packages/aws-cdk/README.md | 48 +++++++------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 8c12b097d..ef2426c97 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -578,15 +578,17 @@ For technical implementation details (function calls, file locations), see [docs ### `cdk publish` -> **Note:** This is an **unstable** feature. You must opt-in using `--unstable=publish`. +Publishes assets for the specified stack(s) without performing a deployment. -Publishes assets (such as Docker images and file assets) for the specified stack(s) to their respective destinations (ECR repositories, S3 buckets) without performing a deployment. +> [!CAUTION] +> `cdk publish` is under development and therefore must be opted in via the +> `--unstable` flag: `cdk publish --unstable=publish`. `--unstable` indicates that the scope and +> API of feature might still change. Otherwise the feature is generally production +> ready and fully supported. -This is useful in CI/CD pipelines where you want to separate the build/publish phase from the deployment phase. For example: +Publishes assets (such as Docker images and file assets) for the specified stack(s) to their respective destinations (ECR repositories, S3 buckets) without performing a deployment. -- Publish assets once in a build stage -- Deploy to multiple environments (dev, staging, prod) using the already-published assets -- Ensure all assets are available before starting deployment +This is useful in CI/CD pipelines where you want to separate the build/publish phase from the deployment phase, allowing you to publish assets once and deploy to multiple environments using the already-published assets. ```console $ # Publish assets for a single stack @@ -602,40 +604,6 @@ $ # Force re-publish even if assets already exist $ cdk publish MyStack --unstable=publish --force ``` -#### Options - -- `--all`: Publish assets for all stacks in the app -- `--exclusively` (`-e`): Only publish assets for the specified stack(s), don't include dependencies -- `--force` (`-f`): Always publish assets, even if they are already published -- `--asset-parallelism`: Whether to build/publish assets in parallel (default: true) -- `--concurrency N`: Maximum number of simultaneous asset publishing operations (default: 1) -- `--role-arn`: ARN of the IAM role to use for asset publishing - -#### Use Cases - -**CI/CD Pipeline Separation:** - -```bash -# Build stage -cdk publish --all --unstable=publish - -# Deploy stage (dev) -cdk deploy --all - -# Deploy stage (staging) - reuses already published assets -cdk deploy --all - -# Deploy stage (prod) - reuses already published assets -cdk deploy --all -``` - -**Debugging Asset Issues:** - -```bash -# Force re-publish a specific asset to debug upload issues -cdk publish MyStack --unstable=publish --force -``` - ### `cdk rollback` If a deployment performed using `cdk deploy --no-rollback` fails, your From 5fa07b2d19bd21f067565f7e51138f4b8c82e60f Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:43:28 +0900 Subject: [PATCH 08/33] fix --- .../publish/cdk-publish-requires-unstable-flag.integtest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts index e571669ad..e9fa0be7c 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/publish/cdk-publish-requires-unstable-flag.integtest.ts @@ -9,7 +9,7 @@ integTest( const stackName = 'lambda'; const fullStackName = fixture.fullStackName(stackName); - const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']);1 + const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']); expect(output).toMatch('Assets published successfully'); // assert the stack wan not deployed From 78db795a7b2d0fe985fb8af4010c2f1d5acce27a Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:56:30 +0900 Subject: [PATCH 09/33] comments --- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index c039e6379..72d7e2ee0 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -825,9 +825,9 @@ export class CdkToolkit { await this.ioHost.asIoHelper().defaults.info('Publishing assets for %s stack(s)', chalk.bold(String(stackCollection.stackCount))); const graphConcurrency: Concurrency = { - 'stack': 1, - 'asset-build': 1, - 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, + 'stack': 1, // Not relevant since we're not deploying stacks + 'asset-build': 1, // This will be CPU-bound/memory bound, mostly matters for Docker builds + 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, // This will be I/O-bound, 8 in parallel seems reasonable }; await workGraph.doParallel(graphConcurrency, { From 81fa49bcb6fe76b21987e4a5ccfbf28193f63a13 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:04:10 +0900 Subject: [PATCH 10/33] removePublishedAssets --- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 72d7e2ee0..2803ef309 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -822,6 +822,11 @@ export class CdkToolkit { true, // prebuild all assets ).build(stacksAndTheirAssetManifests); + // Unless we are running with '--force', skip already published assets + if (!options.force) { + await this.removePublishedAssets(workGraph, options); + } + await this.ioHost.asIoHelper().defaults.info('Publishing assets for %s stack(s)', chalk.bold(String(stackCollection.stackCount))); const graphConcurrency: Concurrency = { From d2abc2bc5db781a322918a7f4440473956aca41f Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 13 Jan 2026 01:02:08 +0900 Subject: [PATCH 11/33] publish.test --- .../aws-cdk/test/commands/publish.test.ts | 477 ++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 packages/aws-cdk/test/commands/publish.test.ts diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts new file mode 100644 index 000000000..80a984ea2 --- /dev/null +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -0,0 +1,477 @@ +import * as path from 'path'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; +import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; +import { CliIoHost } from '../../lib/cli/io-host'; +import { Deployments } from '../../lib/api/deployments'; +import { MockCloudExecutable, TestStackArtifact } from '../_helpers/assembly'; +import { instanceMockFrom } from '../_helpers/as-mock'; + +// Mock stacks for testing +const MOCK_STACK_WITH_ASSET: TestStackArtifact = { + stackName: 'Test-Stack-Asset', + template: { Resources: { TemplateName: 'Test-Stack-Asset' } }, + env: 'aws://123456789012/bermuda-triangle-1', + assetManifest: { + version: Manifest.version(), + files: { + xyz: { + displayName: 'Asset Display Name', + source: { + path: path.resolve(__dirname, '..', '..', 'LICENSE'), + }, + destinations: { + desto: { + bucketName: 'some-bucket', + objectKey: 'some-key', + assumeRoleArn: 'arn:aws:role', + }, + }, + }, + }, + }, + displayName: 'Test-Stack-Asset', +}; + +const MOCK_STACK_A: TestStackArtifact = { + stackName: 'Test-Stack-A', + template: { Resources: { TemplateName: 'Test-Stack-A' } }, + env: 'aws://123456789012/bermuda-triangle-1', + displayName: 'Test-Stack-A', +}; + +const MOCK_STACK_B: TestStackArtifact = { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + displayName: 'Test-Stack-B', + depends: [MOCK_STACK_WITH_ASSET.stackName], +}; + +let cloudExecutable: MockCloudExecutable; +let ioHost: CliIoHost; + +beforeEach(async () => { + jest.clearAllMocks(); + + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_WITH_ASSET], + }); + + ioHost = CliIoHost.instance(); +}); + +describe('cdk publish', () => { + test('publishes assets successfully', async () => { + // GIVEN + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, + }); + + // THEN + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + }); + + test('calls removePublishedAssets to skip already published assets when --force is not provided', async () => { + // GIVEN + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(true); // Asset already published + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, + }); + + // THEN + // removePublishedAssets checks if assets are already published + expect(mockDeployments.isSingleAssetPublished).toHaveBeenCalled(); + // Already-published assets are removed from work graph, so build/publish should not be called + expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); + }); + + test('skips removePublishedAssets and passes forcePublish flag when --force is provided', async () => { + // GIVEN + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + // Note: isSingleAssetPublished is NOT mocked because it should not be called with --force + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, + force: true, + }); + + // THEN + // With --force, removePublishedAssets is skipped, so isSingleAssetPublished should not be called + expect(mockDeployments.isSingleAssetPublished).not.toHaveBeenCalled(); + // Assets should be built and published even if already published + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + forcePublish: true, + }), + ); + }); + + test('publishes assets for multiple stacks', async () => { + // GIVEN + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_A], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: ['*'] }, + }); + + // THEN + // Should process assets from the stack that has assets + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + }); + + test('handles stacks with no assets', async () => { + // GIVEN + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_A], // Stack without assets + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_A.stackName] }, + }); + + // THEN + // Should not attempt to build or publish assets + expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); + }); + + test('respects roleArn option', async () => { + // GIVEN + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + const roleArn = 'arn:aws:iam::123456789012:role/PublishRole'; + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, + roleArn, + }); + + // THEN + expect(mockDeployments.buildSingleAsset).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ + roleArn, + }), + ); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + roleArn, + }), + ); + }); + + test('throws error when no stacks are selected', async () => { + // GIVEN + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: instanceMockFrom(Deployments), // Not used, error occurs during stack selection + }); + + // WHEN/THEN - try to publish a non-existent stack + await expect(toolkit.publish({ + selector: { patterns: ['NonExistentStack'] }, + })).rejects.toThrow('No stacks match the name(s) NonExistentStack'); + }); + + test('includes dependencies by default', async () => { + // GIVEN - Stack with dependency + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_B], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN - publish Test-Stack-B (default behavior includes dependencies) + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_B.stackName] }, + }); + + // THEN - Should include dependency (MOCK_STACK_WITH_ASSET) and publish its assets + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + }); + + test('excludes dependencies with exclusively option', async () => { + // GIVEN - Stack with dependency + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_B], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN - publish Test-Stack-B exclusively (without dependencies) + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_B.stackName] }, + exclusively: true, + }); + + // THEN - Should not include dependency, so no assets are published + expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); + }); + + test('publishes multiple assets from the same stack', async () => { + // GIVEN - Stack with multiple assets + const MOCK_STACK_MULTI_ASSET: TestStackArtifact = { + stackName: 'Test-Stack-Multi-Asset', + template: { Resources: { TemplateName: 'Test-Stack-Multi-Asset' } }, + env: 'aws://123456789012/bermuda-triangle-1', + assetManifest: { + version: Manifest.version(), + files: { + asset1: { + displayName: 'Asset 1', + source: { + path: path.resolve(__dirname, '..', '..', 'LICENSE'), + }, + destinations: { + dest1: { + bucketName: 'bucket-1', + objectKey: 'key-1', + assumeRoleArn: 'arn:aws:role', + }, + }, + }, + asset2: { + displayName: 'Asset 2', + source: { + path: path.resolve(__dirname, '..', '..', 'README.md'), + }, + destinations: { + dest2: { + bucketName: 'bucket-2', + objectKey: 'key-2', + assumeRoleArn: 'arn:aws:role', + }, + }, + }, + }, + }, + displayName: 'Test-Stack-Multi-Asset', + }; + + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_MULTI_ASSET], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_MULTI_ASSET.stackName] }, + }); + + // THEN - Should build and publish both assets + expect(mockDeployments.buildSingleAsset).toHaveBeenCalledTimes(2); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalledTimes(2); + }); + + test('publishes Docker image assets', async () => { + // GIVEN - Stack with Docker asset + const MOCK_STACK_DOCKER: TestStackArtifact = { + stackName: 'Test-Stack-Docker', + template: { Resources: { TemplateName: 'Test-Stack-Docker' } }, + env: 'aws://123456789012/bermuda-triangle-1', + assetManifest: { + version: Manifest.version(), + dockerImages: { + dockerAsset: { + displayName: 'Docker Image', + source: { + directory: path.resolve(__dirname, '..', '..'), + }, + destinations: { + dest: { + repositoryName: 'my-repo', + imageTag: 'latest', + assumeRoleArn: 'arn:aws:role', + }, + }, + }, + }, + }, + displayName: 'Test-Stack-Docker', + }; + + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_DOCKER], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN + await toolkit.publish({ + selector: { patterns: [MOCK_STACK_DOCKER.stackName] }, + }); + + // THEN - Should build and publish Docker asset + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + }); + + test('publishes all stacks with allTopLevel selector', async () => { + // GIVEN + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_A], + }); + + const mockDeployments = instanceMockFrom(Deployments); + mockDeployments.buildSingleAsset.mockResolvedValue(undefined); + mockDeployments.publishSingleAsset.mockResolvedValue(undefined); + mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockDeployments, + }); + + // WHEN - Use allTopLevel to select all stacks (equivalent to --all) + await toolkit.publish({ + selector: { patterns: [], allTopLevel: true }, + }); + + // THEN - Should process assets from all stacks + expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); + expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + }); +}); From 303f1860c9a569c7c8d7768438973fddb9fa01c0 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:33:11 +0900 Subject: [PATCH 12/33] imports --- packages/aws-cdk/test/commands/publish.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index 80a984ea2..a26858f14 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -1,10 +1,11 @@ import * as path from 'path'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; +import { Deployments } from '../../lib/api/deployments'; import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; import { CliIoHost } from '../../lib/cli/io-host'; -import { Deployments } from '../../lib/api/deployments'; -import { MockCloudExecutable, TestStackArtifact } from '../_helpers/assembly'; import { instanceMockFrom } from '../_helpers/as-mock'; +import type { TestStackArtifact } from '../_helpers/assembly'; +import { MockCloudExecutable } from '../_helpers/assembly'; // Mock stacks for testing const MOCK_STACK_WITH_ASSET: TestStackArtifact = { From e592c9e8a659edbe83071781f183d01241b9891b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:25:37 +0900 Subject: [PATCH 13/33] mock --- packages/aws-cdk/test/commands/publish.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index a26858f14..0c152f7c3 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -90,9 +90,8 @@ describe('cdk publish', () => { test('calls removePublishedAssets to skip already published assets when --force is not provided', async () => { // GIVEN const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); mockDeployments.isSingleAssetPublished.mockResolvedValue(true); // Asset already published + // Note: buildSingleAsset and publishSingleAsset are not mocked - they should not be called const toolkit = new CdkToolkit({ ioHost, @@ -187,9 +186,7 @@ describe('cdk publish', () => { }); const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + // Note: No mocks needed - methods should not be called for stacks without assets const toolkit = new CdkToolkit({ ioHost, From a0ee290a9bd71b37ff78e77deb79d7ebb88b3fca Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:28:49 +0900 Subject: [PATCH 14/33] mock --- packages/aws-cdk/test/commands/publish.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index 0c152f7c3..776b0eb7c 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -300,9 +300,7 @@ describe('cdk publish', () => { }); const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); + // Note: No mocks needed - MOCK_STACK_B has no assets, and dependencies are excluded with exclusively: true const toolkit = new CdkToolkit({ ioHost, From d2aed1f51df57d47b89f9bf142f0058e6aeb4a4b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:36:46 +0900 Subject: [PATCH 15/33] README --- packages/aws-cdk/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index c87210ad4..8b4cd1a89 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -640,9 +640,6 @@ $ cdk publish MyStack --unstable=publish $ # Publish assets for all stacks $ cdk publish --all --unstable=publish -$ # Publish with parallel asset operations -$ cdk publish MyStack --unstable=publish --asset-parallelism - $ # Force re-publish even if assets already exist $ cdk publish MyStack --unstable=publish --force ``` From 06769f2ca09eba15c382faf66fcf6646af226928 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:37:35 +0900 Subject: [PATCH 16/33] implement in toolkit-lib --- .../@aws-cdk/toolkit-lib/lib/actions/index.ts | 1 + .../toolkit-lib/lib/actions/publish/index.ts | 60 +++ .../toolkit-lib/lib/payloads/index.ts | 1 + .../toolkit-lib/lib/payloads/publish.ts | 8 + .../toolkit-lib/lib/toolkit/toolkit.ts | 100 ++++ .../toolkit-lib/test/actions/publish.test.ts | 105 ++++ packages/aws-cdk/lib/cli/cdk-toolkit.ts | 77 +-- .../aws-cdk/test/commands/publish.test.ts | 478 ++---------------- 8 files changed, 335 insertions(+), 495 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/payloads/publish.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts index b82900894..f8918d9d8 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts @@ -4,6 +4,7 @@ export * from './destroy'; export * from './diff'; export * from './drift'; export * from './list'; +export * from './publish'; export * from './refactor'; export * from './rollback'; export * from './synth'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts new file mode 100644 index 000000000..3c4921ddf --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts @@ -0,0 +1,60 @@ +import type { StackSelector } from '../../api/cloud-assembly'; + +export interface PublishOptions { + /** + * Select stacks to publish assets for + * + * @default - All stacks + */ + readonly stacks?: StackSelector; + + /** + * Always publish assets, even if they are already published + * + * @default false + */ + readonly force?: boolean; + + /** + * Whether to build/publish assets in parallel + * + * @default true + */ + readonly assetParallelism?: boolean; + + /** + * Maximum number of simultaneous asset publishing operations + * + * @default 1 + */ + readonly concurrency?: number; + + /** + * Role to pass to CloudFormation for asset operations + * + * @default - Current role + */ + readonly roleArn?: string; +} + +export interface PublishResult { + /** + * Number of stacks processed + */ + readonly stackCount: number; + + /** + * Time taken for synthesis in seconds + */ + readonly synthesisTime: number; + + /** + * Time taken for publishing in seconds + */ + readonly publishTime: number; + + /** + * Whether the operation was successful + */ + readonly success: boolean; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts b/packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts index f2428cc42..1b81263f2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts @@ -10,6 +10,7 @@ export * from './stack-activity'; export * from './synth'; export * from './types'; export * from './progress'; +export * from './publish'; export * from './refactor'; export * from './watch'; export * from './stack-details'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/payloads/publish.ts b/packages/@aws-cdk/toolkit-lib/lib/payloads/publish.ts new file mode 100644 index 000000000..0efb3e3ad --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/payloads/publish.ts @@ -0,0 +1,8 @@ +import type { PublishResult } from '../actions'; + +export interface PublishResultPayload { + /** + * The publish result + */ + readonly result: PublishResult; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 16c0bc665..1f019097a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -46,6 +46,7 @@ import type { DiffOptions } from '../actions/diff'; import { appendObject, prepareDiff } from '../actions/diff/private'; import type { DriftOptions, DriftResult } from '../actions/drift'; import { type ListOptions } from '../actions/list'; +import type { PublishOptions, PublishResult } from '../actions/publish'; import type { RefactorOptions } from '../actions/refactor'; import { type RollbackOptions } from '../actions/rollback'; import { type SynthOptions } from '../actions/synth'; @@ -488,6 +489,105 @@ export class Toolkit extends CloudAssemblySourceBuilder { return allDriftResults; } + /** + * Publish Action + * + * Publishes assets for the selected stacks without deploying + */ + public async publish(cx: ICloudAssemblySource, options: PublishOptions = {}): Promise { + const ioHelper = asIoHelper(this.ioHost, 'publish'); + const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + + const stackCollection = await assembly.selectStacksV2(selectStacks); + await this.validateStacksMetadata(stackCollection, ioHelper); + + if (stackCollection.stackCount === 0) { + await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('No stacks selected')); + return { + stackCount: 0, + synthesisTime: assembly.synthDuration.asSec, + publishTime: 0, + success: false, + }; + } + + const deployments = await this.deploymentsForAction('publish'); + const startPublishTime = Date.now(); + + const buildAsset = async (assetNode: AssetBuildNode) => { + const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ + asset: assetNode.asset, + }); + await deployments.buildSingleAsset( + assetNode.assetManifestArtifact, + assetNode.assetManifest, + assetNode.asset, + { + stack: assetNode.parentStack, + roleArn: options.roleArn, + stackName: assetNode.parentStack.stackName, + }, + ); + await buildAssetSpan.end(); + }; + + const publishAsset = async (assetNode: AssetPublishNode) => { + const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({ + asset: assetNode.asset, + }); + await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { + stack: assetNode.parentStack, + roleArn: options.roleArn, + stackName: assetNode.parentStack.stackName, + forcePublish: options.force, + }); + await publishAssetSpan.end(); + }; + + const stacks = stackCollection.stackArtifacts; + const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [ + stack, + ...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)), + ]); + + const workGraph = new WorkGraphBuilder( + ioHelper, + true, // prebuild all assets + ).build(stacksAndTheirAssetManifests); + + if (!options.force) { + await removePublishedAssetsFromWorkGraph(workGraph, deployments, options); + } + + await ioHelper.defaults.info(`Publishing assets for ${chalk.bold(String(stackCollection.stackCount))} stack(s)`); + + const graphConcurrency: Concurrency = { + 'stack': 1, + 'asset-build': 1, + 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, + }; + + await workGraph.doParallel(graphConcurrency, { + deployStack: async () => { + // No-op: we're only publishing assets, not deploying + }, + buildAsset, + publishAsset, + }); + + const publishTime = (Date.now() - startPublishTime) / 1000; + await ioHelper.defaults.info(chalk.green('\n✨ Assets published successfully')); + await ioHelper.defaults.info(`\n✨ Total time: ${formatTime(publishTime)}s\n`); + + return { + stackCount: stackCollection.stackCount, + synthesisTime: assembly.synthDuration.asSec, + publishTime, + success: true, + }; + } + /** * List Action * diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts new file mode 100644 index 000000000..38c8c38cd --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -0,0 +1,105 @@ +import * as deployments from '../../lib/api/deployments'; +import { StackSelectionStrategy } from '../../lib/api/cloud-assembly'; +import { Toolkit } from '../../lib/toolkit'; +import { builderFixture, TestIoHost } from '../_helpers'; + +let ioHost: TestIoHost; +let toolkit: Toolkit; + +beforeEach(() => { + jest.restoreAllMocks(); + ioHost = new TestIoHost('info', true); + toolkit = new Toolkit({ ioHost }); + + // Mock deployments methods for asset operations + jest.spyOn(deployments.Deployments.prototype, 'resolveEnvironment').mockResolvedValue({ + account: '11111111', + region: 'us-east-1', + name: 'aws://11111111/us-east-1', + }); + jest.spyOn(deployments.Deployments.prototype, 'isSingleAssetPublished').mockResolvedValue(false); + jest.spyOn(deployments.Deployments.prototype, 'buildSingleAsset').mockImplementation(); + jest.spyOn(deployments.Deployments.prototype, 'publishSingleAsset').mockImplementation(); +}); + +describe('publish', () => { + test('publishes assets for a single stack', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + const result = await toolkit.publish(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + }); + + // THEN + expect(result.success).toBe(true); + expect(result.stackCount).toBe(1); + expect(result.synthesisTime).toBeGreaterThan(0); + ioHost.expectMessage({ containing: 'Publishing assets for 1 stack(s)', level: 'info' }); + ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + }); + + test('publishes assets for multiple stacks', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'two-different-stacks'); + const result = await toolkit.publish(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + }); + + // THEN + expect(result.success).toBe(true); + expect(result.stackCount).toBeGreaterThanOrEqual(1); + ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + }); + + test('returns error when no stacks are selected', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + const result = await toolkit.publish(cx, { + stacks: { patterns: ['NonExistentStack'], strategy: StackSelectionStrategy.PATTERN_MATCH }, + }); + + // THEN + expect(result.success).toBe(false); + expect(result.stackCount).toBe(0); + ioHost.expectMessage({ containing: 'No stacks selected', level: 'error' }); + }); + + test('can invoke publish action without options', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-asset'); + const result = await toolkit.publish(cx); + + // THEN + expect(result.success).toBe(true); + ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + }); + + test('respects force option', async () => { + // WHEN - publish with force + const cx = await builderFixture(toolkit, 'stack-with-asset'); + const result = await toolkit.publish(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + force: true, + }); + + // THEN + expect(result.success).toBe(true); + ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + }); + + test('skips already published assets when force is false', async () => { + // Mock that asset is already published + jest.spyOn(deployments.Deployments.prototype, 'isSingleAssetPublished').mockResolvedValue(true); + + // WHEN - publish without force + const cx = await builderFixture(toolkit, 'stack-with-asset'); + const result = await toolkit.publish(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + force: false, + }); + + // THEN + expect(result.success).toBe(true); + ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + }); +}); diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index bc55fbe7f..d3c3f1414 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -791,77 +791,16 @@ export class CdkToolkit { } public async publish(options: PublishOptions) { - const startSynthTime = new Date().getTime(); - const stackCollection = await this.selectStacksForDeploy( - options.selector, - options.exclusively, - ); - const elapsedSynthTime = new Date().getTime() - startSynthTime; - await this.ioHost.asIoHelper().defaults.info(`\n✨ Synthesis time: ${formatTime(elapsedSynthTime)}s\n`); - - if (stackCollection.stackCount === 0) { - await this.ioHost.asIoHelper().defaults.error('No stacks selected'); - return; - } - - const buildAsset = async (assetNode: AssetBuildNode) => { - await this.props.deployments.buildSingleAsset( - assetNode.assetManifestArtifact, - assetNode.assetManifest, - assetNode.asset, - { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - }, - ); - }; - - const publishAsset = async (assetNode: AssetPublishNode) => { - await this.props.deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - forcePublish: options.force, - }); - }; - - const startPublishTime = new Date().getTime(); - const stacks = stackCollection.stackArtifacts; - const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [ - stack, - ...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)), - ]); - - const workGraph = new WorkGraphBuilder( - asIoHelper(this.ioHost, 'publish'), - true, // prebuild all assets - ).build(stacksAndTheirAssetManifests); - - // Unless we are running with '--force', skip already published assets - if (!options.force) { - await this.removePublishedAssets(workGraph, options); - } - - await this.ioHost.asIoHelper().defaults.info('Publishing assets for %s stack(s)', chalk.bold(String(stackCollection.stackCount))); - - const graphConcurrency: Concurrency = { - 'stack': 1, // Not relevant since we're not deploying stacks - 'asset-build': 1, // This will be CPU-bound/memory bound, mostly matters for Docker builds - 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, // This will be I/O-bound, 8 in parallel seems reasonable - }; - - await workGraph.doParallel(graphConcurrency, { - deployStack: async () => { - // No-op: we're only publishing assets, not deploying + await this.toolkit.publish(this.props.cloudExecutable, { + stacks: { + patterns: options.selector.patterns, + strategy: options.selector.patterns.length > 0 ? StackSelectionStrategy.PATTERN_MATCH : StackSelectionStrategy.ALL_STACKS, }, - buildAsset, - publishAsset, + force: options.force, + assetParallelism: options.assetParallelism, + concurrency: options.concurrency, + roleArn: options.roleArn, }); - - const elapsedPublishTime = new Date().getTime() - startPublishTime; - await this.ioHost.asIoHelper().defaults.info(chalk.green('\n✨ Assets published successfully')); - await this.ioHost.asIoHelper().defaults.info(`\n✨ Total time: ${formatTime(elapsedPublishTime)}s\n`); } public async watch(options: WatchOptions) { diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index 776b0eb7c..3080f0fe4 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -1,473 +1,99 @@ -import * as path from 'path'; -import { Manifest } from '@aws-cdk/cloud-assembly-schema'; +import type { PublishResult } from '@aws-cdk/toolkit-lib'; import { Deployments } from '../../lib/api/deployments'; import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; import { CliIoHost } from '../../lib/cli/io-host'; -import { instanceMockFrom } from '../_helpers/as-mock'; -import type { TestStackArtifact } from '../_helpers/assembly'; -import { MockCloudExecutable } from '../_helpers/assembly'; +import { instanceMockFrom, MockCloudExecutable } from '../_helpers'; -// Mock stacks for testing -const MOCK_STACK_WITH_ASSET: TestStackArtifact = { - stackName: 'Test-Stack-Asset', - template: { Resources: { TemplateName: 'Test-Stack-Asset' } }, - env: 'aws://123456789012/bermuda-triangle-1', - assetManifest: { - version: Manifest.version(), - files: { - xyz: { - displayName: 'Asset Display Name', - source: { - path: path.resolve(__dirname, '..', '..', 'LICENSE'), - }, - destinations: { - desto: { - bucketName: 'some-bucket', - objectKey: 'some-key', - assumeRoleArn: 'arn:aws:role', +describe('publish', () => { + let cloudExecutable: MockCloudExecutable; + let cloudFormation: jest.Mocked; + let toolkit: CdkToolkit; + let ioHost = CliIoHost.instance(); + + beforeEach(async () => { + cloudExecutable = await MockCloudExecutable.create({ + stacks: [ + { + stackName: 'Stack1', + template: { + Resources: { + Bucket: { Type: 'AWS::S3::Bucket' }, + }, }, }, - }, - }, - }, - displayName: 'Test-Stack-Asset', -}; - -const MOCK_STACK_A: TestStackArtifact = { - stackName: 'Test-Stack-A', - template: { Resources: { TemplateName: 'Test-Stack-A' } }, - env: 'aws://123456789012/bermuda-triangle-1', - displayName: 'Test-Stack-A', -}; - -const MOCK_STACK_B: TestStackArtifact = { - stackName: 'Test-Stack-B', - template: { Resources: { TemplateName: 'Test-Stack-B' } }, - env: 'aws://123456789012/bermuda-triangle-1', - displayName: 'Test-Stack-B', - depends: [MOCK_STACK_WITH_ASSET.stackName], -}; + ], + }, undefined, ioHost); -let cloudExecutable: MockCloudExecutable; -let ioHost: CliIoHost; - -beforeEach(async () => { - jest.clearAllMocks(); - - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_WITH_ASSET], - }); - - ioHost = CliIoHost.instance(); -}); + cloudFormation = instanceMockFrom(Deployments); -describe('cdk publish', () => { - test('publishes assets successfully', async () => { - // GIVEN - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, + toolkit = new CdkToolkit({ cloudExecutable, + deployments: cloudFormation, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, }); - // THEN - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); + // Mock the toolkit.publish method from toolkit-lib + jest.spyOn((toolkit as any).toolkit, 'publish').mockResolvedValue({ + stackCount: 1, + synthesisTime: 0.5, + publishTime: 1.2, + success: true, + } satisfies PublishResult); }); - test('calls removePublishedAssets to skip already published assets when --force is not provided', async () => { - // GIVEN - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.isSingleAssetPublished.mockResolvedValue(true); // Asset already published - // Note: buildSingleAsset and publishSingleAsset are not mocked - they should not be called - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, - }); - - // THEN - // removePublishedAssets checks if assets are already published - expect(mockDeployments.isSingleAssetPublished).toHaveBeenCalled(); - // Already-published assets are removed from work graph, so build/publish should not be called - expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); + afterEach(() => { + jest.restoreAllMocks(); }); - test('skips removePublishedAssets and passes forcePublish flag when --force is provided', async () => { - // GIVEN - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - // Note: isSingleAssetPublished is NOT mocked because it should not be called with --force - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - + test('publishes with correct stack selector and force option', async () => { // WHEN await toolkit.publish({ - selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, + selector: { patterns: ['Stack1'] }, force: true, }); // THEN - // With --force, removePublishedAssets is skipped, so isSingleAssetPublished should not be called - expect(mockDeployments.isSingleAssetPublished).not.toHaveBeenCalled(); - // Assets should be built and published even if already published - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalledWith( - expect.anything(), + expect((toolkit as any).toolkit.publish).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - forcePublish: true, + stacks: expect.objectContaining({ + patterns: ['Stack1'], + }), + force: true, }), ); }); - test('publishes assets for multiple stacks', async () => { - // GIVEN - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_A], - }); - - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - await toolkit.publish({ - selector: { patterns: ['*'] }, - }); - - // THEN - // Should process assets from the stack that has assets - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); - }); - - test('handles stacks with no assets', async () => { - // GIVEN - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_A], // Stack without assets - }); - - const mockDeployments = instanceMockFrom(Deployments); - // Note: No mocks needed - methods should not be called for stacks without assets - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - + test('publishes successfully', async () => { // WHEN await toolkit.publish({ - selector: { patterns: [MOCK_STACK_A.stackName] }, + selector: { patterns: ['Stack1'] }, }); // THEN - // Should not attempt to build or publish assets - expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); + expect((toolkit as any).toolkit.publish).toHaveBeenCalled(); }); - test('respects roleArn option', async () => { - // GIVEN - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - const roleArn = 'arn:aws:iam::123456789012:role/PublishRole'; - + test('passes all options correctly', async () => { // WHEN await toolkit.publish({ - selector: { patterns: [MOCK_STACK_WITH_ASSET.stackName] }, - roleArn, + selector: { patterns: ['Stack1'] }, + force: true, + assetParallelism: false, + concurrency: 5, + roleArn: 'arn:aws:iam::123456789012:role/TestRole', }); // THEN - expect(mockDeployments.buildSingleAsset).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), + expect((toolkit as any).toolkit.publish).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - roleArn, + force: true, + assetParallelism: false, + concurrency: 5, + roleArn: 'arn:aws:iam::123456789012:role/TestRole', }), ); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - roleArn, - }), - ); - }); - - test('throws error when no stacks are selected', async () => { - // GIVEN - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: instanceMockFrom(Deployments), // Not used, error occurs during stack selection - }); - - // WHEN/THEN - try to publish a non-existent stack - await expect(toolkit.publish({ - selector: { patterns: ['NonExistentStack'] }, - })).rejects.toThrow('No stacks match the name(s) NonExistentStack'); - }); - - test('includes dependencies by default', async () => { - // GIVEN - Stack with dependency - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_B], - }); - - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - publish Test-Stack-B (default behavior includes dependencies) - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_B.stackName] }, - }); - - // THEN - Should include dependency (MOCK_STACK_WITH_ASSET) and publish its assets - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); - }); - - test('excludes dependencies with exclusively option', async () => { - // GIVEN - Stack with dependency - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_B], - }); - - const mockDeployments = instanceMockFrom(Deployments); - // Note: No mocks needed - MOCK_STACK_B has no assets, and dependencies are excluded with exclusively: true - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - publish Test-Stack-B exclusively (without dependencies) - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_B.stackName] }, - exclusively: true, - }); - - // THEN - Should not include dependency, so no assets are published - expect(mockDeployments.buildSingleAsset).not.toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).not.toHaveBeenCalled(); - }); - - test('publishes multiple assets from the same stack', async () => { - // GIVEN - Stack with multiple assets - const MOCK_STACK_MULTI_ASSET: TestStackArtifact = { - stackName: 'Test-Stack-Multi-Asset', - template: { Resources: { TemplateName: 'Test-Stack-Multi-Asset' } }, - env: 'aws://123456789012/bermuda-triangle-1', - assetManifest: { - version: Manifest.version(), - files: { - asset1: { - displayName: 'Asset 1', - source: { - path: path.resolve(__dirname, '..', '..', 'LICENSE'), - }, - destinations: { - dest1: { - bucketName: 'bucket-1', - objectKey: 'key-1', - assumeRoleArn: 'arn:aws:role', - }, - }, - }, - asset2: { - displayName: 'Asset 2', - source: { - path: path.resolve(__dirname, '..', '..', 'README.md'), - }, - destinations: { - dest2: { - bucketName: 'bucket-2', - objectKey: 'key-2', - assumeRoleArn: 'arn:aws:role', - }, - }, - }, - }, - }, - displayName: 'Test-Stack-Multi-Asset', - }; - - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_MULTI_ASSET], - }); - - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_MULTI_ASSET.stackName] }, - }); - - // THEN - Should build and publish both assets - expect(mockDeployments.buildSingleAsset).toHaveBeenCalledTimes(2); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalledTimes(2); - }); - - test('publishes Docker image assets', async () => { - // GIVEN - Stack with Docker asset - const MOCK_STACK_DOCKER: TestStackArtifact = { - stackName: 'Test-Stack-Docker', - template: { Resources: { TemplateName: 'Test-Stack-Docker' } }, - env: 'aws://123456789012/bermuda-triangle-1', - assetManifest: { - version: Manifest.version(), - dockerImages: { - dockerAsset: { - displayName: 'Docker Image', - source: { - directory: path.resolve(__dirname, '..', '..'), - }, - destinations: { - dest: { - repositoryName: 'my-repo', - imageTag: 'latest', - assumeRoleArn: 'arn:aws:role', - }, - }, - }, - }, - }, - displayName: 'Test-Stack-Docker', - }; - - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_DOCKER], - }); - - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - await toolkit.publish({ - selector: { patterns: [MOCK_STACK_DOCKER.stackName] }, - }); - - // THEN - Should build and publish Docker asset - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); - }); - - test('publishes all stacks with allTopLevel selector', async () => { - // GIVEN - cloudExecutable = await MockCloudExecutable.create({ - stacks: [MOCK_STACK_WITH_ASSET, MOCK_STACK_A], - }); - - const mockDeployments = instanceMockFrom(Deployments); - mockDeployments.buildSingleAsset.mockResolvedValue(undefined); - mockDeployments.publishSingleAsset.mockResolvedValue(undefined); - mockDeployments.isSingleAssetPublished.mockResolvedValue(false); - - const toolkit = new CdkToolkit({ - ioHost, - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockDeployments, - }); - - // WHEN - Use allTopLevel to select all stacks (equivalent to --all) - await toolkit.publish({ - selector: { patterns: [], allTopLevel: true }, - }); - - // THEN - Should process assets from all stacks - expect(mockDeployments.buildSingleAsset).toHaveBeenCalled(); - expect(mockDeployments.publishSingleAsset).toHaveBeenCalled(); }); }); From 170fc97479455712d336f5abed83df73a1cc3d17 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:59:49 +0900 Subject: [PATCH 17/33] rm success --- .../toolkit-lib/lib/actions/publish/index.ts | 5 ----- .../@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 -- .../toolkit-lib/test/actions/publish.test.ts | 13 +++++-------- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 2 +- packages/aws-cdk/test/cli/cli.test.ts | 4 ++-- packages/aws-cdk/test/commands/publish.test.ts | 8 +------- 6 files changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts index 3c4921ddf..082f75294 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts @@ -52,9 +52,4 @@ export interface PublishResult { * Time taken for publishing in seconds */ readonly publishTime: number; - - /** - * Whether the operation was successful - */ - readonly success: boolean; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 1f019097a..bb17bf050 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -508,7 +508,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { stackCount: 0, synthesisTime: assembly.synthDuration.asSec, publishTime: 0, - success: false, }; } @@ -584,7 +583,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { stackCount: stackCollection.stackCount, synthesisTime: assembly.synthDuration.asSec, publishTime, - success: true, }; } diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts index 38c8c38cd..5b07a32c9 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -1,5 +1,5 @@ -import * as deployments from '../../lib/api/deployments'; import { StackSelectionStrategy } from '../../lib/api/cloud-assembly'; +import * as deployments from '../../lib/api/deployments'; import { Toolkit } from '../../lib/toolkit'; import { builderFixture, TestIoHost } from '../_helpers'; @@ -31,7 +31,6 @@ describe('publish', () => { }); // THEN - expect(result.success).toBe(true); expect(result.stackCount).toBe(1); expect(result.synthesisTime).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Publishing assets for 1 stack(s)', level: 'info' }); @@ -46,12 +45,11 @@ describe('publish', () => { }); // THEN - expect(result.success).toBe(true); expect(result.stackCount).toBeGreaterThanOrEqual(1); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); - test('returns error when no stacks are selected', async () => { + test('returns result when no stacks are selected', async () => { // WHEN const cx = await builderFixture(toolkit, 'stack-with-asset'); const result = await toolkit.publish(cx, { @@ -59,7 +57,6 @@ describe('publish', () => { }); // THEN - expect(result.success).toBe(false); expect(result.stackCount).toBe(0); ioHost.expectMessage({ containing: 'No stacks selected', level: 'error' }); }); @@ -70,7 +67,7 @@ describe('publish', () => { const result = await toolkit.publish(cx); // THEN - expect(result.success).toBe(true); + expect(result.stackCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -83,7 +80,7 @@ describe('publish', () => { }); // THEN - expect(result.success).toBe(true); + expect(result.stackCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -99,7 +96,7 @@ describe('publish', () => { }); // THEN - expect(result.success).toBe(true); + expect(result.stackCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); }); diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index d3c3f1414..2a18549f5 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -790,7 +790,7 @@ export class CdkToolkit { } } - public async publish(options: PublishOptions) { + public async publish(options: PublishOptions): Promise { await this.toolkit.publish(this.props.cloudExecutable, { stacks: { patterns: options.selector.patterns, diff --git a/packages/aws-cdk/test/cli/cli.test.ts b/packages/aws-cdk/test/cli/cli.test.ts index dca3f2e3f..095ef5d71 100644 --- a/packages/aws-cdk/test/cli/cli.test.ts +++ b/packages/aws-cdk/test/cli/cli.test.ts @@ -548,7 +548,7 @@ describe('publish command tests', () => { }); test('throw error when unstable flag is not set', async () => { - const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); + const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(undefined); (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn() }; (ioHost as any).asIoHelper = () => ioHelper; @@ -575,7 +575,7 @@ describe('publish command tests', () => { }); test('should pass options to publish method', async () => { - const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(); + const publishSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'publish').mockResolvedValue(undefined); (ioHost as any).defaults = { warn: jest.fn(), debug: jest.fn(), result: jest.fn(), info: jest.fn() }; (ioHost as any).asIoHelper = () => ioHelper; diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index 3080f0fe4..5edbcc04b 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -1,4 +1,3 @@ -import type { PublishResult } from '@aws-cdk/toolkit-lib'; import { Deployments } from '../../lib/api/deployments'; import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; import { CliIoHost } from '../../lib/cli/io-host'; @@ -34,12 +33,7 @@ describe('publish', () => { }); // Mock the toolkit.publish method from toolkit-lib - jest.spyOn((toolkit as any).toolkit, 'publish').mockResolvedValue({ - stackCount: 1, - synthesisTime: 0.5, - publishTime: 1.2, - success: true, - } satisfies PublishResult); + jest.spyOn((toolkit as any).toolkit, 'publish').mockResolvedValue(undefined); }); afterEach(() => { From c039eb699777263f6a31c533bb8992d5cf2326da Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:13:17 +0900 Subject: [PATCH 18/33] exclusively --- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 2a18549f5..12b9e89ea 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -19,6 +19,7 @@ import { CloudWatchLogEventMonitor, DEFAULT_TOOLKIT_STACK_NAME, DiffFormatter, + ExpandStackSelection, findCloudWatchLogGroups, GarbageCollector, removeNonImportResources, @@ -795,6 +796,7 @@ export class CdkToolkit { stacks: { patterns: options.selector.patterns, strategy: options.selector.patterns.length > 0 ? StackSelectionStrategy.PATTERN_MATCH : StackSelectionStrategy.ALL_STACKS, + expand: options.exclusively ? ExpandStackSelection.NONE : ExpandStackSelection.UPSTREAM, }, force: options.force, assetParallelism: options.assetParallelism, From 9ac288c4b9f08a8b88b1767c40d3581d6e930b95 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:40:21 +0900 Subject: [PATCH 19/33] rm toolkitStackName --- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 7 ------- packages/aws-cdk/lib/cli/cli.ts | 1 - 2 files changed, 8 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 12b9e89ea..57b8b16a0 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -1884,13 +1884,6 @@ export interface PublishOptions { */ readonly exclusively?: boolean; - /** - * Name of the toolkit stack to use/deploy - * - * @default CDKToolkit - */ - readonly toolkitStackName?: string; - /** * Role to pass to CloudFormation for deployment * diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 15f200a7f..82551bf34 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -436,7 +436,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Date: Mon, 23 Feb 2026 02:03:34 +0900 Subject: [PATCH 20/33] use concurrency --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index bb17bf050..ae05097a5 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -564,7 +564,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { const graphConcurrency: Concurrency = { 'stack': 1, 'asset-build': 1, - 'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, + 'asset-publish': (options.assetParallelism ?? true) ? (options.concurrency ?? 8) : 1, }; await workGraph.doParallel(graphConcurrency, { From 198eeeff8ff6e74392f13f5346f256b35f68e2b4 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:24:58 +0900 Subject: [PATCH 21/33] response --- .../toolkit-lib/lib/actions/publish/index.ts | 14 ++------------ .../@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 10 ++++------ .../toolkit-lib/test/actions/publish.test.ts | 13 ++++++------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts index 082f75294..6545dafc7 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts @@ -39,17 +39,7 @@ export interface PublishOptions { export interface PublishResult { /** - * Number of stacks processed + * Number of assets published */ - readonly stackCount: number; - - /** - * Time taken for synthesis in seconds - */ - readonly synthesisTime: number; - - /** - * Time taken for publishing in seconds - */ - readonly publishTime: number; + readonly publishedAssetCount: number; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index ae05097a5..59ef4cec4 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -505,9 +505,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('No stacks selected')); return { - stackCount: 0, - synthesisTime: assembly.synthDuration.asSec, - publishTime: 0, + publishedAssetCount: 0, }; } @@ -559,6 +557,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { await removePublishedAssetsFromWorkGraph(workGraph, deployments, options); } + const publishedAssetCount = Object.values(workGraph.nodes).filter(n => n.type === 'asset-publish').length; + await ioHelper.defaults.info(`Publishing assets for ${chalk.bold(String(stackCollection.stackCount))} stack(s)`); const graphConcurrency: Concurrency = { @@ -580,9 +580,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { await ioHelper.defaults.info(`\n✨ Total time: ${formatTime(publishTime)}s\n`); return { - stackCount: stackCollection.stackCount, - synthesisTime: assembly.synthDuration.asSec, - publishTime, + publishedAssetCount, }; } diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts index 5b07a32c9..7bd458068 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -31,8 +31,7 @@ describe('publish', () => { }); // THEN - expect(result.stackCount).toBe(1); - expect(result.synthesisTime).toBeGreaterThan(0); + expect(result.publishedAssetCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Publishing assets for 1 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -45,7 +44,7 @@ describe('publish', () => { }); // THEN - expect(result.stackCount).toBeGreaterThanOrEqual(1); + expect(result.publishedAssetCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -57,7 +56,7 @@ describe('publish', () => { }); // THEN - expect(result.stackCount).toBe(0); + expect(result.publishedAssetCount).toBe(0); ioHost.expectMessage({ containing: 'No stacks selected', level: 'error' }); }); @@ -67,7 +66,7 @@ describe('publish', () => { const result = await toolkit.publish(cx); // THEN - expect(result.stackCount).toBeGreaterThan(0); + expect(result.publishedAssetCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -80,7 +79,7 @@ describe('publish', () => { }); // THEN - expect(result.stackCount).toBeGreaterThan(0); + expect(result.publishedAssetCount).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -96,7 +95,7 @@ describe('publish', () => { }); // THEN - expect(result.stackCount).toBeGreaterThan(0); + expect(result.publishedAssetCount).toBe(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); }); From 678377f24be471a0a739cc9b9c8cd56b35102504 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:27:24 +0900 Subject: [PATCH 22/33] test --- packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts index 7bd458068..8fea8d793 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -45,6 +45,7 @@ describe('publish', () => { // THEN expect(result.publishedAssetCount).toBeGreaterThan(0); + ioHost.expectMessage({ containing: 'Publishing assets for 2 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); From 31b4858b9f4f30cce3531d03820d6b2031cc24ee Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:36:07 +0900 Subject: [PATCH 23/33] publishedAssets --- .../toolkit-lib/lib/actions/publish/index.ts | 5 +++-- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 9 +++++---- .../toolkit-lib/test/actions/publish.test.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts index 6545dafc7..903ee2e65 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts @@ -1,3 +1,4 @@ +import type { IManifestEntry } from '@aws-cdk/cdk-assets-lib'; import type { StackSelector } from '../../api/cloud-assembly'; export interface PublishOptions { @@ -39,7 +40,7 @@ export interface PublishOptions { export interface PublishResult { /** - * Number of assets published + * List of assets that were published */ - readonly publishedAssetCount: number; + readonly publishedAssets: IManifestEntry[]; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 59ef4cec4..95d34e2a8 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -97,6 +97,7 @@ import { formatErrorMessage, formatTime, obscureTemplate, serializeStructure, va import { pLimit } from '../util/concurrency'; import { createIgnoreMatcher } from '../util/glob-matcher'; import { promiseWithResolvers } from '../util/promises'; +import type { IManifestEntry } from '@aws-cdk/cdk-assets-lib'; export interface ToolkitOptions { /** @@ -505,12 +506,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('No stacks selected')); return { - publishedAssetCount: 0, + publishedAssets: [], }; } const deployments = await this.deploymentsForAction('publish'); const startPublishTime = Date.now(); + const publishedAssets: IManifestEntry[] = []; const buildAsset = async (assetNode: AssetBuildNode) => { const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ @@ -540,6 +542,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { forcePublish: options.force, }); await publishAssetSpan.end(); + publishedAssets.push(assetNode.asset); }; const stacks = stackCollection.stackArtifacts; @@ -557,8 +560,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { await removePublishedAssetsFromWorkGraph(workGraph, deployments, options); } - const publishedAssetCount = Object.values(workGraph.nodes).filter(n => n.type === 'asset-publish').length; - await ioHelper.defaults.info(`Publishing assets for ${chalk.bold(String(stackCollection.stackCount))} stack(s)`); const graphConcurrency: Concurrency = { @@ -580,7 +581,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { await ioHelper.defaults.info(`\n✨ Total time: ${formatTime(publishTime)}s\n`); return { - publishedAssetCount, + publishedAssets, }; } diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts index 8fea8d793..61b1a66c4 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -31,7 +31,7 @@ describe('publish', () => { }); // THEN - expect(result.publishedAssetCount).toBeGreaterThan(0); + expect(result.publishedAssets.length).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Publishing assets for 1 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -44,7 +44,7 @@ describe('publish', () => { }); // THEN - expect(result.publishedAssetCount).toBeGreaterThan(0); + expect(result.publishedAssets.length).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Publishing assets for 2 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -57,7 +57,7 @@ describe('publish', () => { }); // THEN - expect(result.publishedAssetCount).toBe(0); + expect(result.publishedAssets.length).toBe(0); ioHost.expectMessage({ containing: 'No stacks selected', level: 'error' }); }); @@ -67,7 +67,7 @@ describe('publish', () => { const result = await toolkit.publish(cx); // THEN - expect(result.publishedAssetCount).toBeGreaterThan(0); + expect(result.publishedAssets.length).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -80,7 +80,7 @@ describe('publish', () => { }); // THEN - expect(result.publishedAssetCount).toBeGreaterThan(0); + expect(result.publishedAssets.length).toBeGreaterThan(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -96,7 +96,7 @@ describe('publish', () => { }); // THEN - expect(result.publishedAssetCount).toBe(0); + expect(result.publishedAssets.length).toBe(0); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); }); From 4d8e80f8dbefde2977b643d1380cbe3c8a35b1e3 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:38:07 +0900 Subject: [PATCH 24/33] improve --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 95d34e2a8..df93e0c73 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -512,7 +512,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { const deployments = await this.deploymentsForAction('publish'); const startPublishTime = Date.now(); - const publishedAssets: IManifestEntry[] = []; const buildAsset = async (assetNode: AssetBuildNode) => { const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ @@ -542,7 +541,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { forcePublish: options.force, }); await publishAssetSpan.end(); - publishedAssets.push(assetNode.asset); }; const stacks = stackCollection.stackArtifacts; @@ -576,6 +574,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { publishAsset, }); + const publishedAssets = Object.values(workGraph.nodes) + .filter((n): n is AssetPublishNode => n.type === 'asset-publish') + .map(n => n.asset); + const publishTime = (Date.now() - startPublishTime) / 1000; await ioHelper.defaults.info(chalk.green('\n✨ Assets published successfully')); await ioHelper.defaults.info(`\n✨ Total time: ${formatTime(publishTime)}s\n`); From 5a10ccfb6aa23298c6e423b72a698004f141da13 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:39:44 +0900 Subject: [PATCH 25/33] fix --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index df93e0c73..ba7689008 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -97,7 +97,6 @@ import { formatErrorMessage, formatTime, obscureTemplate, serializeStructure, va import { pLimit } from '../util/concurrency'; import { createIgnoreMatcher } from '../util/glob-matcher'; import { promiseWithResolvers } from '../util/promises'; -import type { IManifestEntry } from '@aws-cdk/cdk-assets-lib'; export interface ToolkitOptions { /** From c19903172d9994ce43ad4e7d042c0af6eb8d47f1 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:22:20 +0900 Subject: [PATCH 26/33] modify --- .../toolkit-lib/lib/toolkit/toolkit.ts | 19 +++++++++++-------- .../toolkit-lib/test/actions/publish.test.ts | 4 +--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index ba7689008..5c0ba17fe 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -557,7 +557,15 @@ export class Toolkit extends CloudAssemblySourceBuilder { await removePublishedAssetsFromWorkGraph(workGraph, deployments, options); } - await ioHelper.defaults.info(`Publishing assets for ${chalk.bold(String(stackCollection.stackCount))} stack(s)`); + const assetNodes = Object.values(workGraph.nodes) + .filter((n): n is AssetPublishNode => n.type === 'asset-publish'); + + if (assetNodes.length === 0) { + await ioHelper.defaults.info(chalk.green('\n✨ All assets are already published\n')); + return { + publishedAssets: [], + }; + } const graphConcurrency: Concurrency = { 'stack': 1, @@ -573,13 +581,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { publishAsset, }); - const publishedAssets = Object.values(workGraph.nodes) - .filter((n): n is AssetPublishNode => n.type === 'asset-publish') - .map(n => n.asset); - - const publishTime = (Date.now() - startPublishTime) / 1000; - await ioHelper.defaults.info(chalk.green('\n✨ Assets published successfully')); - await ioHelper.defaults.info(`\n✨ Total time: ${formatTime(publishTime)}s\n`); + await ioHelper.defaults.info(chalk.green('\n✨ Assets published successfully\n')); + const publishedAssets = assetNodes.map(n => n.asset); return { publishedAssets, diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts index 61b1a66c4..3f69f2868 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts @@ -32,7 +32,6 @@ describe('publish', () => { // THEN expect(result.publishedAssets.length).toBeGreaterThan(0); - ioHost.expectMessage({ containing: 'Publishing assets for 1 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -45,7 +44,6 @@ describe('publish', () => { // THEN expect(result.publishedAssets.length).toBeGreaterThan(0); - ioHost.expectMessage({ containing: 'Publishing assets for 2 stack(s)', level: 'info' }); ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); }); @@ -97,6 +95,6 @@ describe('publish', () => { // THEN expect(result.publishedAssets.length).toBe(0); - ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' }); + ioHost.expectMessage({ containing: 'All assets are already published', level: 'info' }); }); }); From 51b4b1d85d3866afa4f885fbfe0ab7274a3d21ff Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:32:06 +0900 Subject: [PATCH 27/33] rm --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 5c0ba17fe..0a15099d5 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -510,7 +510,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { } const deployments = await this.deploymentsForAction('publish'); - const startPublishTime = Date.now(); const buildAsset = async (assetNode: AssetBuildNode) => { const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ From 2b4ce75cf6515469da70d965ec8b3a08ffdc415a Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:28:42 +0900 Subject: [PATCH 28/33] forgot to build --- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 14 +++++++------- packages/aws-cdk/lib/cli/cli-config.ts | 1 - packages/aws-cdk/lib/cli/cli-type-registry.json | 5 ----- packages/aws-cdk/lib/cli/convert-to-user-input.ts | 2 -- .../lib/cli/parse-command-line-arguments.ts | 6 ------ packages/aws-cdk/lib/cli/user-input.ts | 7 ------- 6 files changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 57b8b16a0..70eb6cfd7 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -1884,13 +1884,6 @@ export interface PublishOptions { */ readonly exclusively?: boolean; - /** - * Role to pass to CloudFormation for deployment - * - * @default - Current role - */ - readonly roleArn?: string; - /** * Always publish assets, even if they are already published * @@ -1911,6 +1904,13 @@ export interface PublishOptions { * @default 1 */ readonly concurrency?: number; + + /** + * Role to pass to CloudFormation for deployment + * + * @default - Current role + */ + readonly roleArn?: string; } export interface ImportOptions extends CfnDeployOptions { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index c79ce9b17..5443c598c 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -253,7 +253,6 @@ export async function makeConfig(): Promise { options: { 'all': { type: 'boolean', desc: 'Publish assets for all available stacks', default: false }, 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only publish assets for requested stacks, don\'t include dependencies' }, - 'toolkit-stack-name': { type: 'string', desc: 'The name of the existing CDK toolkit stack', requiresArg: true }, 'force': { type: 'boolean', alias: 'f', desc: 'Always publish assets, even if they are already published', default: false }, 'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }, 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', default: 1, requiresArg: true }, diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 95afb86db..395059251 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -618,11 +618,6 @@ "alias": "e", "desc": "Only publish assets for requested stacks, don't include dependencies" }, - "toolkit-stack-name": { - "type": "string", - "desc": "The name of the existing CDK toolkit stack", - "requiresArg": true - }, "force": { "type": "boolean", "alias": "f", diff --git a/packages/aws-cdk/lib/cli/convert-to-user-input.ts b/packages/aws-cdk/lib/cli/convert-to-user-input.ts index af7dee592..d6ec1ba8e 100644 --- a/packages/aws-cdk/lib/cli/convert-to-user-input.ts +++ b/packages/aws-cdk/lib/cli/convert-to-user-input.ts @@ -162,7 +162,6 @@ export function convertYargsToUserInput(args: any): UserInput { commandOptions = { all: args.all, exclusively: args.exclusively, - toolkitStackName: args.toolkitStackName, force: args.force, assetParallelism: args.assetParallelism, concurrency: args.concurrency, @@ -454,7 +453,6 @@ export function convertConfigToUserInput(config: any): UserInput { const publishOptions = { all: config.publish?.all, exclusively: config.publish?.exclusively, - toolkitStackName: config.publish?.toolkitStackName, force: config.publish?.force, assetParallelism: config.publish?.assetParallelism, concurrency: config.publish?.concurrency, diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 4a6da2ca3..9e4232bb9 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -645,12 +645,6 @@ export function parseCommandLineArguments(args: Array): any { alias: 'e', desc: "Only publish assets for requested stacks, don't include dependencies", }) - .option('toolkit-stack-name', { - default: undefined, - type: 'string', - desc: 'The name of the existing CDK toolkit stack', - requiresArg: true, - }) .option('force', { default: false, type: 'boolean', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index db1b8fff3..cf6c710dd 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1014,13 +1014,6 @@ export interface PublishOptions { */ readonly exclusively?: boolean; - /** - * The name of the existing CDK toolkit stack - * - * @default - undefined - */ - readonly toolkitStackName?: string; - /** * Always publish assets, even if they are already published * From ff5920e4eda7aa3457a9de874cbc4e1829149c55 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:43:44 +0900 Subject: [PATCH 29/33] add createBuildAssetFunction and createPublishAssetFunction --- .../toolkit-lib/lib/toolkit/toolkit.ts | 111 +++++++++--------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 0a15099d5..93e2938f8 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -511,35 +511,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { const deployments = await this.deploymentsForAction('publish'); - const buildAsset = async (assetNode: AssetBuildNode) => { - const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ - asset: assetNode.asset, - }); - await deployments.buildSingleAsset( - assetNode.assetManifestArtifact, - assetNode.assetManifest, - assetNode.asset, - { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - }, - ); - await buildAssetSpan.end(); - }; - - const publishAsset = async (assetNode: AssetPublishNode) => { - const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({ - asset: assetNode.asset, - }); - await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - forcePublish: options.force, - }); - await publishAssetSpan.end(); - }; + const buildAsset = this.createBuildAssetFunction(ioHelper, deployments, options.roleArn); + const publishAsset = this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.force); const stacks = stackCollection.stackArtifacts; const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [ @@ -654,35 +627,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { const stackOutputs: { [key: string]: any } = {}; const outputsFile = options.outputsFile; - const buildAsset = async (assetNode: AssetBuildNode) => { - const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ - asset: assetNode.asset, - }); - await deployments.buildSingleAsset( - assetNode.assetManifestArtifact, - assetNode.assetManifest, - assetNode.asset, - { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - }, - ); - await buildAssetSpan.end(); - }; - - const publishAsset = async (assetNode: AssetPublishNode) => { - const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({ - asset: assetNode.asset, - }); - await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { - stack: assetNode.parentStack, - roleArn: options.roleArn, - stackName: assetNode.parentStack.stackName, - forcePublish: options.forceAssetPublishing, - }); - await publishAssetSpan.end(); - }; + const buildAsset = this.createBuildAssetFunction(ioHelper, deployments, options.roleArn); + const publishAsset = this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.forceAssetPublishing); const deployStack = async (stackNode: StackNode) => { const stack = stackNode.stack; @@ -1507,6 +1453,55 @@ export class Toolkit extends CloudAssemblySourceBuilder { throw new ToolkitError(`Unstable feature '${requestedFeature}' is not enabled. Please enable it under 'unstableFeatures'`); } } + + /** + * Create a buildAsset function for use in WorkGraph + */ + private createBuildAssetFunction( + ioHelper: IoHelper, + deployments: Deployments, + roleArn: string | undefined, + ) { + return async (assetNode: AssetBuildNode) => { + const buildAssetSpan = await ioHelper.span(SPAN.BUILD_ASSET).begin({ + asset: assetNode.asset, + }); + await deployments.buildSingleAsset( + assetNode.assetManifestArtifact, + assetNode.assetManifest, + assetNode.asset, + { + stack: assetNode.parentStack, + roleArn, + stackName: assetNode.parentStack.stackName, + }, + ); + await buildAssetSpan.end(); + }; + } + + /** + * Create a publishAsset function for use in WorkGraph + */ + private createPublishAssetFunction( + ioHelper: IoHelper, + deployments: Deployments, + roleArn: string | undefined, + forcePublish?: boolean, + ) { + return async (assetNode: AssetPublishNode) => { + const publishAssetSpan = await ioHelper.span(SPAN.PUBLISH_ASSET).begin({ + asset: assetNode.asset, + }); + await deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, { + stack: assetNode.parentStack, + roleArn, + stackName: assetNode.parentStack.stackName, + forcePublish, + }); + await publishAssetSpan.end(); + }; + } } /** From 4025ad859d8837c0f71c3fd517c7ac9daeca6b7a Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:56:21 +0900 Subject: [PATCH 30/33] rm assetParallelism --- .../@aws-cdk/toolkit-lib/lib/actions/publish/index.ts | 7 ------- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 8 -------- packages/aws-cdk/lib/cli/cli-config.ts | 1 - packages/aws-cdk/lib/cli/cli-type-registry.json | 4 ---- packages/aws-cdk/lib/cli/cli.ts | 1 - packages/aws-cdk/lib/cli/convert-to-user-input.ts | 2 -- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 5 ----- packages/aws-cdk/lib/cli/user-input.ts | 7 ------- packages/aws-cdk/test/cli/cli.test.ts | 7 +------ packages/aws-cdk/test/commands/publish.test.ts | 2 -- 11 files changed, 2 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts index 903ee2e65..31327c862 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts @@ -16,13 +16,6 @@ export interface PublishOptions { */ readonly force?: boolean; - /** - * Whether to build/publish assets in parallel - * - * @default true - */ - readonly assetParallelism?: boolean; - /** * Maximum number of simultaneous asset publishing operations * diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 93e2938f8..fb72aa876 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -542,7 +542,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { const graphConcurrency: Concurrency = { 'stack': 1, 'asset-build': 1, - 'asset-publish': (options.assetParallelism ?? true) ? (options.concurrency ?? 8) : 1, + 'asset-publish': options.concurrency ?? 1, }; await workGraph.doParallel(graphConcurrency, { diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 70eb6cfd7..4d303041b 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -799,7 +799,6 @@ export class CdkToolkit { expand: options.exclusively ? ExpandStackSelection.NONE : ExpandStackSelection.UPSTREAM, }, force: options.force, - assetParallelism: options.assetParallelism, concurrency: options.concurrency, roleArn: options.roleArn, }); @@ -1891,13 +1890,6 @@ export interface PublishOptions { */ readonly force?: boolean; - /** - * Whether to build/publish assets in parallel - * - * @default true - */ - readonly assetParallelism?: boolean; - /** * Maximum number of simultaneous asset publishing operations * diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 5443c598c..10e300cd7 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -254,7 +254,6 @@ export async function makeConfig(): Promise { 'all': { type: 'boolean', desc: 'Publish assets for all available stacks', default: false }, 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only publish assets for requested stacks, don\'t include dependencies' }, 'force': { type: 'boolean', alias: 'f', desc: 'Always publish assets, even if they are already published', default: false }, - 'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }, 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', default: 1, requiresArg: true }, }, }, diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 395059251..8ea2e9cca 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -624,10 +624,6 @@ "desc": "Always publish assets, even if they are already published", "default": false }, - "asset-parallelism": { - "type": "boolean", - "desc": "Whether to build/publish assets in parallel" - }, "concurrency": { "type": "number", "desc": "Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.", diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 82551bf34..83fadc64a 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -438,7 +438,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { alias: 'f', desc: 'Always publish assets, even if they are already published', }) - .option('asset-parallelism', { - default: undefined, - type: 'boolean', - desc: 'Whether to build/publish assets in parallel', - }) .option('concurrency', { default: 1, type: 'number', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index cf6c710dd..628aabdaa 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1023,13 +1023,6 @@ export interface PublishOptions { */ readonly force?: boolean; - /** - * Whether to build/publish assets in parallel - * - * @default - undefined - */ - readonly assetParallelism?: boolean; - /** * Maximum number of simultaneous asset publishing operations (dependency permitting) to execute. * diff --git a/packages/aws-cdk/test/cli/cli.test.ts b/packages/aws-cdk/test/cli/cli.test.ts index 095ef5d71..0c247a001 100644 --- a/packages/aws-cdk/test/cli/cli.test.ts +++ b/packages/aws-cdk/test/cli/cli.test.ts @@ -76,9 +76,6 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({ if (args.includes('--force')) { result = { ...result, force: true }; } - if (args.includes('--asset-parallelism')) { - result = { ...result, assetParallelism: true }; - } const concurrencyIndex = args.findIndex((arg: string) => arg === '--concurrency'); if (concurrencyIndex !== -1 && args[concurrencyIndex + 1]) { result = { ...result, concurrency: parseInt(args[concurrencyIndex + 1], 10) }; @@ -587,7 +584,6 @@ describe('publish command tests', () => { settings: { get: jest.fn().mockImplementation((key: string[]) => { if (key[0] === 'unstable') return ['publish']; - if (key[0] === 'assetParallelism') return true; return undefined; }), }, @@ -598,13 +594,12 @@ describe('publish command tests', () => { Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig); - await exec(['publish', '--unstable=publish', '--exclusively', '--force', '--asset-parallelism', '--concurrency', '4']); + await exec(['publish', '--unstable=publish', '--exclusively', '--force', '--concurrency', '4']); expect(publishSpy).toHaveBeenCalledWith( expect.objectContaining({ exclusively: true, force: true, - assetParallelism: true, concurrency: 4, }), ); diff --git a/packages/aws-cdk/test/commands/publish.test.ts b/packages/aws-cdk/test/commands/publish.test.ts index 5edbcc04b..b857a1c83 100644 --- a/packages/aws-cdk/test/commands/publish.test.ts +++ b/packages/aws-cdk/test/commands/publish.test.ts @@ -74,7 +74,6 @@ describe('publish', () => { await toolkit.publish({ selector: { patterns: ['Stack1'] }, force: true, - assetParallelism: false, concurrency: 5, roleArn: 'arn:aws:iam::123456789012:role/TestRole', }); @@ -84,7 +83,6 @@ describe('publish', () => { expect.anything(), expect.objectContaining({ force: true, - assetParallelism: false, concurrency: 5, roleArn: 'arn:aws:iam::123456789012:role/TestRole', }), From 7b00e47a6db1d4eae8ce3def826479646ad66f0a Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:06:47 +0900 Subject: [PATCH 31/33] fix lint --- packages/aws-cdk/lib/cli/cli-config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 10e300cd7..d0515abb0 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -251,10 +251,10 @@ export async function makeConfig(): Promise { variadic: true, }, options: { - 'all': { type: 'boolean', desc: 'Publish assets for all available stacks', default: false }, - 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only publish assets for requested stacks, don\'t include dependencies' }, - 'force': { type: 'boolean', alias: 'f', desc: 'Always publish assets, even if they are already published', default: false }, - 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', default: 1, requiresArg: true }, + all: { type: 'boolean', desc: 'Publish assets for all available stacks', default: false }, + exclusively: { type: 'boolean', alias: 'e', desc: 'Only publish assets for requested stacks, don\'t include dependencies' }, + force: { type: 'boolean', alias: 'f', desc: 'Always publish assets, even if they are already published', default: false }, + concurrency: { type: 'number', desc: 'Maximum number of simultaneous asset publishing operations (dependency permitting) to execute.', default: 1, requiresArg: true }, }, }, 'import': { From 9e96c9adbed704a3eb39ad9e4cd6ba1a7263fb6e Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:11:53 +0900 Subject: [PATCH 32/33] add publish to BUNDLING_COMMANDS --- packages/aws-cdk/lib/cli/user-configuration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/aws-cdk/lib/cli/user-configuration.ts b/packages/aws-cdk/lib/cli/user-configuration.ts index b3526f960..207afe2b0 100644 --- a/packages/aws-cdk/lib/cli/user-configuration.ts +++ b/packages/aws-cdk/lib/cli/user-configuration.ts @@ -50,6 +50,7 @@ const BUNDLING_COMMANDS = [ Command.SYNTHESIZE, Command.WATCH, Command.IMPORT, + Command.PUBLISH, ]; export type Arguments = { From bcfc4399b45096b0ca351fad6e3b59b077832acd Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:14:50 +0900 Subject: [PATCH 33/33] refactor --- .../@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index fb72aa876..4c6f6e588 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -511,9 +511,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { const deployments = await this.deploymentsForAction('publish'); - const buildAsset = this.createBuildAssetFunction(ioHelper, deployments, options.roleArn); - const publishAsset = this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.force); - const stacks = stackCollection.stackArtifacts; const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [ stack, @@ -549,8 +546,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { deployStack: async () => { // No-op: we're only publishing assets, not deploying }, - buildAsset, - publishAsset, + buildAsset: this.createBuildAssetFunction(ioHelper, deployments, options.roleArn), + publishAsset: this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.force), }); await ioHelper.defaults.info(chalk.green('\n✨ Assets published successfully\n')); @@ -627,9 +624,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { const stackOutputs: { [key: string]: any } = {}; const outputsFile = options.outputsFile; - const buildAsset = this.createBuildAssetFunction(ioHelper, deployments, options.roleArn); - const publishAsset = this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.forceAssetPublishing); - const deployStack = async (stackNode: StackNode) => { const stack = stackNode.stack; if (stackCollection.stackCount !== 1) { @@ -883,8 +877,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { await workGraph.doParallel(graphConcurrency, { deployStack, - buildAsset, - publishAsset, + buildAsset: this.createBuildAssetFunction(ioHelper, deployments, options.roleArn), + publishAsset: this.createPublishAssetFunction(ioHelper, deployments, options.roleArn, options.forceAssetPublishing), }); return ret;