Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8dad596
init
go-to-k Jan 11, 2026
a267160
wip for assetParallelism
go-to-k Jan 11, 2026
4196ccc
modify
go-to-k Jan 12, 2026
8be83e6
integ
go-to-k Jan 12, 2026
ef1e062
tweak
go-to-k Jan 12, 2026
5240325
tweak
go-to-k Jan 12, 2026
7ca3fdf
README
go-to-k Jan 12, 2026
5fa07b2
fix
go-to-k Jan 12, 2026
f137fcc
Merge branch 'main' into publish
go-to-k Jan 12, 2026
78db795
comments
go-to-k Jan 12, 2026
81fa49b
removePublishedAssets
go-to-k Jan 12, 2026
d2abc2b
publish.test
go-to-k Jan 12, 2026
303f186
imports
go-to-k Jan 12, 2026
bd89d27
Merge branch 'main' of https://github.com/go-to-k/aws-cdk-cli into pu…
go-to-k Jan 17, 2026
e592c9e
mock
go-to-k Jan 17, 2026
a0ee290
mock
go-to-k Jan 17, 2026
69980aa
Merge branch 'main' into publish
go-to-k Jan 17, 2026
d2aed1f
README
go-to-k Jan 17, 2026
afb379c
Merge branch 'main' of https://github.com/go-to-k/aws-cdk-cli into pu…
go-to-k Feb 22, 2026
06769f2
implement in toolkit-lib
go-to-k Feb 22, 2026
170fc97
rm success
go-to-k Feb 22, 2026
c039eb6
exclusively
go-to-k Feb 22, 2026
9ac288c
rm toolkitStackName
go-to-k Feb 22, 2026
514989a
use concurrency
go-to-k Feb 22, 2026
198eeef
response
go-to-k Feb 22, 2026
678377f
test
go-to-k Feb 22, 2026
31b4858
publishedAssets
go-to-k Feb 22, 2026
4d8e80f
improve
go-to-k Feb 22, 2026
5a10ccf
fix
go-to-k Feb 22, 2026
c199031
modify
go-to-k Feb 23, 2026
51b4b1d
rm
go-to-k Feb 23, 2026
86ddcff
implement in toolkit-lib
go-to-k Feb 23, 2026
2b4ce75
forgot to build
go-to-k Feb 23, 2026
ff5920e
add createBuildAssetFunction and createPublishAssetFunction
go-to-k Feb 23, 2026
4025ad8
rm assetParallelism
go-to-k Feb 23, 2026
7b00e47
fix lint
go-to-k Feb 23, 2026
9e96c9a
add publish to BUNDLING_COMMANDS
go-to-k Feb 23, 2026
bcfc439
refactor
go-to-k Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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',
withDefaultFixture(async (fixture) => {
const stackName = 'lambda';
const fullStackName = fixture.fullStackName(stackName);

const output = await fixture.cdk(['publish', fullStackName, '--unstable=publish']);
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/);
}),
);
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
39 changes: 39 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/publish/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { IManifestEntry } from '@aws-cdk/cdk-assets-lib';
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;

/**
* 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 {
/**
* List of assets that were published
*/
readonly publishedAssets: IManifestEntry[];
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ToolkitAction =
| 'deploy'
| 'drift'
| 'rollback'
| 'publish'
| 'watch'
| 'destroy'
| 'doctor'
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/payloads/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/payloads/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PublishResult } from '../actions';

export interface PublishResultPayload {
/**
* The publish result
*/
readonly result: PublishResult;
}
153 changes: 121 additions & 32 deletions packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -488,6 +489,75 @@ 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<PublishResult> {
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 {
publishedAssets: [],
};
}

const deployments = await this.deploymentsForAction('publish');

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);
}

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,
'asset-build': 1,
'asset-publish': options.concurrency ?? 1,
};

await workGraph.doParallel(graphConcurrency, {
deployStack: async () => {
// No-op: we're only publishing assets, not deploying
},
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'));
const publishedAssets = assetNodes.map(n => n.asset);

return {
publishedAssets,
};
}

/**
* List Action
*
Expand Down Expand Up @@ -554,36 +624,6 @@ 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 deployStack = async (stackNode: StackNode) => {
const stack = stackNode.stack;
if (stackCollection.stackCount !== 1) {
Expand Down Expand Up @@ -837,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;
Expand Down Expand Up @@ -1407,6 +1447,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();
};
}
}

/**
Expand Down
100 changes: 100 additions & 0 deletions packages/@aws-cdk/toolkit-lib/test/actions/publish.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { StackSelectionStrategy } from '../../lib/api/cloud-assembly';
import * as deployments from '../../lib/api/deployments';
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.publishedAssets.length).toBeGreaterThan(0);
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.publishedAssets.length).toBeGreaterThan(0);
ioHost.expectMessage({ containing: 'Assets published successfully', level: 'info' });
});

test('returns result 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.publishedAssets.length).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.publishedAssets.length).toBeGreaterThan(0);
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.publishedAssets.length).toBeGreaterThan(0);
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.publishedAssets.length).toBe(0);
ioHost.expectMessage({ containing: 'All assets are already published', level: 'info' });
});
});
Loading
Loading